第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.ゲーム開始処理をまとめよう
タイトルをクリックしてゲームを始めるときは、
- 盤面を完成状態へ戻す
- その盤面をシャッフルする
という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));
だけを置いておきます。
ここで一度実行しよう
- TITLEを表示する
- タイトルをクリックする
- PLAYへ進む
- 盤面が完成状態ではなく、シャッフルされている
ところまで確認してください。
ここまでで、ゲームを始めるたびに問題を作るところまで完成です。
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へ戻してゲームを開始し、盤面が十分に崩れることを確認します。
- 起動するとTITLEが表示される
- タイトルをクリックすると新しい問題が作られる
- PLAY画面で盤面が崩れている
- マウスでタイルを動かせる
- 動かせるタイルだけが移動する
- CLEARへの遷移は、直前の
SHUFFLE_COUNT 1のテストで確認済み - 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画面を仕上げながら、今まで作った処理の役割を整理します。

No Comments