Skip to main content

第3回 マウスでタイルを動かす

第3回 マウスでタイルを動かそう

今回やること

前回は、コンソール版のBoardを使い、タイル画像で盤面をDxLibのウィンドウへ表示しました。

今回は、マウスでタイルを選んで動かせるようにします。

コンソール版では、

scanf()
  ↓
タイル番号
  ↓
MoveTile()

という流れでした。

DxLib版では、

マウスクリック
  ↓
クリックした場所のタイル番号
  ↓
MoveTile()

に変えます。

MoveTile()そのものは、コンソール版で作ったものをそのまま使います。

つまり今回は、

タイル番号をどうやって決めるか

の部分だけを、キーボード入力からマウス入力へ置き換えます。


1.PLAY画面の仮処理を外そう

第1回では状態遷移を確認するために、PLAY画面をクリックするとCLEAR画面へ進む仮処理を作りました。

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

今回はPLAY画面のクリックを、タイルを選ぶために使います。

そのため、この仮処理をいったん外します。

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

void PlayUpdate(enum GameState* state);

から、

void PlayUpdate(struct Board* board);

へ変更してください。

次に、今までのPlayUpdate()の定義を、いったん次の形へ置き換えます。

void PlayUpdate(struct Board* board)
{
}

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

PlayUpdate(&state);

を、

PlayUpdate(&board);

へ変更します。

ここで一度ビルドしよう

実行すると、

  • TITLEからPLAYへ進める
  • PLAY画面には盤面が表示される
  • PLAY画面をクリックしてもCLEARへ進まない

状態になります。

ここまでできれば、PLAY画面のクリックを新しい操作に使う準備ができました。


2.マウスの座標を取ってみよう

DxLibでは、

GetMousePoint(&mouseX, &mouseY);

で現在のマウス座標を取得できます。

たとえば、

int mouseX;
int mouseY;

GetMousePoint(&mouseX, &mouseY);

とすると、

mouseX → マウスの横方向の位置
mouseY → マウスの縦方向の位置

が入ります。

GetMousePoint()がmouseXとmouseYへ値を書き込むので、

&mouseX
&mouseY

と変数の場所を渡しています。

これは、これまでInitBoard(&board)などで使ってきた&と同じ考え方です。


3.マウスがタイルの中にあるか調べよう

前回、タイル1枚の左上を、

left
top

で求めました。

タイルの大きさはTILE_SIZEなので、タイル1枚の範囲は次のようになります。

left                      left + TILE_SIZE
  │                              │
  ├──────────────────────────────┤
  │                              │
  │            タイル             │
  │                              │
  ├──────────────────────────────┤
 top                       top + TILE_SIZE

マウスがこの中にあるか調べるには、横方向と縦方向の両方を確認します。

左端より右にある

mouseX >= left

右端より左にある

mouseX < left + TILE_SIZE

上端より下にある

mouseY >= top

下端より上にある

mouseY < top + TILE_SIZE

4つを全部満たしていれば、マウスはそのタイルの中にあります。

if (mouseX >= left &&
    mouseX < left + TILE_SIZE &&
    mouseY >= top &&
    mouseY < top + TILE_SIZE)
{
    // マウスはこのタイルの中
}


4.GetClickedTile()を作ろう

クリックした場所にあるタイル番号を返す関数を作ります。

まず、WinMain()より前へプロトタイプ宣言を追加します。

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

この関数では、盤面のマスを順番に調べて、

マウスが入っているマスを見つけたら、その場所のタイル番号を返す

ようにします。

タイルが見つからなかった場合は-1を返します。

穴埋め

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++)
        {
            // xとyから配列の添字を求める
            int index = ______________________________;

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

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

            // マウスが左端より右にあるか調べる
            int insideLeft =
                ______________________________;

            // マウスが右端より左にあるか調べる
            int insideRight =
                ______________________________;

            // マウスが上端より下にあるか調べる
            int insideTop =
                ______________________________;

            // マウスが下端より上にあるか調べる
            int insideBottom =
                ______________________________;

            // 4方向すべてでタイルの内側にあるか調べる
            if (____________________________________)
            {
                // 空白タイルなら、タイルを選ばなかったことにする
                if (________________________________)
                    return -1;

                // この場所に入っているタイル番号を返す
                return ______________________________;
            }
        }
    }

    // どのタイルにも当たらなかった
    return -1;
}
模範解答を見る
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;
}

4つの条件を別々の変数へ入れたのは、

左から入っている?
右から入っている?
上から入っている?
下から入っている?

を1つずつ確認しやすくするためです。

慣れてきたら、4つの条件を直接ifへ書いても構いません。


5.まずクリックした番号を確認しよう

