Skip to main content

第2回 盤面を描画するまで

第2回 コンソール版の盤面をDxLibで表示しよう

今回やること

第1回では、

TITLE → PLAY → CLEAR → TITLE

というゲーム全体の流れを作りました。

今回はPLAY画面へ、コンソール版で作ったスライドパズルの盤面を表示します。

パズルの仕組みを最初から作り直すのではありません。

コンソール版で作った、

  • 盤面データ
  • タイルを探す処理
  • 動かせるか調べる処理
  • タイルを動かす処理
  • クリア判定
  • シャッフル

は、そのまま利用します。

パズルの中身を作り直す必要はありません。

今回、DxLib用に作り替えるのは、文字で盤面を表示していたPrintBoard()の部分です。

コンソール版       DxLib版

PrintBoard()  →  DrawBoard()

つまり今回は、パズルのロジックはそのままにして、表示方法だけをDxLibへ置き換えるところから始めます。


1.盤面のデータを持ってこよう

コンソール版で使った盤面を、DxLib版でも使います。

第1回のコードへ、次の定義を追加してください。

#define BOARD_WIDTH  4
#define BOARD_HEIGHT 4
#define BLANK_TILE (BOARD_WIDTH * BOARD_HEIGHT)
#define SHUFFLE_COUNT 100

struct Board
{
    int tile[BOARD_WIDTH * BOARD_HEIGHT];
};

struct Position
{
    int x;
    int y;
};

変数を作るときは、

struct Board board;

のように書きます。

struct Boardの中にあるtile[]も、コンソール版で使っていた盤面データと同じです。

ここでは、コンソール版で作った盤面データをそのままDxLib版でも使います。


2.コンソール版の関数を移そう

次の関数は、コンソール版で作ったものをそのまま使います。

void InitBoard(struct Board* board);
struct Position SearchTile(struct Board* board, int number);
int CanMoveTile(struct Board* board, int number);
void MoveTile(struct Board* board, int number);
int IsClear(struct Board* board);
void ShuffleBoard(struct Board* board);

これらのプロトタイプ宣言を、WinMain()より前へ追加してください。

また、ShuffleBoard()の中ではrand()を使っているので、

#include <stdlib.h>

も追加します。

time.hとsrand()は、実際にシャッフルを使い始める第4回で追加します。

関数の定義は、コンソール版からコピーして、第1回で作った関数の下へ追加してください。

必要な場合は、次のコードを使えます。

コンソール版から持ってくる関数を見る
void InitBoard(struct Board* board)
{
    for (int i = 0; i < BOARD_WIDTH * BOARD_HEIGHT; i++)
    {
        board->tile[i] = i + 1;
    }
}

struct Position SearchTile(struct Board* board, int number)
{
    struct Position result = { -1, -1 };

    for (int y = 0; y < BOARD_HEIGHT; y++)
    {
        for (int x = 0; x < BOARD_WIDTH; x++)
        {
            int index = y * BOARD_WIDTH + x;

            if (board->tile[index] == number)
            {
                result.x = x;
                result.y = y;
                return result;
            }
        }
    }

    return result;
}

int CanMoveTile(struct Board* board, int number)
{
    struct Position blank = SearchTile(board, BLANK_TILE);
    struct Position tile = SearchTile(board, number);

    int dx = blank.x - tile.x;
    int dy = blank.y - tile.y;

    if (dx * dx + dy * dy == 1)
        return 1;

    return 0;
}

void MoveTile(struct Board* board, int number)
{
    if (CanMoveTile(board, number) == 0)
        return;

    struct Position blank = SearchTile(board, BLANK_TILE);
    struct Position tile = SearchTile(board, number);

    int blankIndex = blank.y * BOARD_WIDTH + blank.x;
    int tileIndex = tile.y * BOARD_WIDTH + tile.x;

    int temp = board->tile[blankIndex];
    board->tile[blankIndex] = board->tile[tileIndex];
    board->tile[tileIndex] = temp;
}

