Skip to main content

第7回 文字版コスコマンを完成させよう

第7回 文字版コスコマンを完成させよう

前回までで、コスコマンは壁に止まり、ゴールを覚えたまま歩き、荷物を押せるようになりました。

今回は、ゲームとして最後まで遊ぶために、

  • クリア判定
  • 複数ステージ
  • リトライ
  • 次のステージへ進む処理

を追加します。

クリア判定

今回の流れ

プレイ中
  ↓
全ての荷物がゴールへ入った?
  ↓ YES
クリア表示
  ↓ ENTER
次のステージを読み込む

Rキーなら
現在のステージを最初から読み直す

今回追加・変更する関数

第7回では、ゲームを最後まで進めるための関数をまとめて用意します。

まず、既存の

void LoadStage(Map* map);

を、

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_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の全マスを二重ループで調べます。途中で普通の荷物を見つけた時点で「未クリア」と判断できます。

模範解答を見る
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.ステージを3つに増やそう

これまでは、最初のステージ1つだけを、

const Map START_STAGE = { ... };

としていました。

ゲームとして遊ぶなら、クリアしたあとに次のステージへ進みたいところです。

まず、ステージ数を定義します。

#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を持つことになります。


4.読み込むステージ番号を指定できるようにしよう

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] = ________________________________;
        }
    }
}

ヒント

今まで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()では、現在のステージ番号を持つ変数を追加します。

Map currentMap;

// 最初は0番目のステージから始める
int currentStage = ____________________;

そして読み込みを、

// 現在選ばれているステージをプレイ用Mapへ読み込む
LoadStage(&currentMap, ____________________);

へ変更します。

模範解答を見る
int currentStage = 0;
LoadStage(&currentMap, currentStage);

実行して確認

いったんcurrentStageを1、2へ変えて実行し、別のステージが表示されることを確認してください。

確認できたら0へ戻します。


5.クリア中かどうかを覚えておこう

クリアした瞬間に次のステージへ飛ぶと、クリアした盤面を見る時間がありません。

そこで、

0 → まだプレイ中
1 → クリア済み

を表すclearFlagを使います。

WinMain()へ追加します。

// まだクリアしていない状態から始める
int clearFlag = ____________________;
模範解答を見る
int clearFlag = 0;

6.ENTERとRキーを「押した瞬間」で使おう

クリア後はENTERで次のステージ、プレイ中はRでリトライできるようにします。

先ほど宣言した2つの関数本体を、CheckClear()の下へ追加してください。

int IsEnterPressed(void)
{
    static int previous = 0;
    int current = CheckHitKey(KEY_INPUT_RETURN);

    // 前のフレームでは離されていて、今のフレームで押されているときだけ1にする
    int pressed = ________________________________;

    previous = current;
    return pressed;
}

int IsRPressed(void)
{
    static int previous = 0;
    int current = CheckHitKey(KEY_INPUT_R);

    // Rキーも押しっぱなしではなく、押した瞬間だけ1にする
    int pressed = ________________________________;

    previous = current;
    return pressed;
}

ヒント

第3回のIsKeyPressed()と同じ考え方です。今と前のキー状態を比べます。

模範解答を見る
int pressed = current != 0 && previous == 0;

2つの関数は同じ式になります。


7.ゲーム中の更新をUpdateGame()へまとめよう

これからは、ゲームループの中に処理を増やし続けるのではなく、ゲームの更新をUpdateGame()へまとめます。

先ほど宣言したUpdateGame()の中身を、IsRPressed()の下へ追加します。

void UpdateGame(Map* map, int* currentStage, int* clearFlag)
{
    if (*clearFlag)
    {
        if (IsEnterPressed())
        {
            // 次のステージ番号へ進める
            (____________________)++;

            // 最後のステージ番号を越えたか調べる
            if (*currentStage >= ____________________)
                // 最後まで進んだら最初のステージ番号へ戻す
                *currentStage = ____________________;

            // 新しいステージをプレイ用Mapへ読み込む
            LoadStage(map, ____________________);

            // 新しいステージではクリア状態を解除する
            *clearFlag = ____________________;
        }

        return;
    }

    if (IsRPressed())
    {
        // 現在のステージを原本から読み直して最初の配置へ戻す
        LoadStage(map, ____________________);
        return;
    }

    Direction dir = GetDirection();

    if (dir != DIR_NONE)
    {
        MoveResult result = MoveObject(dir, map);

        // 実際に動いたあと、全荷物がゴールへ入ったか調べる
        if (result != MOVE_NONE && ________________________________)
            // クリアしたことを覚えておく
            *clearFlag = ____________________;
    }
}

ヒント

currentStageとclearFlagは、この関数の外にある値を書き換えるためポインタで受け取っています。クリア判定には、さきほど作った関数を使います。

模範解答を見る
void UpdateGame(Map* map, int* currentStage, int* clearFlag)
{
    if (*clearFlag)
    {
        if (IsEnterPressed())
        {
            (*currentStage)++;

            if (*currentStage >= STAGE_COUNT)
                *currentStage = 0;

            LoadStage(map, *currentStage);
            *clearFlag = 0;
        }

        return;
    }

    if (IsRPressed())
    {
        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.表示もDrawGame()へまとめよう

先ほど宣言したDrawGame()の中身を、UpdateGame()の下へ追加します。

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

    // 現在のプレイ用Mapを文字で描く
    ________________________________;

    if (clearFlag)
    {
        DrawString(250, 360, "CLEAR!", GetColor(255, 220, 80));
        DrawString(185, 405, "PRESS ENTER : NEXT STAGE", GetColor(255, 255, 255));
    }
}

ヒント

ステージを文字で表示する関数は第2回から使い続けています。

模範解答を見る
DrawStageText(map, fontHandle);

WinMain()のゲームループは、更新と描画を呼ぶ形へ整理します。

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

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

    // 現在のステージとクリア表示を描く
    DrawGame(____________________, ____________________, ____________________, ____________________);

    ScreenFlip();
}

ヒント

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

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

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

実行して、次を一通り確認してください。

  • WASDで歩ける
  • 壁を通れない
  • 荷物を押せる
  • 荷物をゴールへ置くと*になる
  • 全ての荷物をゴールへ置くとCLEAR!が出る
  • ENTERで次のステージへ進む
  • Rで現在のステージを最初からやり直せる
  • 3ステージ目のあと0番のステージへ戻る

STAGES[]は原本のまま残し、遊んでいる間に書き換えるのは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 = current != 0 && previous[key] == 0;

    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())
        {
            (*currentStage)++;

            if (*currentStage >= STAGE_COUNT)
                *currentStage = 0;

            LoadStage(map, *currentStage);
            *clearFlag = 0;
        }

        return;
    }

    if (IsRPressed())
    {
        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 = current != 0 && previous[key] == 0;

    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())
        {
            (*currentStage)++;

            if (*currentStage >= STAGE_COUNT)
                *currentStage = 0;

            LoadStage(map, *currentStage);
            *clearFlag = 0;
        }

        return;
    }

    // 【解説・理由】Rが押されたら現在ステージを読み直し、そのフレームの移動処理は行わない。
    if (IsRPressed())
    {
        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));
    }
}