Skip to main content

第4回 クリア判定(はとても簡単)と問題生成(すでにある)

第4回 スライドパズルをゲームとして完成させよう

今回やること

前回までで、

  • 盤面を描く
  • マウスでタイルを選ぶ
  • タイルを動かす

ところまでできました。

ただし、ゲームを始めても盤面は完成状態のままです。

今回は、コンソール版ですでに作った、

ShuffleBoard()
IsClear()

をゲームの流れへつなぎます。

今回が終わると、

TITLE
  ↓
問題を作る
  ↓
PLAY
  ↓
パズルを完成
  ↓
CLEAR
  ↓
TITLE

と、最初から最後まで遊べるスライドパズルになります。


1.乱数を毎回変えよう

ShuffleBoard()ではrand()を使って問題を作っています。

第2回ですでに、

#include <stdlib.h>

を追加しているので、今回はさらに、

#include <time.h>

を追加します。

そして、DxLib_Init()が成功したあと、ゲームループへ入る前に次の1行を追加してください。

srand((unsigned int)time(0));

time(0)で現在時刻を取得し、その値を使って乱数の並びを変えます。

srand()は1回だけ

srand()は、ゲームを始めるたびに呼ぶのではなく、プログラムを起動したときに1回だけ呼びます。

プログラム起動
    ↓
srand()
    ↓
ゲームループ

StartGame()の中へsrand()を入れる必要はありません。


2.ゲーム開始処理をまとめよう

タイトルをクリックしてゲームを始めるときは、

  1. 盤面を完成状態へ戻す
  2. その盤面をシャッフルする

という2つの処理を続けて行います。

そこで、この2つをまとめた、

StartGame()

を作ります。

まず、プロトタイプ宣言を追加してください。

void StartGame(struct Board* board);

穴埋め

void StartGame(struct Board* board)
{
    // 盤面を完成状態へ戻す
    ______________________________;

    // 完成状態の盤面をシャッフルして問題を作る
    ______________________________;
}
模範解答を見る
void StartGame(struct Board* board)
{
    InitBoard(board);
    ShuffleBoard(board);
}

StartGame()を呼べば、

完成状態へ戻す
      ↓
問題を作る

ところまでをまとめて行えるようになりました。


3.タイトルからゲームを始めよう

これまでのTitleUpdate()は、

void TitleUpdate(enum GameState* state);

でした。

今回は、タイトルをクリックしたときにStartGame()へ盤面を渡す必要があります。

そのため、まずプロトタイプ宣言を次の形へ変更します。

void TitleUpdate(
    struct Board* board,
    enum GameState* state);

次に、TitleUpdate()の定義も同じ形へ変更します。

穴埋め

void TitleUpdate(
    struct Board* board,
    enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        // 新しいパズルの問題を作る
        ______________________________;

        // プレイ画面へ変更する
        ______________________________;
    }
}
模範解答を見る
void TitleUpdate(
    struct Board* board,
    enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        StartGame(board);
        *state = GAME_STATE_PLAY;
    }
}

ゲームループ側も、

TitleUpdate(&state);

から、

TitleUpdate(&board, &state);

へ変更してください。

最後にInitBoard()を移そう

前回までは、ゲームループへ入る前に、

InitBoard(&board);

と初期化していました。

これからは、タイトルをクリックしたときにStartGame()が初期化するので、この1行は削除します。

代わりに先ほどの、

srand((unsigned int)time(0));

だけを置いておきます。

ここで一度実行しよう

  1. TITLEを表示する
  2. タイトルをクリックする
  3. PLAYへ進む
  4. 盤面が完成状態ではなく、シャッフルされている

ところまで確認してください。

ここまでで、ゲームを始めるたびに問題を作るところまで完成です。


4.タイルを動かしたあとにクリア判定しよう

次は、パズルが完成したらCLEAR画面へ進むようにします。

今のPlayUpdate()には、

マウスをクリック
      ↓
タイル番号を取得
      ↓
MoveTile()

まで入っています。

このあとに、

IsClear()

を追加します。

クリアしたときはstateも変更するので、PlayUpdate()にもstateを渡します。

まずプロトタイプ宣言を、

void PlayUpdate(struct Board* board);

から、

void PlayUpdate(
    struct Board* board,
    enum GameState* state);

へ変更してください。

ゲームループ側も、

PlayUpdate(&board);

から、

PlayUpdate(&board, &state);

へ変更します。

最後に、関数の定義を変更します。

穴埋め

void PlayUpdate(
    struct Board* board,
    enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        int mouseX;
        int mouseY;

        GetMousePoint(&mouseX, &mouseY);

        int number =
            GetClickedTile(board, mouseX, mouseY);

        if (number != -1)
        {
            // クリックしたタイルを動かす
            ______________________________;

            // タイルを動かしたあとの盤面が完成しているか調べる
            if (______________________________)
            {
                // ゲームの状態をクリア画面へ変更する
                ______________________________;
            }
        }
    }
}
模範解答を見る
void PlayUpdate(
    struct Board* board,
    enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        int mouseX;
        int mouseY;

        GetMousePoint(&mouseX, &mouseY);

        int number =
            GetClickedTile(board, mouseX, mouseY);

        if (number != -1)
        {
            MoveTile(board, number);

            if (IsClear(board))
            {
                *state = GAME_STATE_CLEAR;
            }
        }
    }
}

