第7回 文字版コスコマンを完成させよう
第7回 文字版コスコマンを完成させよう
前回までで、コスコマンは壁に止まり、ゴールを覚えたまま消さずに歩き、荷物を押せるようになりました。
今回は、文字版をゲームとして最後まで遊ぶためべるように、します。
今回できるようになること
- すべての荷物をゴールへ置くとクリアになる
- クリア
判定 複数ステージリトライ- 後にENTERを押すと、次のステージへ進む
処理 - Rを押すと、現在のステージを開始時の配置で読み込み直す
- 3つのステージを順番に遊べる
- 最後のステージをクリアすると、最初のステージを読み込む
を追加し
ゲーム中の処理は、次の順番で行います。
今回の流れ
プレイ中
↓
全てRが押された? ── YES → 現在の荷物ステージを読み込み直す
↓ NO
移動キーがゴールへ入っ押された?
↓ YES
コスコマンまたは荷物を動かす
↓
通常の荷物LUGGが残っている?
↓ NO
クリア表示状態にする
↓
ENTER
ENTERが押されたら次のステージを読み込む
Rキーなら
現在のステージを最初から読み直す
1.クリア判定を作ろう
今回追加・変更す何を調べる関数のか
第7回すべての荷物をゴールへ置けばクリアです。
普通の床にある荷物はLUGG、ゲゴームを最後ル上にある荷物はLUGG_ON_GOALとしてMapへ入っていまで進めるす。
そのための関数、Map全体をまとめ調べて用意、LUGGが残っているかを確認します。
まず、既存の
voidLUGGが1個でも見つかった
LoadStage(Map*→ map);まだクリアしていないので0を返す
最後までLUGGが見つからなかった
→ すべての荷物がゴール上にあるので1を返す
を関数宣言が並んでいる場所で、
void LoadStage(Map* map, int stageNumber);
へ変更します。
さらにMoveObject()の宣言の下へ、次をまとめて追加してくださいます。
int CheckClear(Map* map);
int IsEnterPressed(void);
int IsRPressed(void);
void UpdateGame(Map* map, int* currentStage, int* clearFlag);
void DrawGame(Map* map, int currentStage, int clearFlag, int fontHandle);
LoadStage() → 番号で選んだステージを読み込む
CheckClear() → 全ての荷物がゴールへ入ったか調べる
IsEnterPressed() → ENTERを押した瞬間を調べる
IsRPressed() → Rを押した瞬間を調べる
UpdateGame() → 入力・移動・クリア・ステージ切替を更新する
DrawGame() → ステージ番号やクリア表示をまとめて描く
1.何をもって「クリア」とする?
コスコマンでは、すべての荷物をゴールへ運べばクリアです。
今回使うステージは、荷物とゴールの数が同じになるように作ってあります。
普通の荷物はMapでは、
LUGG
ゴールへ入った荷物は、
LUGG_ON_GOAL
です。
するとMap全体を調べて、
LUGGが1個でも残っている
↓
まだクリアしていない
LUGGが1個も残っていない
↓
すべての荷物がゴールへ入った
↓
クリア
と判断できます。
Mapにすべての状態が入っている完成条件
ここまで作ってきたコスコマンでは、床や壁だけでなく、
コMapの全マスコマンを二重ループで調べる荷物LUGGを見つけたら、その時点で0を返すゴールゴールに乗全マスを調べ終わったコスコマンゴールに乗った荷物ら1を返す
といった状態も、すべてMapの値として持っています。
例えば、
LUGG_ON_GOAL
という値を見れば、
このマスはゴールで、その上に荷物が乗っている
ということが、その値だけで分かります。
もし、ゴールの位置をMapに持ち、荷物の位置を別のデータとして管理していた場合関数本体は、
荷物はどこにある?
↓
その位置はゴールになっている?
というように、複数のデータを組み合わせて状態を調べる必要があります。
しかし今回のコスコマンでは、
LUGG
LUGG_ON_GOAL
のように、そのマスが今どういう状態なのかをMapへ直接書き込んでいます。
そのためクリア判定では、
Map全体を見る
↓
LUGGが残っているか調べる
だけで判断できます。
このように、ものが重なっている状態までMapの値として持たせておくと、Mapを見るだけで現在のゲーム状態が分かるため、今回のような判定は比較的簡単に作れます。
2.CheckClear()を作ろう
先ほど宣言したCheckClear()の中身を作ります。
MoveObject()の下定義が終わったあとへ次の関数を追加します。先に処理の順番をコメントで確かめてから、次の空欄を埋めてください。
int CheckClear(Map* map)
{
// ステージの上から下まで、全すべての行を調べる
for (int y = 0; y < ____________________; y++)
{
// 1行の左から右まで、全すべてのマスを調べる
for (int x = 0; x < ____________________; x++)
{
// 今調べているマスが、まだゴールへ入っていない荷物か調べる
if (______________________________ == ____________________)
{
// 普通の荷物が残っていたるので、未クリアを返す
return ____________________;
}
}
}
// 普通の荷物が1個も見つからなければかったので、クリアを返す
return ____________________;
}
ヒント
Mapの全縦の繰り返し回数はmap->stage_height、横の繰り返し回数はmap->stage_widthです。各マスを二重ループはmap->Dat[y][x]で調べます。途中で普通の荷物を見つけた時点で「未クリア」と判断できられます。
模範解答を見る
int CheckClear(Map* map)
{
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
if (map->Dat[y][x] == LUGG)
{
return 0;
}
}
}
return 1;
}
いったん実行して確かめよう
ゲームループで移動したあとに、一時的に次を入れてみてください。
if (CheckClear(¤tMap))
DrawString(250, 360, "CLEAR!", GetColor(255, 220, 80));
荷物をゴールへ押し込んだときにCLEAR!が出れば、クリア判定ができています。
確認できたら、この一時表示は削除します。このあとゲーム全体の更新と描画へ組み込みます。
3.2.ステージを3つに増やそ用意しよう
これまでは、最初1つのステージ1つだけを、START_STAGEへ入れていました。
const Map START_STAGE = { ... };
としていました。
ゲームとして遊ぶならこれを、クリアしたあとに次3つのステージを持つ配列STAGESへ進みたいところで変更します。
まず最初に、ステージ数を定義します。MAX_STAGE_HEIGHTの定義の下へ追加してください。
#define STAGE_COUNT 3
MAX_STAGE_HEIGHTの下へ追加してください。
次にSTART_STAGEを削除し、3ステージ分次のSTAGESへ置き換えます。
3ステージ分のデータを見る
const Map STAGES[STAGE_COUNT] = {
{
10, 7,
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 3, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 2, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 4, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
}
},
{
10, 7,
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 },
{ 1, 0, 2, 0, 1, 0, 3, 0, 0, 1 },
{ 1, 0, 0, 2, 0, 0, 3, 0, 0, 1 },
{ 1, 0, 0, 0, 1, 1, 0, 0, 0, 1 },
{ 1, 0, 4, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
}
},
{
10, 7,
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 0, 0, 1, 0, 4, 1 },
{ 1, 1, 1, 0, 0, 0, 1, 0, 0, 1 },
{ 1, 1, 1, 2, 0, 2, 0, 2, 0, 1 },
{ 1, 1, 1, 0, 2, 2, 1, 0, 0, 1 },
{ 1, 3, 3, 3, 3, 3, 0, 0, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
}
}
};
各ステージは、STAGES[0]STAGES[1]、STAGES[2]という3つ配列のMapを持つことになり番号で選べます。
STAGES[0] 4.読み込む→ 1つ目のステージ
STAGES[1] → 2つ目のステージ
STAGES[2] → 3つ目のステージ
3.番号で選んだステージを指定できるようにしよ読み込もう
LoadStage()も、どのへステージ番号を渡し、STAGESから選んだMapをcurrentMapへコピーするか選べできるようにします。
プロトタイプ関数宣言は冒頭でを次の形へ変更したます。
void LoadStage(Map* map, int stageNumber);
完成条件
STAGES[stageNumber]ので、ここでは横幅をコピーするSTAGES[stageNumber]の高さをコピーする- Mapの全マスを
map->Datへコピーする
LoadStage()の関数本体を次の形へ変更して、空欄を埋めますてください。
void LoadStage(Map* map, int stageNumber)
{
// 選ばれたステージの横幅を、プレイ用中のMapへコピーする
map->stage_width = ________________________________;
// 選ばれたステージの高さを、プレイ用中のMapへコピーする
map->stage_height = ________________________________;
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
// 選ばれたステージの同じマスを、プレイ用中のMapへコピーする
map->Dat[y][x] = ________________________________;
}
}
}
ヒント
今ま選ばれたステージはSTAGES[stageNumber]でSTART_STAGEからす。横幅、高さ、各マスの値を同じ場所へコピーしていた場所を、STAGESの中からstageNumberで選ぶ形に変えます。
模範解答を見る
void LoadStage(Map* map, int stageNumber)
{
map->stage_width = STAGES[stageNumber].stage_width;
map->stage_height = STAGES[stageNumber].stage_height;
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
map->Dat[y][x] = STAGES[stageNumber].Dat[y][x];
}
}
}
WinMain()ではの先頭にある変数宣言へ、現在の選ばれているステージ番号を持つ変数表すcurrentStageを追加します。Map currentMap;とint fontHandle;の間を、次の形にしてください。
Map currentMap;
// 最初は配列の0番目、つまり1つ目のステージから始める
int currentStage = ____________________;
int fontHandle;
そして最初のステージ読み込みにもcurrentStageを、渡します。
// 現在選ばれていSTAGES[currentStage]をcurrentMapへコピーするステージをプレイ用Mapへ読み込む
LoadStage(¤tMap, ____________________);
へ変更します。
模範解答を見る
int currentStage = 0;
LoadStage(¤tMap, currentStage);
実行して確認
いったんcurrentStageをへ1を代入すると2つ目、2へ変えて実行し、別を代入すると3つ目のステージが表示されることをます。確認後は、開始するステージ番号として0を代入してください。
確認できたら0へ戻します。
5.4.クリア中かどうか状態を覚えておこ記録しよう
クリアした瞬間に次のステージへ飛ぶ切り替えると、クリアした盤面とCLEAR!を見る時間があり表示できません。
そこで、
0 → まだプレイ中
1 → かクリア済み
中かを表すclearFlagを使います。
へWinMain()追加記録します。
clearFlagが0 → プレイ中
clearFlagが1 → クリア中
WinMain()の変数を次の形にします。
Map currentMap;
int currentStage = 0;
// まだクリアしていゲーム開始時はプレイ中ない状態から始めので0にする
int clearFlag = ____________________;
int fontHandle;
模範解答を見る
int clearFlag = 0;
6.5.ENTERとRキーを「押した瞬間」RもIsKeyPressed()で使お調べよう
クリア後第3回で作ったIsKeyPressed()はENTERで次のステ、引数で渡したキージ、プレイ中はRでリトライできるようにが押された瞬間だけ1を返します。
先ほど宣言した2つのWASDだけでなく、ENTERとRにも同じ関数本体を、CheckClear()の下へ追加してください使えます。
int IsEnterPressed(void)
{
static int previous = 0;
int current = CheckHitKey(KEY_INPUT_RETURN);
// 前のフレームでは離されていて、今のフレームでENTERが押されているときだけ1にする
int pressed = ________________________________;
previous = current;
return pressed;
}
int IsRPressed(void)
{
static int previous = 0;
int current = CheckHitKey(KEY_INPUT_R);
// Rキーも押しっぱなしではなく、押した瞬間だけ1にすなる
intIsKeyPressed(KEY_INPUT_RETURN)
pressed// =Rが押された瞬間だけ1になる
________________________________;
previous = current;
return pressed;
}IsKeyPressed(KEY_INPUT_R)
ヒント
第3回入力判定の仕組みはIsKeyPressed()の中にすでにあるため、ENTER用とR用に同じ考え方で処理を作り直す。今と前のキー状態を比べ必要はありますせん。
模範解答次の更新処理では、この2つを見る
int pressed = current != 0 && previous == 0;
2つの関数は同じ式になり直接使います。
7.6.ゲーム中の状態をUpdateGame()で変更新をしよう
UpdateGame()の役割
UpdateGame()へまとめよう
これからは、ゲキームループの中入力に処理を増やし続けるのではなく、応じてゲームの状態を変更新をUpdateGame()へまとめします。
- クリア中なら、ENTERで次のステージを読み込む
- プレイ中にRが押されたら、現在のステージを読み込み直す
- 移動キーが押されたら、コスコマンまたは荷物を動かす
- 移動後にクリア条件を満たしたら、
clearFlagを1にする
先ほど関数宣言したが並んでいる場所で、のUpdateGame(CheckClear()中身を、宣言の下へ次を追加します。IsRPressed()
void UpdateGame(Map* map, int* currentStage, int* clearFlag);
currentStageとclearFlagは、WinMain()にある値を書き換えるため、ポインタで受け取ります。
*currentStage → WinMain()のcurrentStage
*clearFlag → WinMain()のclearFlag
ポインタの指す整数を1増やすときは、(*currentStage)++と書きます。*clearFlag = 0;や*clearFlag = 1;も、WinMain()にあるclearFlagを書き換える式です。
処理する順番
クリア中?
↓ YES
ENTERが押されたら次のステージを読み込む
↓
そのフレームの処理を終了
プレイ中
↓
Rが押された?
↓ YES
現在のステージを読み込み直して処理を終了
Rが押されていない
↓
移動キーを調べる
↓
動けた場合だけクリア判定を行う
完成条件
- クリア中はコスコマンを動かさない
- ENTERが押されたらステージ番号を1増やす
- ステージ番号が
STAGE_COUNT以上になったら0にする - 次のステージを読み込み、
clearFlagを0にする - プレイ中にRが押されたら、現在のステージを読み込み直す
- 移動できたあとに
CheckClear()が1なら、clearFlagを1にする
関数本体は、CheckClear()の定義が終わったあとへ追加します。上の順番と各行のコメントを照らし合わせて、次の空欄を埋めてください。
void UpdateGame(Map* map, int* currentStage, int* clearFlag)
{
// クリア中の処理
if (*clearFlag)
{
// ENTERが押された瞬間か調べる
if (IsEnterPressed()IsKeyPressed(____________________))
{
// 次のステージ番号へ進めにする
(____________________)++;
// 最後のステージ番号を越えが、用意したステージ数以上になったか調べる
if (*currentStage >= ____________________)
{
// 最後配列の0番目、つまで進んだら最初り1つ目のステージ番号へ戻すを選ぶ
*currentStage = ____________________;
}
// 新しい選ばれたステージをプレイ用中のMapへ読み込む
LoadStage(map, ____________________);
// 新しい次のステージではクリア状態を解除プレイ中にする
*clearFlag = ____________________;
}
// クリア中は、この下にある移動処理を行わない
return;
}
// Rが押された瞬間か調べる
if (IsRPressed()IsKeyPressed(____________________))
{
// 現在STAGES[*currentStage]をプレイ中のステージを原本からMapへ読み直して最初の配置へ戻す込む
LoadStage(map, ____________________);
return;
}
// WASDのどれが押されたか調べる
Direction dir = GetDirection();
if (dir != DIR_NONE)
{
// コスコマンまたは荷物を動かす
MoveResult result = MoveObject(dir, map);
// 実際に動いたあとけて、全通常の荷物がゴールへ入残ったか調べるていなければクリア
if (result != MOVE_NONE && ________________________________)
{
// クリアしたことを覚えておく中にする
*clearFlag = ____________________;
}
}
}
ヒント
ENTERは
currentStageKEY_INPUT_RETURNと、Rはです。ステージ数はclearFlagKEY_INPUT_R、この関数の外にある値を書き換えるためポインタSTAGE_COUNTで受け取っています。クリア判定には、さきほど作った関数CheckClear(map)を使います。
模範解答を見る
void UpdateGame(Map* map, int* currentStage, int* clearFlag)
{
if (*clearFlag)
{
if (IsEnterPressed()IsKeyPressed(KEY_INPUT_RETURN))
{
(*currentStage)++;
if (*currentStage >= STAGE_COUNT)
{
*currentStage = 0;
}
LoadStage(map, *currentStage);
*clearFlag = 0;
}
return;
}
if (IsRPressed()IsKeyPressed(KEY_INPUT_R))
{
LoadStage(map, *currentStage);
return;
}
Direction dir = GetDirection();
if (dir != DIR_NONE)
{
MoveResult result = MoveObject(dir, map);
if (result != MOVE_NONE && CheckClear(map))
{
*clearFlag = 1;
}
}
}
8.7.現在のゲーム状態をDrawGame()で表示もしよう
DrawGame()の役割
DrawGame()は、現在のゲーム状態を画面へ表示します。この関数では、Mapやステージ番号、クリア状態を書き換えません。
- 現在のステージ番号を表示する
- Rでリトライできること
めようを表示する - プレイ中のMapを表示する
- クリア中なら
CLEAR!とENTERの案内を表示する
先ほど関数宣言したDrawGame()の中身をが並んでいる場所で、UpdateGame()の宣言の下へ次を追加します。
void DrawGame(Map* map, int currentStage, int clearFlag, int fontHandle);
完成条件
DrawStageText()へMapとフォントハンドルを渡すclearFlagが1のときだけクリア表示を描く- ゲーム状態を変更する関数を呼ばない
関数本体は、UpdateGame()の定義が終わったあとへ追加します。次の空欄を埋めてください。
void DrawGame(Map* map, int currentStage, int clearFlag, int fontHandle)
{
// 画面上では1から数えるため、currentStageへ1を足して表示する
DrawFormatString(24, 20, GetColor(255, 255, 255),
"STAGE %d / %d", currentStage + 1, STAGE_COUNT);
// 操作方法を表示する
DrawString(300, 20, "WASD : MOVE R : RETRY", GetColor(200, 200, 200));
// 現在のプレイ用中Mapを文字で描く表示する
________________________________;
// クリア中だけ、クリア表示と次ステージの案内を表示する
if (clearFlag)
{
DrawString(250, 360, "CLEAR!", GetColor(255, 220, 80));
DrawString(185, 405, "PRESS ENTER : NEXT STAGE", GetColor(255, 255, 255));
}
}
ヒント
ステージをMapを文字で表示する関数は第2回から使い続けていまDrawStageText()です。引数はmapとfontHandleです。
模範解答を見る
DrawStageText(map, fontHandle);
ゲームループでは、最初にゲーム状態を変更し、その変更後の状態を画面へ表示します。 第6回のゲームループにあった 実行して、次の動きを これで、文字だけで最後まで遊べるコスコマンが完成しました。 次回から、ゲームのルールは ここまでできれば、 自分でステージを考えてもよいですが、倉庫番形式のステージデータを公開しているサイトを参考にする方法もあります。 Google DeepMindが公開しているBoxoban Levelsには、箱を押してゴールへ運ぶパズルのステージデータが大量に収録されています。 ステージは 使われている主な文字は、コスコマンとほぼ同じ意味です。 例えば、テキストの という形を見ながら、 のように、コスコマンのMapデータへ置き換えていけば使えます。 Boxobanのステージは10×10なので、現在の の範囲にも収まります。 新しいステージを追加したら、 注意: 公開されているステージなら何でも自由に転載できるとは限りません。Boxoban LevelsはApache License 2.0で公開されていますが、利用・改変・再配布するときは配布元のライセンス条件を確認してください。このページではステージデータそのものを転載せず、配布元へのリンクを紹介しています。
8.WinMain()から更新と表示を呼び出そう
UpdateGame()でゲーム状態を変更する
↓
DrawGame()で現在の状態を表示する
完成条件
UpdateGame()へcurrentMap、currentStage、clearFlagのアドレスを渡すDrawGame()へ表示に必要な4つの値を渡すGetDirection()、MoveObject()、DrawStageText()の呼び出しは、更新これからUpdateGame()と描画を呼ぶ形へ整理DrawGame()が担当します。while (ProcessMessage() == 0)から対応する閉じかっこまでを次の形へ置き換え、空欄を埋めてください。while (ProcessMessage() == 0)
{
ClearDrawScreen();
// ゲームの入力・に応じて、移動・クリア・ステージ切り替えを更新処理する
UpdateGame(____________________, ____________________, ____________________);
// 現在更新後のステージと番号、Map、クリア状態を表示を描くする
DrawGame(____________________, ____________________, ____________________, ____________________);
ScreenFlip();
}
ヒント
UpdateGame()ではステージ番号currentStageとクリア状態clearFlagを書き換えるため、アドレス&を付けて渡します。DrawGame()は表示するだけなので、ステージ番号とクリア状態は値のまま渡せるものがありします。模範解答を見る
UpdateGame(¤tMap, ¤tStage, &clearFlag);
DrawGame(¤tMap, currentStage, clearFlag, fontHandle);
9.文字版を最後まで遊んでみよう
一通りすべて確認してください。
*になる全すべての荷物をゴールへ置くとCLEAR!が出表示されるENTERでクリア中はWASDを押してもコスコマンが動かないへ進を読み込むRでRを押すと現在のステージを最初からやり開始時の配置で読み込み直せるす3ステージ3つ目のあと0番のステージへ戻るをクリアしたあとは、1つ目のステージを読み込むにはSTAGES[]STAGES原本各ステージの開始時のデータが入っていまま残し、遊んでいる間す。プレイ中に書き換えるのはcurrentMapだけです。そのRを押したときは、STAGES[currentStage]をcurrentMapへコピーするため、何度リトライしてもLoadStage()で元現在のステージを開始時の配置へ戻で遊び直せます。そのまま変えずに、文字表示を画像表示へ変えていき更します。
10.もっとステージを増やしてみよう
STAGESへ新しいMapを追加することで、さらにステージを増やせます。テキスト形式のステージ集を参考にする
.txtファイルに、10×10の文字の並びとして記録されています。1つのテキストファイルには複数のステージが入り、;と番号の行がステージの区切りになっています。
Boxoban
意味
コスコマン
#壁
WALL
半角スペース
床
FLOOR
@プレイヤー
HUMAN
$荷物
LUGG
.ゴール
GOAL##########
# . #
# $ #
# @ #
##########
{ WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL },
#define MAX_STAGE_WIDTH 10
#define MAX_STAGE_HEIGHT 10
STAGESへMapを増やし、STAGE_COUNTもその数に合わせて変更してください。
11.第7回の完成コード
第7回の完成コードを見る
#include "DxLib.h"
#define SCREEN_WIDTH 720
#define SCREEN_HEIGHT 600
#define MAX_STAGE_WIDTH 10
#define MAX_STAGE_HEIGHT 10
#define STAGE_COUNT 3
#define TEXT_SIZE 32
#define TEXT_STAGE_X 40
#define TEXT_STAGE_Y 90
enum OBJNAME
{
FLOOR = 0,
WALL = 1,
LUGG = 2,
GOAL = 3,
HUMAN = 4,
HUMAN_ON_GOAL = 5,
LUGG_ON_GOAL = 6,
OUTSIDE = 7
};
struct Point
{
int x;
int y;
};
struct Map
{
int stage_width;
int stage_height;
int Dat[MAX_STAGE_HEIGHT][MAX_STAGE_WIDTH];
};
const char OBJECT_CHAR[] = {
' ',
'#',
'$',
'.',
'@',
'+',
'*',
' '
};
enum Direction
{
DIR_UP,
DIR_LEFT,
DIR_DOWN,
DIR_RIGHT,
DIR_NONE
};
enum MoveResult
{
MOVE_NONE,
MOVE_WALK,
MOVE_PUSH,
MOVE_PUSH_GOAL
};
const Point DIR_VECTOR[] = {
{ 0, -1 },
{-1, 0 },
{ 0, +1 },
{+1, 0 },
{ 0, 0 }
};
const Map STAGES[STAGE_COUNT] = {
{
10, 7,
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 3, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 2, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 4, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
}
},
{
10, 7,
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 },
{ 1, 0, 2, 0, 1, 0, 3, 0, 0, 1 },
{ 1, 0, 0, 2, 0, 0, 3, 0, 0, 1 },
{ 1, 0, 0, 0, 1, 1, 0, 0, 0, 1 },
{ 1, 0, 4, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
}
},
{
10, 7,
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 0, 0, 1, 0, 4, 1 },
{ 1, 1, 1, 0, 0, 0, 1, 0, 0, 1 },
{ 1, 1, 1, 2, 0, 2, 0, 2, 0, 1 },
{ 1, 1, 1, 0, 2, 2, 1, 0, 0, 1 },
{ 1, 3, 3, 3, 3, 3, 0, 0, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
}
}
};
void LoadStage(Map* map, int stageNumber);
void DrawStageText(Map* map, int fontHandle);
Direction GetDirection(void);
Point GetPlayerPos(Map* map);
OBJNAME GetObjectNum(Point pos, Map* map);
void SetObjtoMap(OBJNAME obj, Point pos, Map* map);
int IsKeyPressed(int key);
MoveResult MoveObject(Direction dir, Map* map);
int CheckClear(Map* map);
int IsEnterPressed(void);
int IsRPressed(void);
void UpdateGame(Map* map, int* currentStage, int* clearFlag);
void DrawGame(Map* map, int currentStage, int clearFlag, int fontHandle);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
Map currentMap;
int currentStage = 0;
int clearFlag = 0;
int fontHandle;
ChangeWindowMode(TRUE);
SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);
SetMainWindowText("Costco Man");
if (DxLib_Init() != 0)
return -1;
SetDrawScreen(DX_SCREEN_BACK);
fontHandle = CreateFontToHandle("MS ゴシック", TEXT_SIZE, 1);
LoadStage(¤tMap, currentStage);
while (ProcessMessage() == 0)
{
ClearDrawScreen();
UpdateGame(¤tMap, ¤tStage, &clearFlag);
DrawGame(¤tMap, currentStage, clearFlag, fontHandle);
ScreenFlip();
}
DeleteFontToHandle(fontHandle);
DxLib_End();
return 0;
}
void LoadStage(Map* map, int stageNumber)
{
map->stage_width = STAGES[stageNumber].stage_width;
map->stage_height = STAGES[stageNumber].stage_height;
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
map->Dat[y][x] = STAGES[stageNumber].Dat[y][x];
}
}
}
void DrawStageText(Map* map, int fontHandle)
{
char line[MAX_STAGE_WIDTH + 1];
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
int obj = map->Dat[y][x];
line[x] = OBJECT_CHAR[obj];
}
line[map->stage_width] = '\0';
DrawStringToHandle(
TEXT_STAGE_X,
TEXT_STAGE_Y + y * TEXT_SIZE,
line,
GetColor(255, 255, 255),
fontHandle);
}
}
Direction GetDirection(void)
{
// 4方向のキーが押された瞬間を調べる
int up = IsKeyPressed(KEY_INPUT_W);
int left = IsKeyPressed(KEY_INPUT_A);
int down = IsKeyPressed(KEY_INPUT_S);
int right = IsKeyPressed(KEY_INPUT_D);
if (up)
{
return DIR_UP;
}
if (left)
{
return DIR_LEFT;
}
if (down)
{
return DIR_DOWN;
}
if (right)
{
return DIR_RIGHT;
}
return DIR_NONE;
}
Point GetPlayerPos(Map* map)
{
Point result = { -1, -1 };
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
int obj = map->Dat[y][x];
if (obj == HUMAN || obj == HUMAN_ON_GOAL)
{
result.x = x;
result.y = y;
return result;
}
}
}
return result;
}
OBJNAME GetObjectNum(Point pos, Map* map)
{
if (pos.x < 0 || pos.x >= map->stage_width || pos.y < 0 || pos.y >= map->stage_height)
return OUTSIDE;
return (OBJNAME)map->Dat[pos.y][pos.x];
}
void SetObjtoMap(OBJNAME obj, Point pos, Map* map)
{
map->Dat[pos.y][pos.x] = obj;
}
int IsKeyPressed(int key)
{
static int previous[256] = { 0 };
int current = CheckHitKey(key);
int pressed = 0;
if (current != 0 && previous[key] == 0;0)
{
pressed = 1;
}
previous[key] = current;
return pressed;
}
MoveResult MoveObject(Direction dir, Map* map)
{
Point playerPos = GetPlayerPos(map);
Point nextPos = {
playerPos.x + DIR_VECTOR[dir].x,
playerPos.y + DIR_VECTOR[dir].y
};
OBJNAME current = GetObjectNum(playerPos, map);
OBJNAME next = GetObjectNum(nextPos, map);
if (next == FLOOR || next == GOAL)
{
if (current == HUMAN_ON_GOAL)
SetObjtoMap(GOAL, playerPos, map);
else
SetObjtoMap(FLOOR, playerPos, map);
if (next == GOAL)
SetObjtoMap(HUMAN_ON_GOAL, nextPos, map);
else
SetObjtoMap(HUMAN, nextPos, map);
return MOVE_WALK;
}
if (next == LUGG || next == LUGG_ON_GOAL)
{
Point nextNextPos = {
nextPos.x + DIR_VECTOR[dir].x,
nextPos.y + DIR_VECTOR[dir].y
};
OBJNAME nextNext = GetObjectNum(nextNextPos, map);
if (nextNext != FLOOR && nextNext != GOAL)
return MOVE_NONE;
if (current == HUMAN_ON_GOAL)
SetObjtoMap(GOAL, playerPos, map);
else
SetObjtoMap(FLOOR, playerPos, map);
if (next == LUGG_ON_GOAL)
SetObjtoMap(HUMAN_ON_GOAL, nextPos, map);
else
SetObjtoMap(HUMAN, nextPos, map);
if (nextNext == GOAL)
{
SetObjtoMap(LUGG_ON_GOAL, nextNextPos, map);
return MOVE_PUSH_GOAL;
}
SetObjtoMap(LUGG, nextNextPos, map);
return MOVE_PUSH;
}
return MOVE_NONE;
}
int CheckClear(Map* map)
{
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
if (map->Dat[y][x] == LUGG)
return 0;
}
}
return 1;
}
int IsEnterPressed(void)
{
static int previous = 0;
int current = CheckHitKey(KEY_INPUT_RETURN);
int pressed = current != 0 && previous == 0;
previous = current;
return pressed;
}
int IsRPressed(void)
{
static int previous = 0;
int current = CheckHitKey(KEY_INPUT_R);
int pressed = current != 0 && previous == 0;
previous = current;
return pressed;
}
void UpdateGame(Map* map, int* currentStage, int* clearFlag)
{
if (*clearFlag)
{
if (IsEnterPressed()IsKeyPressed(KEY_INPUT_RETURN))
{
(*currentStage)++;
if (*currentStage >= STAGE_COUNT)
*currentStage = 0;
LoadStage(map, *currentStage);
*clearFlag = 0;
}
return;
}
if (IsRPressed()IsKeyPressed(KEY_INPUT_R))
{
LoadStage(map, *currentStage);
return;
}
Direction dir = GetDirection();
if (dir != DIR_NONE)
{
MoveResult result = MoveObject(dir, map);
if (result != MOVE_NONE && CheckClear(map))
*clearFlag = 1;
}
}
void DrawGame(Map* map, int currentStage, int clearFlag, int fontHandle)
{
DrawFormatString(24, 20, GetColor(255, 255, 255), "STAGE %d / %d", currentStage + 1, STAGE_COUNT);
DrawString(300, 20, "WASD : MOVE R : RETRY", GetColor(200, 200, 200));
DrawStageText(map, fontHandle);
if (clearFlag)
{
DrawString(250, 360, "CLEAR!", GetColor(255, 220, 80));
DrawString(185, 405, "PRESS ENTER : NEXT STAGE", GetColor(255, 255, 255));
}
}
第7回の
変更点入りコメント付き完成コードを見るこのコクリア判定、ステードはジ切り替え、上の説明なし完成コードと同じリトライ、更新処理です。今回追加・変更した場所、描画処理の流れを中心に、役割や理由もコメント付きで確認できます。
#include "DxLib.h"
#define SCREEN_WIDTH 720
#define SCREEN_HEIGHT 600
#define MAX_STAGE_WIDTH 10
#define MAX_STAGE_HEIGHT 10
// 【解説・今回追加定数】用意したステージ数。番号がこの値以上にならないよう管理する。
#define STAGE_COUNT 3
#define TEXT_SIZE 32
#define TEXT_STAGE_X 40
#define TEXT_STAGE_Y 90
enum OBJNAME
{
FLOOR = 0,
WALL = 1,
LUGG = 2,
GOAL = 3,
HUMAN = 4,
HUMAN_ON_GOAL = 5,
LUGG_ON_GOAL = 6,
OUTSIDE = 7
};
struct Point
{
int x;
int y;
};
struct Map
{
int stage_width;
int stage_height;
int Dat[MAX_STAGE_HEIGHT][MAX_STAGE_WIDTH];
};
const char OBJECT_CHAR[] = {
' ',
'#',
'$',
'.',
'@',
'+',
'*',
' '
};
enum Direction
{
DIR_UP,
DIR_LEFT,
DIR_DOWN,
DIR_RIGHT,
DIR_NONE
};
enum MoveResult
{
MOVE_NONE,
MOVE_WALK,
MOVE_PUSH,
MOVE_PUSH_GOAL
};
const Point DIR_VECTOR[] = {
{ 0, -1 },
{-1, 0 },
{ 0, +1 },
{+1, 0 },
{ 0, 0 }
};
// 【解説・今回変更データ】1つだった元複数のステージを配列にし、ステージ番号で選べるようにする。
const Map STAGES[STAGE_COUNT] = {
{
10, 7,
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 3, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 2, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 4, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
}
},
{
10, 7,
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 },
{ 1, 0, 2, 0, 1, 0, 3, 0, 0, 1 },
{ 1, 0, 0, 2, 0, 0, 3, 0, 0, 1 },
{ 1, 0, 0, 0, 1, 1, 0, 0, 0, 1 },
{ 1, 0, 4, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
}
},
{
10, 7,
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 0, 0, 1, 0, 4, 1 },
{ 1, 1, 1, 0, 0, 0, 1, 0, 0, 1 },
{ 1, 1, 1, 2, 0, 2, 0, 2, 0, 1 },
{ 1, 1, 1, 0, 2, 2, 1, 0, 0, 1 },
{ 1, 3, 3, 3, 3, 3, 0, 0, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
}
}
};
void LoadStage(Map* map, int stageNumber);
void DrawStageText(Map* map, int fontHandle);
Direction GetDirection(void);
Point GetPlayerPos(Map* map);
OBJNAME GetObjectNum(Point pos, Map* map);
void SetObjtoMap(OBJNAME obj, Point pos, Map* map);
int IsKeyPressed(int key);
MoveResult MoveObject(Direction dir, Map* map);
// 【解説・今回追加判定】通常の荷物が残っているかを調べ、クリア状態を判定する。
int CheckClear(Map* map);
int IsEnterPressed(void);
int IsRPressed(void);
// 【解説・今回追加更新】入力・移動・クリア・ステージ切り替えを更新処理へまとめする。
void UpdateGame(Map* map, int* currentStage, int* clearFlag);
// 【解説・今回追加描画】ステージと番号、盤面、操作案内、クリア表示を描画処理へまとめるく。
void DrawGame(Map* map, int currentStage, int clearFlag, int fontHandle);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
Map currentMap;
// 【解説・データ】現在のステージ番号とクリア状態を、ゲームループを越えて保持する。
int currentStage = 0;
int clearFlag = 0;
int fontHandle;
ChangeWindowMode(TRUE);
SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);
SetMainWindowText("Costco Man");
if (DxLib_Init() != 0)
return -1;
SetDrawScreen(DX_SCREEN_BACK);
fontHandle = CreateFontToHandle("MS ゴシック", TEXT_SIZE, 1);
// 【解説・今回変更】読み込むみ】ステージ番号を渡し、STAGESから対応するMapをコピーする。
LoadStage(¤tMap, currentStage);
while (ProcessMessage() == 0)
{
ClearDrawScreen();
// 【解説・流れ】1フレームで状態を更新してから、その更新後の状態を描画する。
UpdateGame(¤tMap, ¤tStage, &clearFlag);
DrawGame(¤tMap, currentStage, clearFlag, fontHandle);
ScreenFlip();
}
DeleteFontToHandle(fontHandle);
DxLib_End();
return 0;
}
// 【解説・役割】STAGES[stageNumber]をゲーム中のMapへコピーする。
void LoadStage(Map* map, int stageNumber)
{
map->stage_width = STAGES[stageNumber].stage_width;
map->stage_height = STAGES[stageNumber].stage_height;
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
map->Dat[y][x] = STAGES[stageNumber].Dat[y][x];
}
}
}
void DrawStageText(Map* map, int fontHandle)
{
char line[MAX_STAGE_WIDTH + 1];
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
int obj = map->Dat[y][x];
line[x] = OBJECT_CHAR[obj];
}
line[map->stage_width] = '\0';
DrawStringToHandle(
TEXT_STAGE_X,
TEXT_STAGE_Y + y * TEXT_SIZE,
line,
GetColor(255, 255, 255),
fontHandle);
}
}
Direction GetDirection(void)
{
// 4方向のキーが押された瞬間を調べる
int up = IsKeyPressed(KEY_INPUT_W);
int left = IsKeyPressed(KEY_INPUT_A);
int down = IsKeyPressed(KEY_INPUT_S);
int right = IsKeyPressed(KEY_INPUT_D);
if (up)
{
return DIR_UP;
}
if (left)
{
return DIR_LEFT;
}
if (down)
{
return DIR_DOWN;
}
if (right)
{
return DIR_RIGHT;
}
return DIR_NONE;
}
Point GetPlayerPos(Map* map)
{
Point result = { -1, -1 };
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
int obj = map->Dat[y][x];
if (obj == HUMAN || obj == HUMAN_ON_GOAL)
{
result.x = x;
result.y = y;
return result;
}
}
}
return result;
}
OBJNAME GetObjectNum(Point pos, Map* map)
{
if (pos.x < 0 || pos.x >= map->stage_width || pos.y < 0 || pos.y >= map->stage_height)
return OUTSIDE;
return (OBJNAME)map->Dat[pos.y][pos.x];
}
void SetObjtoMap(OBJNAME obj, Point pos, Map* map)
{
map->Dat[pos.y][pos.x] = obj;
}
int IsKeyPressed(int key)
{
static int previous[256] = { 0 };
int current = CheckHitKey(key);
int pressed = 0;
if (current != 0 && previous[key] == 0;0)
{
pressed = 1;
}
previous[key] = current;
return pressed;
}
MoveResult MoveObject(Direction dir, Map* map)
{
Point playerPos = GetPlayerPos(map);
Point nextPos = {
playerPos.x + DIR_VECTOR[dir].x,
playerPos.y + DIR_VECTOR[dir].y
};
OBJNAME current = GetObjectNum(playerPos, map);
OBJNAME next = GetObjectNum(nextPos, map);
if (next == FLOOR || next == GOAL)
{
if (current == HUMAN_ON_GOAL)
SetObjtoMap(GOAL, playerPos, map);
else
SetObjtoMap(FLOOR, playerPos, map);
if (next == GOAL)
SetObjtoMap(HUMAN_ON_GOAL, nextPos, map);
else
SetObjtoMap(HUMAN, nextPos, map);
return MOVE_WALK;
}
if (next == LUGG || next == LUGG_ON_GOAL)
{
Point nextNextPos = {
nextPos.x + DIR_VECTOR[dir].x,
nextPos.y + DIR_VECTOR[dir].y
};
OBJNAME nextNext = GetObjectNum(nextNextPos, map);
if (nextNext != FLOOR && nextNext != GOAL)
return MOVE_NONE;
if (current == HUMAN_ON_GOAL)
SetObjtoMap(GOAL, playerPos, map);
else
SetObjtoMap(FLOOR, playerPos, map);
if (next == LUGG_ON_GOAL)
SetObjtoMap(HUMAN_ON_GOAL, nextPos, map);
else
SetObjtoMap(HUMAN, nextPos, map);
if (nextNext == GOAL)
{
SetObjtoMap(LUGG_ON_GOAL, nextNextPos, map);
return MOVE_PUSH_GOAL;
}
SetObjtoMap(LUGG, nextNextPos, map);
return MOVE_PUSH;
}
return MOVE_NONE;
}
// 【解説・役割】Map全体を調べ、ゴールに入っていないLUGGが1つでもあれば未クリアとする。
int CheckClear(Map* map)
{
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
// 【解説・判定】LUGG_ON_GOALは数えない。通常のLUGGだけが残っているかを見る。
if (map->Dat[y][x] == LUGG)
return 0;
}
}
return 1;
}
// 【解説・役割】クリア後の次ステージ操作も、押した瞬間だけ反応させる。
int IsEnterPressed(void)
{
static int previous = 0;
int current = CheckHitKey(KEY_INPUT_RETURN);
int pressed = current != 0 && previous == 0;
previous = current;
return pressed;
}
int IsRPressed(void)
{
static int previous = 0;
int current = CheckHitKey(KEY_INPUT_R);
int pressed = current != 0 && previous == 0;
previous = current;
return pressed;
}
// 【解説・流れ】クリア中→リトライ→通常移動の優先順で処理する。
void UpdateGame(Map* map, int* currentStage, int* clearFlag)
{
// 【解説・理由】クリア表示中は移動を受け付けず、Enterによる次ステージだけを処理する。
if (*clearFlag)
{
if (IsEnterPressed()IsKeyPressed(KEY_INPUT_RETURN))
{
(*currentStage)++;
if (*currentStage >= STAGE_COUNT)
*currentStage = 0;
LoadStage(map, *currentStage);
*clearFlag = 0;
}
return;
}
// 【解説・理由】Rが押されたら現在ステージを読み直し、そのフレームの移動処理は行わない。
if (IsRPressed()IsKeyPressed(KEY_INPUT_R))
{
LoadStage(map, *currentStage);
return;
}
Direction dir = GetDirection();
if (dir != DIR_NONE)
{
// 【解説・流れ】実際に移動できた場合だけ、続けてクリア状態を調べる。
MoveResult result = MoveObject(dir, map);
// 【解説・判定】動けなかったフレームで毎回全Mapを調べないよう、移動が成功後したときだけ盤面が変わるため、そのときにクリア判定するを行う。
if (result != MOVE_NONE && CheckClear(map))
*clearFlag = 1;
}
}
// 【解説・描画】ゲーム状態を変更せず、ステージ番号・操作説明・クリア表示を描く。
void DrawGame(Map* map, int currentStage, int clearFlag, int fontHandle)
{
DrawFormatString(24, 20, GetColor(255, 255, 255), "STAGE %d / %d", currentStage + 1, STAGE_COUNT);
DrawString(300, 20, "WASD : MOVE R : RETRY", GetColor(200, 200, 200));
DrawStageText(map, fontHandle);
if (clearFlag)
{
DrawString(250, 360, "CLEAR!", GetColor(255, 220, 80));
DrawString(185, 405, "PRESS ENTER : NEXT STAGE", GetColor(255, 255, 255));
}
}