int IsClear(struct Board* board)
{
    for (int i = 0; i < BOARD_WIDTH * BOARD_HEIGHT; i++)
    {
        if (board->tile[i] != i + 1)
            return 0;
    }

    return 1;
}

void ShuffleBoard(struct Board* board)
{
    while (IsClear(board))
    {
        for (int i = 0; i < SHUFFLE_COUNT; i++)
        {
            int number =
                rand() % (BLANK_TILE - 1) + 1;

            if (CanMoveTile(board, number))
            {
                MoveTile(board, number);
            }
        }
    }
}

ここで一度ビルドしよう

まだ画面は第1回と同じです。

この段階では、

コンソール版のパズル処理をDxLibのプログラムへ持ってきても、エラーなくビルドできる

ことだけ確認します。


3.盤面を作って初期化しよう

次に、WinMain()の中へ盤面を作ります。

stateを作っているところの近くに追加してください。

struct Board board;
enum GameState state = GAME_STATE_TITLE;

ゲームループへ入る前に、盤面を完成状態へ初期化します。

穴埋め

// 盤面を完成状態へ初期化する
______________________________;
模範解答を見る
InitBoard(&board);

まだ盤面は画面に描いていないので、見た目は変わりません。

ここでも一度ビルドして、エラーがないことを確認しておきましょう。


4.盤面を置く場所を決めよう

今度は、盤面を画面のどこへ描くのか決めます。

タイル1枚の大きさと、タイル同士の隙間を次のようにします。

#define TILE_SIZE 80
#define TILE_GAP   6

#define BOARD_X 24
#define BOARD_Y 80

BOARD_XとBOARD_Yは、盤面の左上の位置です。

画面の左上 (0, 0)

        BOARD_X
     ─────────→

     ┌──────────────
     │
     │  (BOARD_X, BOARD_Y)
     │       ↓
     │      盤面
     │
     ↓
   BOARD_Y

DxLibの画面座標は、

  • xが大きくなると右へ進む
  • yが大きくなると下へ進む

ようになっています。


5.盤面全体の大きさを求めよう

横にタイルが4枚ある場合、タイル同士の隙間は3個あります。

タイル  隙間  タイル  隙間  タイル  隙間  タイル
  80     6      80     6      80     6      80

そのため、

タイル4枚分 + 隙間3個分

で盤面の幅を求められます。

穴埋め

// 横のタイル全部と、タイルの間の隙間から盤面の幅を求める
#define BOARD_PIXEL_WIDTH  (____________________________________________)

// 縦のタイル全部と、タイルの間の隙間から盤面の高さを求める
#define BOARD_PIXEL_HEIGHT (____________________________________________)
模範解答を見る
#define BOARD_PIXEL_WIDTH \
    (BOARD_WIDTH * TILE_SIZE + (BOARD_WIDTH - 1) * TILE_GAP)

#define BOARD_PIXEL_HEIGHT \
    (BOARD_HEIGHT * TILE_SIZE + (BOARD_HEIGHT - 1) * TILE_GAP)

第1回で作った、

#define SCREEN_WIDTH  480
#define SCREEN_HEIGHT 480

は削除して、次の定義へ置き換えます。

#define SCREEN_WIDTH \
    (BOARD_X * 2 + BOARD_PIXEL_WIDTH)

#define SCREEN_HEIGHT \
    (BOARD_Y + BOARD_PIXEL_HEIGHT + 40)

これで、盤面の大きさからウィンドウサイズを決められます。


6.タイル1枚の描画位置を考えよう

盤面の左上は、

BOARD_X
BOARD_Y

です。

では、横方向のxが変わったとき、各タイルの左端はどこになるでしょうか。

x = 0 のとき

一番左のタイルなので、

left = BOARD_X;

です。

x = 1 のとき

タイル1枚分と、隙間1個分だけ右へ進みます。

left = BOARD_X + TILE_SIZE + TILE_GAP;

x = 2 のとき

同じ幅を2回分進みます。

left = BOARD_X + 2 * (TILE_SIZE + TILE_GAP);

まとめると、

left = BOARD_X + x * (TILE_SIZE + TILE_GAP);