いきなりタイルを動かす前に、

クリックした場所から、正しいタイル番号を取得できているか

を確認します。

PlayUpdate()へマウス入力を追加します。

穴埋め

void PlayUpdate(struct Board* board)
{
    // マウスの左ボタンを押した瞬間だけ処理する
    if (______________________________)
    {
        int mouseX;
        int mouseY;

        // 現在のマウス座標を取得する
        ______________________________;

        // クリックした場所のタイル番号を調べる
        int number =
            ______________________________;

        // 動作確認のため、取得した番号を一時的に表示する
        printfDx("clicked = %d\n", number);
    }
}
模範解答を見る
void PlayUpdate(struct Board* board)
{
    if (IsMouseLeftPressed())
    {
        int mouseX;
        int mouseY;

        GetMousePoint(&mouseX, &mouseY);

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

        printfDx("clicked = %d\n", number);
    }
}

ここで使っているprintfDx()は、動作確認のための一時的な表示です。

printfDx()は呼び出すたびに簡易表示の履歴へ行が追加されるので、1番、5番、15番とクリックすると、確認した番号が順番に残ります。 今回は入力結果を確認するためだけに使い、確認が終わったら次の節でprintfDx()の呼び出しを削除します。

確認してみよう

  • 1番をクリック → 1
  • 5番をクリック → 5
  • 15番をクリック → 15
  • 空白をクリック → -1
  • タイルとタイルの隙間をクリック → -1

となるか確認してください。


6.取得した番号をMoveTile()へ渡そう

番号が正しく取れたら、printfDx()を削除して、本当の移動処理へ置き換えます。

ここで使うのが、コンソール版で作った、

MoveTile()

です。

穴埋め

void PlayUpdate(struct Board* board)
{
    if (IsMouseLeftPressed())
    {
        int mouseX;
        int mouseY;

        GetMousePoint(&mouseX, &mouseY);

        // クリックしたタイル番号を取得する
        int number =
            ______________________________;

        // タイルが見つかったときだけ処理する
        if (______________________________)
        {
            // 取得した番号のタイルを動かす
            ______________________________;
        }
    }
}
模範解答を見る
void PlayUpdate(struct Board* board)
{
    if (IsMouseLeftPressed())
    {
        int mouseX;
        int mouseY;

        GetMousePoint(&mouseX, &mouseY);

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

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

7.どうして動かせないタイルは動かない?

PlayUpdate()では、

MoveTile(board, number);

を呼んでいるだけです。

ここでは、

そのタイルが空白の隣にあるか

を調べていません。

これはMoveTile()の中ですでに、

if (CanMoveTile(board, number) == 0)
    return;

と判定しているからです。

処理の流れは次のようになります。

マウスをクリック
      ↓
GetClickedTile()
      ↓
タイル番号
      ↓
MoveTile()
      ↓
CanMoveTile()
      ↓
動かせるときだけ交換

マウス入力のために、CanMoveTile()やMoveTile()を書き直す必要はありません。


8.動作確認しよう

完成状態の盤面は、次のようになっています。

 1  2  3  4
 5  6  7  8
 9 10 11 12
13 14 15 [空白]

この状態では、空白の隣にある、

  • 15番
  • 12番

だけを動かせます。

テスト1

プログラムを起動し直してPLAYへ進み、15番をクリックします。

15番が右へ移動すれば成功です。

テスト2

もう一度プログラムを起動し直してPLAYへ進み、12番をクリックします。

12番が下へ移動すれば成功です。

そのほかの確認

  • 空白から離れたタイルをクリック → 動かない
  • 空白をクリック → 何も起こらない
  • タイルの隙間をクリック → 何も起こらない
  • マウスを押しっぱなしにしても、1回のクリックで何枚も動かない

ここまで動けば、第3回は完成です。


9.第3回の完成コード

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

第3回の完成コードを見る
#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);

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 TitleUpdate(enum GameState* state);
void TitleDraw(void);

void PlayUpdate(struct Board* board);
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(&board);
            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 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(struct Board* board)
{
    if (IsMouseLeftPressed())
    {
        int mouseX;
        int mouseY;

        GetMousePoint(&mouseX, &mouseY);

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

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

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

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

マウス座標からタイル番号を取得して、コンソール版のMoveTile()へ渡せるようになりました。

マウス
  ↓
GetMousePoint()
  ↓
GetClickedTile()
  ↓
タイル番号
  ↓
MoveTile()

入力方法は変わりましたが、

SearchTile()
CanMoveTile()
MoveTile()

はコンソール版で作ったものをそのまま使えています。

次回は、盤面をシャッフルして本当の問題を作り、クリアしたらCLEAR画面へ進むようにします。