第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つ作る
↓
動かして確認する
↓
次を追加する
を繰り返すと、少しずつ大きなプログラムへ育てていけます。
自分なりのスライドパズルへ仕上げてみよう!
No Comments