と書けます。

縦方向も同じ考え方です。

top = BOARD_Y + y * (TILE_SIZE + TILE_GAP);


7.タイル画像を読み込もう

ここまでは、タイルの位置を計算しました。

今度は、その位置へタイル画像を表示します。

教材のtile_imagesフォルダには、

tile_01.png
tile_02.png
...
tile_15.png

という15枚の画像が入っています。

1枚の大きさは、今回のTILE_SIZEと同じ80×80ピクセルです。

tile_imagesフォルダは、フォルダごとプロジェクトへ置いてください。

タイル番号と画像を対応させる

WinMain()の中に、画像ハンドルを保存する配列を用意します。

struct Board board;
int tileGraph[BLANK_TILE];
enum GameState state = GAME_STATE_TITLE;

今回は、

1番のタイル  → tileGraph[1]
2番のタイル  → tileGraph[2]
...
15番のタイル → tileGraph[15]

という対応にします。

つまり、盤面に入っているタイル番号をそのまま画像の番号として使えます。

画像を読み込む

次のプロトタイプ宣言を追加してください。

void LoadTileImages(int tileGraph[]);
void DeleteTileImages(int tileGraph[]);

画像の読み込みと削除は、次のコードをそのまま使います。

タイル画像の読み込み処理を見る
void LoadTileImages(int tileGraph[])
{
    tileGraph[0] = -1;

    tileGraph[1]  = LoadGraph("tile_images/tile_01.png");
    tileGraph[2]  = LoadGraph("tile_images/tile_02.png");
    tileGraph[3]  = LoadGraph("tile_images/tile_03.png");
    tileGraph[4]  = LoadGraph("tile_images/tile_04.png");
    tileGraph[5]  = LoadGraph("tile_images/tile_05.png");
    tileGraph[6]  = LoadGraph("tile_images/tile_06.png");
    tileGraph[7]  = LoadGraph("tile_images/tile_07.png");
    tileGraph[8]  = LoadGraph("tile_images/tile_08.png");
    tileGraph[9]  = LoadGraph("tile_images/tile_09.png");
    tileGraph[10] = LoadGraph("tile_images/tile_10.png");
    tileGraph[11] = LoadGraph("tile_images/tile_11.png");
    tileGraph[12] = LoadGraph("tile_images/tile_12.png");
    tileGraph[13] = LoadGraph("tile_images/tile_13.png");
    tileGraph[14] = LoadGraph("tile_images/tile_14.png");
    tileGraph[15] = LoadGraph("tile_images/tile_15.png");
}

void DeleteTileImages(int tileGraph[])
{
    for (int i = 1; i < BLANK_TILE; i++)
    {
        DeleteGraph(tileGraph[i]);
    }
}

DxLib_Init()が成功したあと、ゲームループへ入る前に読み込みます。

SetDrawScreen(DX_SCREEN_BACK);

LoadTileImages(tileGraph);

InitBoard(&board);

終了するときは、DxLib_End()の前で削除します。

DeleteTileImages(tileGraph);

DxLib_End();

DrawGraph()で画像を描く

画像を表示するときはDrawGraph()を使います。

DrawGraph(
    x,
    y,
    画像ハンドル,
    TRUE);

例えば1番なら、

DrawGraph(
    left,
    top,
    tileGraph[1],
    TRUE);

です。

今回は画像自体が80×80ピクセルなので、そのまま描けばTILE_SIZEとぴったり合います。


8.DrawBoard()を作ろう

盤面全体を描くDrawBoard()を作ります。

今回は、盤面データとタイル画像の両方を使います。

void DrawBoard(
    struct Board* board,
    int tileGraph[]);

穴埋め

