Skip to main content

第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(&currentMap))
    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(&currentMap, ____________________);

へ変更します。

模範解答を見る
int currentStage = 0;
LoadStage(&currentMap, 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);


8.WinMain()から更新と表示を呼び出そう

ゲームループでは、最初にゲーム状態を変更し、その変更後の状態を画面へ表示します。

UpdateGame()でゲーム状態を変更する
        ↓
DrawGame()で現在の状態を表示する

完成条件

  • UpdateGame()へcurrentMap、currentStage、clearFlagのアドレスを渡す
  • DrawGame()へ表示に必要な4つの値を渡す
  • 更新してから表示する順番にする

第6回のゲームループにあったGetDirection()、MoveObject()、DrawStageText()の呼び出しは、更新これからUpdateGame()と描画を呼ぶ形へ整理DrawGame()が担当します。while (ProcessMessage() == 0)から対応する閉じかっこまでを次の形へ置き換え、空欄を埋めてください。

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

    // ゲームの入力・に応じて、移動・クリア・ステージ切り替えを更新処理する
    UpdateGame(____________________, ____________________, ____________________);

    // 現在更新後のステージと番号、Map、クリア状態を表示を描くする
    DrawGame(____________________, ____________________, ____________________, ____________________);

    ScreenFlip();
}

ヒント

UpdateGame()ではステージ番号currentStageとクリア状態clearFlagを書き換えるため、アドレス&を付けて渡します。DrawGame()は表示するだけなので、ステージ番号とクリア状態は値のまま渡せるものがありします。

模範解答を見る
UpdateGame(&currentMap, &currentStage, &clearFlag);
DrawGame(&currentMap, currentStage, clearFlag, fontHandle);

9.文字版を最後まで遊んでみよう

実行して、次の動きを一通りすべて確認してください。

  • WASDで歩ける
  • 壁を通れない
  • 荷物を押せる
  • 荷物をゴールへ置くと*になる
  • 全すべての荷物をゴールへ置くとCLEAR!が出表示される
  • ENTERでクリア中はWASDを押してもコスコマンが動かない
  • ENTERを押すと次のステージへ進を読み込む
  • RでRを押すと現在のステージを最初からやり開始時の配置で読み込み直せるす
  • 3ステージ3つ目のあと0番のステージへ戻るをクリアしたあとは、1つ目のステージを読み込む

STAGES[]STAGESには原本各ステージの開始時のデータが入っていまま残し、遊んでいる間す。プレイ中に書き換えるのはcurrentMapだけです。

そのRを押したときは、STAGES[currentStage]をcurrentMapへコピーするため、何度リトライしてもLoadStage()で元現在のステージを開始時の配置へ戻で遊び直せます。

これで、文字だけで最後まで遊べるコスコマンが完成しました。

次回から、ゲームのルールはそのまま変えずに、文字表示を画像表示へ変えていき更します。


10.もっとステージを増やしてみよう

ここまでできれば、STAGESへ新しいMapを追加することで、さらにステージを増やせます。

自分でステージを考えてもよいですが、倉庫番形式のステージデータを公開しているサイトを参考にする方法もあります。

テキスト形式のステージ集を参考にする

Google DeepMindが公開しているBoxoban Levelsには、箱を押してゴールへ運ぶパズルのステージデータが大量に収録されています。

ステージは.txtファイルに、10×10の文字の並びとして記録されています。1つのテキストファイルには複数のステージが入り、;と番号の行がステージの区切りになっています。

使われている主な文字は、コスコマンとほぼ同じ意味です。

Boxoban 意味 コスコマン
# 壁 WALL
半角スペース 床 FLOOR
@ プレイヤー HUMAN
$ 荷物 LUGG
. ゴール GOAL

例えば、テキストの

##########
#   .    #
#   $    #
#   @    #
##########

という形を見ながら、

{ WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL },

のように、コスコマンのMapデータへ置き換えていけば使えます。

Boxobanのステージは10×10なので、現在の

#define MAX_STAGE_WIDTH  10
#define MAX_STAGE_HEIGHT 10

の範囲にも収まります。

新しいステージを追加したら、STAGESへMapを増やし、STAGE_COUNTもその数に合わせて変更してください。

注意: 公開されているステージなら何でも自由に転載できるとは限りません。Boxoban LevelsはApache License 2.0で公開されていますが、利用・改変・再配布するときは配布元のライセンス条件を確認してください。このページではステージデータそのものを転載せず、配布元へのリンクを紹介しています。


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(&currentMap, currentStage);

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

        UpdateGame(&currentMap, &currentStage, &clearFlag);
        DrawGame(&currentMap, 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(&currentMap, currentStage);

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

        // 【解説・流れ】1フレームで状態を更新してから、その更新後の状態を描画する。
        UpdateGame(&currentMap, &currentStage, &clearFlag);
        DrawGame(&currentMap, 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));
    }
}