Skip to main content

第6回 あとやれることある?

第6回 完成と自由制作

今回やること

前回までで、基本版のスライドパズルは完成しました。

今回は最初に、完成したプログラムが正しく動くことを確認します。

そのあと、自分で追加したい機能を1つ選んで、作品を仕上げてみよう!

自由制作でも、

何を追加する?
    ↓
どのデータが必要?
    ↓
どこで値を変える?
    ↓
どこで描く?
    ↓
実行して確認

という順番で考えていきます。


1.ここまで作ったゲームの流れ

第1回では、最初にゲーム全体の入れ物を作りました。

そこへ少しずつスライドパズルの処理を追加してきました。

TITLE
  │
  │ クリック
  ↓
StartGame()
  ├ InitBoard()
  └ ShuffleBoard()
  │
  ↓
PLAY
  │
  ├ GetClickedTile()
  ├ MoveTile()
  └ IsClear()
       │
       │ クリア
       ↓
CLEAR
  │
  │ クリック
  ↓
TITLE

コンソール版で作った、

InitBoard()
SearchTile()
CanMoveTile()
MoveTile()
IsClear()
ShuffleBoard()

は、DxLib版でもそのままパズルの中身として使えています。


2.基本版を最終確認しよう

自由制作へ進む前に、基本版が正しく動くか確認します。

  • 起動するとTITLE画面になる
  • TITLE画面にタイトルと開始方法が表示される
  • タイトルをクリックすると新しい問題が作られる
  • PLAY画面に4×4の盤面が表示される
  • 1~15のタイル番号が表示される
  • 空白タイルは描画されない
  • 空白の隣にあるタイルだけ動く
  • 空白から離れたタイルは動かない
  • 空白やタイルの隙間をクリックしても何も起こらない
  • マウスを押しっぱなしにしても何枚も連続で動かない
  • パズルを完成するとCLEAR画面へ進む
  • CLEAR画面にタイトルへ戻る方法が表示される
  • CLEAR画面をクリックするとTITLEへ戻る
  • もう一度タイトルをクリックすると新しい問題で始まる
  • TITLE / PLAY / CLEARで背景が切り替わる

全部確認できたら、基本版完成です。


3.完成コード

途中でコードが分からなくなった場合は、完成した基本版と比べて確認してください。

このコードは、第5回で完成したものと同じです。

完成コードを見る
#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();

        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)
{
    DrawBox(
        0, 0,
        SCREEN_WIDTH, SCREEN_HEIGHT,
        GetColor(54, 89, 96),
        TRUE);

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

    DrawString(
        100, 210,
        "CLICK TO START",
        GetColor(230, 230, 230));
}

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[])
{
    DrawBox(
        0, 0,
        SCREEN_WIDTH, SCREEN_HEIGHT,
        GetColor(238, 233, 210),
        TRUE);

    DrawString(
        24, 24,
        "SLIDE PUZZLE",
        GetColor(55, 55, 55));

    DrawBoard(board, tileGraph);
}

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

void ClearDraw(void)
{
    DrawBox(
        0, 0,
        SCREEN_WIDTH, SCREEN_HEIGHT,
        GetColor(70, 120, 90),
        TRUE);

    DrawString(
        150, 150,
        "CLEAR!",
        GetColor(255, 255, 255));

    DrawString(
        95, 210,
        "CLICK TO TITLE",
        GetColor(230, 230, 230));
}

4.自由制作で1つ追加しよう

ここからは、自分で追加したい機能を選びます。

全部やる必要はありません。

まずは1つだけ選んで完成させることを目標にしよう!

見た目を変える

比較的取り組みやすいものです。

  • タイル画像を自分のデザインへ差し替える
  • 背景色を変える
  • タイトル画面を装飾する
  • クリア画面を装飾する
  • 文字の位置を調整する

操作を分かりやすくする

マウス座標やタイル判定を使います。

  • マウスを乗せたタイルに枠を描く
  • 動かせるタイルに印を付ける
  • クリックしたタイルを目立たせる

ゲーム情報を追加する

新しい変数を追加して使います。

  • 移動回数を表示する
  • 経過時間を表示する
  • リセット機能を付ける

音を追加する

DxLibのサウンド機能を使ってみたい人向けです。

  • タイルを動かしたときに効果音を鳴らす
  • クリアしたときに効果音を鳴らす
  • BGMを流す

ルールを広げる

少し難しい発展です。

  • 難易度によってシャッフルの試行回数を変える(実際に動く回数とは同じではありません)
  • 手数制限を追加する
  • 制限時間を追加する