void DrawBoard(
    struct Board* board,
    int tileGraph[])
{
    for (int y = 0; y < BOARD_HEIGHT; y++)
    {
        for (int x = 0; x < BOARD_WIDTH; x++)
        {
            // xとyから、1次元配列の添字を求める
            int index = ______________________________;

            // xから、このタイルの画面上の左端を求める
            int left = ______________________________;

            // yから、このタイルの画面上の上端を求める
            int top = ______________________________;

            // このマスに入っているタイル番号を取り出す
            int number = ______________________________;

            // 空白以外なら、その番号の画像を描く
            if (number != BLANK_TILE)
            {
                DrawGraph(
                    left,
                    top,
                    ______________________________,
                    TRUE);
            }
        }
    }
}
模範解答を見る
void DrawBoard(
    struct Board* board,
    int tileGraph[])
{
    for (int y = 0; y < BOARD_HEIGHT; y++)
    {
        for (int x = 0; x < BOARD_WIDTH; x++)
        {
            int index =
                y * BOARD_WIDTH + x;

            int left =
                BOARD_X + x * (TILE_SIZE + TILE_GAP);

            int top =
                BOARD_Y + y * (TILE_SIZE + TILE_GAP);

            int number = board->tile[index];

            if (number != BLANK_TILE)
            {
                DrawGraph(
                    left,
                    top,
                    tileGraph[number],
                    TRUE);
            }
        }
    }
}

ここでは、

int number = board->tile[index];

でタイル番号を取り出しています。

その番号を、

tileGraph[number]

にもそのまま使います。

例えば12番のタイルなら、

board->tile[index] → 12
        ↓
tileGraph[12]
        ↓
12番の画像を描く

となります。


9.PlayDraw()から盤面を描こう

第1回のPlayDraw()は、

void PlayDraw(void);

でした。

今回は盤面とタイル画像を描くので、BoardとtileGraph[]を受け取る形へ変更します。

プロトタイプ宣言を、

void PlayDraw(struct Board* board, int tileGraph[]);

へ変更してください。

関数の定義も変更します。

穴埋め

void PlayDraw(struct Board* board, int tileGraph[])
{
    // プレイ画面の見出しを描く
    DrawString(
        24, 24,
        "SLIDE PUZZLE",
        GetColor(255, 255, 255));

    // 現在の盤面をタイル画像で描く
    ______________________________;
}
模範解答を見る
void PlayDraw(struct Board* board, int tileGraph[])
{
    DrawString(
        24, 24,
        "SLIDE PUZZLE",
        GetColor(255, 255, 255));

    DrawBoard(board, tileGraph);
}

最後に、ゲームループの、

PlayDraw();

を、

PlayDraw(&board, tileGraph);

へ変更します。

boardは3節ですでに作ってあるので、この段階でもそのままビルドできます。


10.動作確認しよう

実行して、次の順番で確認してください。

  1. 起動するとタイトル画面が表示される
  2. タイトル画面をクリックしてPLAYへ進む
  3. 4×4のタイルが表示される
  4. 1~15のタイル画像が順番に並んでいる
  5. 16番にあたる右下の場所は空白になっている
  6. タイル同士の間に隙間がある

ここまでできれば完成です。

第1回で作ったPlayUpdate()には、まだ、

クリックしたらCLEARへ進む

という動作確認用の処理が残っています。

そのため、PLAY画面で盤面を見るときはクリックしないでください。

次回、この仮処理を外して、クリックを本当のタイル操作へ使います。


11.第2回の完成コード

途中で分からなくなった場合は、完成した形と比べて確認してください。

第2回の完成コードを見る
#include "DxLib.h"
#include <stdlib.h>

#define BOARD_WIDTH  4
#define BOARD_HEIGHT 4
#define BLANK_TILE (BOARD_WIDTH * BOARD_HEIGHT)
#define SHUFFLE_COUNT 100

#define TILE_SIZE 80
#define TILE_GAP   6

#define BOARD_X 24
#define BOARD_Y 80

#define BOARD_PIXEL_WIDTH \
    (BOARD_WIDTH * TILE_SIZE + (BOARD_WIDTH - 1) * TILE_GAP)

#define BOARD_PIXEL_HEIGHT \
    (BOARD_HEIGHT * TILE_SIZE + (BOARD_HEIGHT - 1) * TILE_GAP)

#define SCREEN_WIDTH \
    (BOARD_X * 2 + BOARD_PIXEL_WIDTH)

