Skip to main content

第5回 画面の調整(自分でかっこよくすればいいじゃん)

第5回 タイトル画面とクリア画面を仕上げよう

今回やること

前回までで、

TITLE
  ↓
PLAY
  ↓
CLEAR
  ↓
TITLE

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

今回は新しいパズルの仕組みを増やすのではなく、

  • タイトル画面
  • プレイ画面
  • クリア画面

の見た目を仕上げます。

そのあと、ここまで作った関数を見直して、

どの関数が、どんな仕事を担当しているのか

を整理します。


1.今の背景処理を見てみよう

第4回のゲームループでは、switchより前に次の処理があります。

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

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

    switch (state)
    {
        ...
    }

    ScreenFlip();
}

このDrawBox()は画面全体を塗っているので、TITLE・PLAY・CLEARのどの状態でも同じ背景になります。

今回は、

TITLE → タイトル用の背景
PLAY  → プレイ用の背景
CLEAR → クリア用の背景

と分けます。

そのため、まずゲームループにあるこのDrawBox()を削除してください。

ClearDrawScreen()は削除しません。

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

    switch (state)
    {
        ...
    }

    ScreenFlip();
}

背景を描く処理は、それぞれのDraw関数へ移します。


2.タイトル画面を仕上げよう

まずTitleDraw()です。

タイトル画面では、

  1. 背景を描く
  2. ゲームタイトルを描く
  3. 開始方法を描く

という順番にします。

穴埋め

void TitleDraw(void)
{
    // 画面全体にタイトル用の背景を描く
    ______________________________(
        0, 0,
        SCREEN_WIDTH, SCREEN_HEIGHT,
        GetColor(54, 89, 96),
        TRUE);

    // ゲームタイトルを描く
    ______________________________(
        100, 150,
        "SLIDE PUZZLE",
        GetColor(255, 255, 255));

    // ゲームの開始方法を描く
    ______________________________(
        100, 210,
        "CLICK TO START",
        GetColor(230, 230, 230));
}
模範解答を見る
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));
}

ここで実行しよう

起動して、

  • 背景色が変わった
  • SLIDE PUZZLEが表示される
  • CLICK TO STARTが表示される

ことを確認してください。


3.プレイ画面を仕上げよう

次はPlayDraw()です。

プレイ画面では、

  1. 背景を描く
  2. 見出しを描く
  3. 盤面を描く

という順番にします。

穴埋め

void PlayDraw(struct Board* board, int tileGraph[])
{
    // 画面全体にプレイ用の背景を描く
    ______________________________(
        0, 0,
        SCREEN_WIDTH, SCREEN_HEIGHT,
        GetColor(238, 233, 210),
        TRUE);

    // プレイ画面の見出しを描く
    ______________________________(
        24, 24,
        "SLIDE PUZZLE",
        GetColor(55, 55, 55));

    // 現在の盤面を描く
    ______________________________;
}
模範解答を見る
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);
}

ここで実行しよう

タイトルをクリックしてPLAYへ進み、

  • タイトル画面とは違う背景になる
  • 見出しが表示される
  • 盤面が今までどおり表示される

ことを確認してください。


4.クリア画面を仕上げよう

最後にClearDraw()です。

クリア画面では、

  1. 背景を描く
  2. CLEAR!を描く
  3. タイトルへ戻る方法を描く

ようにします。

穴埋め

void ClearDraw(void)
{
    // 画面全体にクリア用の背景を描く
    ______________________________(
        0, 0,
        SCREEN_WIDTH, SCREEN_HEIGHT,
        GetColor(70, 120, 90),
        TRUE);

    // クリアメッセージを描く
    ______________________________(
        150, 150,
        "CLEAR!",
        GetColor(255, 255, 255));

    // タイトルへ戻る方法を描く
    ______________________________(
        95, 210,
        "CLICK TO TITLE",
        GetColor(230, 230, 230));
}
模範解答を見る
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));
}

すぐに確認したいとき

前回と同じように、動作確認中だけ、

#define SHUFFLE_COUNT 1

にすると、1手でクリアできる問題になります。