これで、

タイルを動かす
      ↓
IsClear()
      ↓
完成していたらCLEAR

と処理できるようになりました。


5.クリア画面からタイトルへ戻ろう

CLEAR画面からタイトルへ戻る処理は、第1回で作ったものをそのまま使えます。

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

CLEARからTITLEへ戻っただけでは、まだ新しい問題は作りません。

CLEAR
  ↓ クリック
TITLE
  ↓ クリック
StartGame()
  ↓
新しい問題

という順番になります。


6.クリア判定をすぐ試せるようにしよう

通常は、

#define SHUFFLE_COUNT 100

として、1回のシャッフル処理の中でランダムなタイル選択を100回試します。 選ばれたタイルが動かせない場合は移動しないので、100は「実際に100回タイルが動く」という意味ではありません。

通常の問題を実際に解いてCLEAR画面を確認するのは時間がかかるので、動作確認中だけ値を小さくします。

動作確認中だけ、

#define SHUFFLE_COUNT 1

へ変更してみましょう。

今のShuffleBoard()は、盤面が完成状態の間はシャッフルを続けます。

SHUFFLE_COUNTが1なら、

完成状態
  ↓
動かせない番号が選ばれた
  ↓
まだ完成状態なので続ける
  ↓
動かせる番号が選ばれた
  ↓
タイルを1回だけ動かす
  ↓
完成状態ではなくなったので終了

となります。

つまり、完成状態から合法手を1回だけ動かした問題ができます。

試してみよう

PLAY画面を見ると、12番か15番のどちらかが右下へ移動しています。

右下へ移動してきたタイルを1回クリックしてください。

1手で完成
  ↓
IsClear() が1
  ↓
CLEAR画面

となれば、クリア判定と状態遷移は成功です。

確認が終わったら、

#define SHUFFLE_COUNT 100

へ戻してください。


7.ゲームを一周してみよう

最後に、通常のSHUFFLE_COUNTへ戻してゲームを開始し、盤面が十分に崩れることを確認します。

  1. 起動するとTITLEが表示される
  2. タイトルをクリックすると新しい問題が作られる
  3. PLAY画面で盤面が崩れている
  4. マウスでタイルを動かせる
  5. 動かせるタイルだけが移動する
  6. CLEARへの遷移は、直前のSHUFFLE_COUNT 1のテストで確認済み
  7. CLEARからTITLEへ戻り、もう一度新しい問題を始められる

授業中のクリア遷移確認はSHUFFLE_COUNT 1で行えばよく、通常設定の問題を最後まで解く必要はありません。

ここまで動けば、スライドパズルの基本部分は完成です。


8.第4回の完成コード

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

第4回の完成コードを見る
#include "DxLib.h"
#include <stdlib.h>
#include <time.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);

int GetClickedTile(
    struct Board* board,
    int mouseX,
    int mouseY);

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

void TitleUpdate(
    struct Board* board,
    enum GameState* state);
void TitleDraw(void);

void PlayUpdate(
    struct Board* board,
    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);

    srand((unsigned int)time(0));

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

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

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

        case GAME_STATE_PLAY:
            PlayUpdate(&board, &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);
            }
        }
    }
}

int GetClickedTile(
    struct Board* board,
    int mouseX,
    int mouseY)
{
    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 insideLeft =
                mouseX >= left;

            int insideRight =
                mouseX < left + TILE_SIZE;

            int insideTop =
                mouseY >= top;

            int insideBottom =
                mouseY < top + TILE_SIZE;

            if (insideLeft &&
                insideRight &&
                insideTop &&
                insideBottom)
            {
                if (board->tile[index] == BLANK_TILE)
                    return -1;

                return board->tile[index];
            }
        }
    }

    return -1;
}

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 StartGame(struct Board* board)
{
    InitBoard(board);
    ShuffleBoard(board);
}

void TitleUpdate(
    struct Board* board,
    enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        StartGame(board);
        *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(
    struct Board* board,
    enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        int mouseX;
        int mouseY;

        GetMousePoint(&mouseX, &mouseY);

        int number =
            GetClickedTile(board, mouseX, mouseY);

        if (number != -1)
        {
            MoveTile(board, number);

            if (IsClear(board))
            {
                *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));
}

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

コンソール版で作った、

InitBoard()
ShuffleBoard()
MoveTile()
IsClear()

を、ゲーム全体の状態遷移へつなげられました。

TITLE
  ↓
StartGame()
  ├ InitBoard()
  └ ShuffleBoard()
  ↓
PLAY
  ↓
MoveTile()
  ↓
IsClear()
  ↓
CLEAR

これで、スライドパズルとして最初から最後まで遊べるようになりました。

次回は、最低限の文字だけだったTITLE画面とCLEAR画面を仕上げながら、今まで作った処理の役割を整理します。