#define SCREEN_HEIGHT \
    (BOARD_Y + BOARD_PIXEL_HEIGHT + 40)

enum GameState
{
    GAME_STATE_TITLE,
    GAME_STATE_PLAY,
    GAME_STATE_CLEAR
};

struct Board
{
    int tile[BOARD_WIDTH * BOARD_HEIGHT];
};

struct Position
{
    int x;
    int y;
};

int IsMouseLeftPressed(void);

void InitBoard(struct Board* board);
struct Position SearchTile(struct Board* board, int number);
int CanMoveTile(struct Board* board, int number);
void MoveTile(struct Board* board, int number);
int IsClear(struct Board* board);
void ShuffleBoard(struct Board* board);

void LoadTileImages(int tileGraph[]);
void DeleteTileImages(int tileGraph[]);
void DrawBoard(struct Board* board, int tileGraph[]);

void TitleUpdate(enum GameState* state);
void TitleDraw(void);

void PlayUpdate(enum GameState* state);
void PlayDraw(struct Board* board, int tileGraph[]);

void ClearUpdate(enum GameState* state);
void ClearDraw(void);

int WINAPI WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow)
{
    struct Board board;
    int tileGraph[BLANK_TILE];
    enum GameState state = GAME_STATE_TITLE;

    ChangeWindowMode(TRUE);
    SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);
    SetMainWindowText("Slide Puzzle");

    if (DxLib_Init() != 0)
        return -1;

    SetDrawScreen(DX_SCREEN_BACK);

    LoadTileImages(tileGraph);

    InitBoard(&board);

    while (ProcessMessage() == 0)
    {
        ClearDrawScreen();

        DrawBox(
            0, 0,
            SCREEN_WIDTH, SCREEN_HEIGHT,
            GetColor(70, 100, 110),
            TRUE);

        switch (state)
        {
        case GAME_STATE_TITLE:
            TitleUpdate(&state);
            TitleDraw();
            break;

        case GAME_STATE_PLAY:
            PlayUpdate(&state);
            PlayDraw(&board, tileGraph);
            break;

        case GAME_STATE_CLEAR:
            ClearUpdate(&state);
            ClearDraw();
            break;
        }

        ScreenFlip();
    }

    DeleteTileImages(tileGraph);

    DxLib_End();

    return 0;
}

int IsMouseLeftPressed(void)
{
    static int previousInput = 0;

    int currentInput = GetMouseInput();

    int pressed =
        (currentInput & MOUSE_INPUT_LEFT) != 0 &&
        (previousInput & MOUSE_INPUT_LEFT) == 0;

    previousInput = currentInput;

    return pressed;
}

void InitBoard(struct Board* board)
{
    for (int i = 0; i < BOARD_WIDTH * BOARD_HEIGHT; i++)
    {
        board->tile[i] = i + 1;
    }
}

struct Position SearchTile(struct Board* board, int number)
{
    struct Position result = { -1, -1 };

    for (int y = 0; y < BOARD_HEIGHT; y++)
    {
        for (int x = 0; x < BOARD_WIDTH; x++)
        {
            int index = y * BOARD_WIDTH + x;

            if (board->tile[index] == number)
            {
                result.x = x;
                result.y = y;
                return result;
            }
        }
    }

    return result;
}

int CanMoveTile(struct Board* board, int number)
{
    struct Position blank = SearchTile(board, BLANK_TILE);
    struct Position tile = SearchTile(board, number);

    int dx = blank.x - tile.x;
    int dy = blank.y - tile.y;

    if (dx * dx + dy * dy == 1)
        return 1;

    return 0;
}

void MoveTile(struct Board* board, int number)
{
    if (CanMoveTile(board, number) == 0)
        return;

    struct Position blank = SearchTile(board, BLANK_TILE);
    struct Position tile = SearchTile(board, number);

    int blankIndex = blank.y * BOARD_WIDTH + blank.x;
    int tileIndex = tile.y * BOARD_WIDTH + tile.x;

    int temp = board->tile[blankIndex];
    board->tile[blankIndex] = board->tile[tileIndex];
    board->tile[tileIndex] = temp;
}