CLEAR画面を確認したら、

#define SHUFFLE_COUNT 100

へ戻してください。


5.UpdateとDrawの仕事を見てみよう

3つの画面が仕上がったところで、これまで作った処理を見てみます。

TITLE

TitleUpdate()
    クリックを調べる
    StartGame()を呼ぶ
    PLAYへ変更する

TitleDraw()
    タイトル画面を描く

PLAY

PlayUpdate()
    マウス入力を調べる
    タイル番号を取得する
    タイルを動かす
    クリア判定をする
    CLEARへ変更する

PlayDraw()
    プレイ画面を描く
    DrawBoard()を呼ぶ

CLEAR

ClearUpdate()
    クリックを調べる
    TITLEへ変更する

ClearDraw()
    クリア画面を描く

ここまでのコードでは、

Update
    入力を調べる
    データを変更する
    ゲームの状態を変更する

Draw
    画面へ描く

と仕事を分けています。

たとえば、

*state = GAME_STATE_PLAY;

のようなゲームの状態変更はUpdate側にあり、

DrawString(...)
DrawBox(...)

のような描画はDraw側にあります。


6.パズル本体の処理を見てみよう

次は、コンソール版から持ってきた関数です。

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

これらは、

  • 盤面を初期化する
  • タイルを探す
  • 動かせるか調べる
  • タイルを動かす
  • 完成したか調べる
  • 問題を作る

という、スライドパズルそのものの処理を担当しています。

DxLib版になっても、この部分はほとんど変わっていません。


7.DxLibで追加した処理を見てみよう

コンソール版からDxLib版へ変えるために、主に次の処理を追加しました。

IsMouseLeftPressed()
GetClickedTile()
LoadTileImages() / DeleteTileImages()
DrawBoard()

IsMouseLeftPressed()

マウスを押した瞬間を調べる

GetClickedTile()

マウス座標
    ↓
クリックしたタイル番号

へ変換します。

LoadTileImages() / DeleteTileImages()

タイル画像を読み込む
        ↓
ゲーム中に使う
        ↓
終了時に画像を削除する

第2回で追加した、タイル画像を使うための準備と後片付けです。

DrawBoard()

struct Boardのデータ
    ↓
画面上のタイル

として描画します。

コンソール版で、

scanf()
PrintBoard()

が担当していた「入力」と「表示」の部分が、DxLib用の処理へ変わりました。


8.ゲーム全体を進める処理を見てみよう

TITLE・PLAY・CLEARを進めているのは、次の関数です。

StartGame()

TitleUpdate()
TitleDraw()

PlayUpdate()
PlayDraw()

ClearUpdate()
ClearDraw()

そしてWinMain()のゲームループを見ると、

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;
}

となっています。

ここには、

MoveTile(...)
IsClear(...)
DrawBoard(...)

などの細かな処理を直接並べていません。

ゲームループは、

今どの状態なのかを見て、その状態のUpdateとDrawを呼ぶ

という仕事に集中しています。


9.基本版を確認しよう

ここまでで基本版は仕上がりました。

次を一周して確認してください。

  • 起動するとTITLE画面になる
  • TITLE画面にタイトルと開始方法が表示される
  • タイトルをクリックすると新しい問題が作られる
  • PLAY画面に4×4の盤面が表示される
  • マウスでタイルを動かせる
  • 動かせないタイルは動かない
  • パズルを完成するとCLEAR画面へ進む
  • CLEAR画面に戻り方が表示される
  • CLEARをクリックするとTITLEへ戻る
  • もう一度ゲームを始められる
  • TITLE / PLAY / CLEARで背景が切り替わる

全部確認できれば完成です。


10.第5回の完成コード

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

第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));
}

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

TITLE・PLAY・CLEARそれぞれの画面を仕上げて、ゲーム全体の処理を整理できました。

今のプログラムは、大きく見ると、

パズル本体の処理
        +
DxLibでの入力・描画
        +
TITLE / PLAY / CLEARのゲーム進行

でできています。

次回は完成コードを確認したあと、自分で追加したい機能を選んで作品を仕上げます。