5.追加する前に作戦を考えよう

作り始める前に、追加する機能を1つ決めます。

次の4つを考えてからプログラムを変更してみよう。

① 何を追加する?

② 新しく覚えておくデータは必要?

③ Updateでは何をする?

④ Drawでは何をする?

たとえば「移動回数を表示する」なら、

① 何を追加する?
    移動回数を表示する

② 新しく覚えておくデータは必要?
    moveCount が必要

③ Updateでは何をする?
    タイルが実際に動いたときに moveCount を増やす

④ Drawでは何をする?
    moveCount を画面へ表示する

と考えられます。


6.例:移動回数ならどこを変える?

自由制作の考え方の例として、移動回数を見てみます。

まず大事なのは、moveCountをどこへ置くかです。

今回はstateやboardと同じように、WinMain()でゲーム全体のデータとして持たせます。

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

グローバル変数にはせず、必要な関数へ引数で渡します。

ゲーム開始時に0へ戻す

StartGame()へmoveCountの場所も渡します。

void StartGame(
    struct Board* board,
    int* moveCount)
{
    InitBoard(board);
    ShuffleBoard(board);
    *moveCount = 0;
}

そのため、TitleUpdate()もmoveCountを受け取る形へ変更します。

void TitleUpdate(
    struct Board* board,
    int* moveCount,
    enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        StartGame(board, moveCount);
        *state = GAME_STATE_PLAY;
    }
}

ゲームループからは、

TitleUpdate(&board, &moveCount, &state);

と渡します。

実際に動いたときだけ増やす

タイルをクリックしても、動かせないタイルなら回数を増やしてはいけません。

そこでPlayUpdate()にはint* moveCountを渡し、次のようにします。

if (number != -1)
{
    if (CanMoveTile(board, number))
    {
        MoveTile(board, number);
        (*moveCount)++;

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

MoveTile()の中でもCanMoveTile()を確認していますが、ここでは回数を増やしてよいか判断するため、移動前にも確認しています。

PLAY画面へ表示する

PlayDraw()には、今まで使っていたtileGraph[]に加えて、現在の回数を値として渡します。

void PlayDraw(
    struct Board* board,
    int tileGraph[],
    int moveCount)
{
    DrawBox(
        0, 0,
        SCREEN_WIDTH, SCREEN_HEIGHT,
        GetColor(238, 233, 210),
        TRUE);

    DrawString(
        24, 24,
        "SLIDE PUZZLE",
        GetColor(55, 55, 55));

    DrawBoard(board, tileGraph);

    DrawFormatString(
        24, 55,
        GetColor(55, 55, 55),
        "MOVE : %d",
        moveCount);
}

ゲームループでは、

PlayUpdate(&board, &moveCount, &state);
PlayDraw(&board, tileGraph, moveCount);

のように、今まで渡していた引数を残したまま、新しい引数を追加します。

このように、機能を1つ増やすときも、

データはどこに置く?
      ↓
どのUpdateへ渡す?
      ↓
どのDrawへ渡す?

まで考えると、プログラム全体を崩さずに追加できます。


7.作るときは1段階ずつ確認しよう

自由制作でも、一気に全部書かないようにします。

たとえば移動回数なら、

moveCountを作る
    ↓
ビルド
    ↓
ゲーム開始時に0へ戻す
    ↓
ビルド・実行
    ↓
タイルを動かしたら増やす
    ↓
値を一時表示して確認
    ↓
PLAY画面へきれいに表示する

という順番で進められます。

途中で動かなくなった場合は、

最後に動いていたところから、何を追加したか

を確認してください。


8.自由制作チェック

選んだ機能について確認します。

  • 追加したい機能を1つ決めた
  • 必要な変数やデータを考えた
  • Updateで行う処理を考えた
  • Drawで行う処理を考えた
  • 少し変更するたびにビルドした
  • 実行して追加した機能を確認した
  • 元のスライドパズルも壊れていないことを確認した

9.完成!

今回のスライドパズルでは、

  • structで関係するデータをまとめる
  • 関数へ必要なデータの場所を渡す
  • ゲームループを作る
  • enumとswitchでゲームの状態を切り替える
  • UpdateとDrawで仕事を分ける
  • コンソール版で作ったパズル処理を再利用する
  • 入力と表示だけをDxLib用に作り替える

という形でゲームを作りました。

ゲームを作るときは、

1つ作る
  ↓
動かして確認する
  ↓
次を追加する

を繰り返すと、少しずつ大きなプログラムへ育てていけます。

自分なりのスライドパズルへ仕上げてみよう!