int IsClear(struct Board* board)
{
    for (int i = 0; i < BOARD_WIDTH * BOARD_HEIGHT; i++)
    {
        if (board->tile[i] != i + 1)
            return 0;
    }

    return 1;
}

void ShuffleBoard(struct Board* board)
{
    while (IsClear(board))
    {
        for (int i = 0; i < SHUFFLE_COUNT; i++)
        {
            int number =
                rand() % (BLANK_TILE - 1) + 1;

            if (CanMoveTile(board, number))
            {
                MoveTile(board, number);
            }
        }
    }
}

void LoadTileImages(int tileGraph[])
{
    tileGraph[0] = -1;

    tileGraph[1]  = LoadGraph("tile_images/tile_01.png");
    tileGraph[2]  = LoadGraph("tile_images/tile_02.png");
    tileGraph[3]  = LoadGraph("tile_images/tile_03.png");
    tileGraph[4]  = LoadGraph("tile_images/tile_04.png");
    tileGraph[5]  = LoadGraph("tile_images/tile_05.png");
    tileGraph[6]  = LoadGraph("tile_images/tile_06.png");
    tileGraph[7]  = LoadGraph("tile_images/tile_07.png");
    tileGraph[8]  = LoadGraph("tile_images/tile_08.png");
    tileGraph[9]  = LoadGraph("tile_images/tile_09.png");
    tileGraph[10] = LoadGraph("tile_images/tile_10.png");
    tileGraph[11] = LoadGraph("tile_images/tile_11.png");
    tileGraph[12] = LoadGraph("tile_images/tile_12.png");
    tileGraph[13] = LoadGraph("tile_images/tile_13.png");
    tileGraph[14] = LoadGraph("tile_images/tile_14.png");
    tileGraph[15] = LoadGraph("tile_images/tile_15.png");
}

void DeleteTileImages(int tileGraph[])
{
    for (int i = 1; i < BLANK_TILE; i++)
    {
        DeleteGraph(tileGraph[i]);
    }
}

void DrawBoard(struct Board* board, int tileGraph[])
{
    for (int y = 0; y < BOARD_HEIGHT; y++)
    {
        for (int x = 0; x < BOARD_WIDTH; x++)
        {
            int index =
                y * BOARD_WIDTH + x;

            int left =
                BOARD_X + x * (TILE_SIZE + TILE_GAP);

            int top =
                BOARD_Y + y * (TILE_SIZE + TILE_GAP);

            int number = board->tile[index];

            if (number != BLANK_TILE)
            {
                DrawGraph(
                    left,
                    top,
                    tileGraph[number],
                    TRUE);
            }
        }
    }
}

void TitleUpdate(enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        *state = GAME_STATE_PLAY;
    }
}

void TitleDraw(void)
{
    DrawString(
        100, 150,
        "SLIDE PUZZLE",
        GetColor(255, 255, 255));

    DrawString(
        100, 200,
        "CLICK TO START",
        GetColor(255, 255, 255));
}

void PlayUpdate(enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        *state = GAME_STATE_CLEAR;
    }
}

void PlayDraw(struct Board* board, int tileGraph[])
{
    DrawString(
        24, 24,
        "SLIDE PUZZLE",
        GetColor(255, 255, 255));

    DrawBoard(board, tileGraph);
}

void ClearUpdate(enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        *state = GAME_STATE_TITLE;
    }
}

void ClearDraw(void)
{
    DrawString(
        100, 150,
        "CLEAR!",
        GetColor(255, 255, 255));
}

今回できるようになったこと

コンソール版の盤面データやパズル処理を作り直さず、表示だけをDxLibのタイル画像へ変更できました。

同じ struct Board
       ↓
コンソール版では PrintBoard()
       ↓
DxLib版では DrawBoard()

また、配列のx・yを使って、画面上のタイル位置も求められるようになりました。

次回は、キーボードから番号を入力する代わりに、マウスでタイルを選べるようにします。