★ゲームを作ろう9(迷路)★


では、これまでやってきたことを踏まえて「迷路ゲーム」を作ってみます。

また、迷路?と言わずに付き合ってください(笑

実際全く新しいジャンルとなると、説明も非常に長くなりますし、コンソールでは限界がありますからね・・・

色々と復習しながら作っていきましょう。


今回は、極力マジックナンバーも使わずに作ろうと思います。

マジックナンバー」についてはリンクからコラムを見てください。


まずは、ゲーム概要を書きます。

・迷路はファイルから読み込む

・ファイルの先頭に、
  迷路のサイズ(縦、横)
  スタート位置(X、Y)
  ゴール位置(X、Y)
 が格納されている

・ファイルは3ステージ分用意する

・ステージごとに迷路のサイズは異なる

・_kbhitを使って、asdwキーでプレイヤーを移動させる

・3ステージ終わるとゲームクリア

ステージのファイルは↓のようになっています。

こちらからプロジェクトファイルにダウンロードしてください。

maze1.txt

maze2.txt

maze3.txt

これに必要な定数や構造体を考えましょう。

まずは定数からです。

  /* 最初のステージ番号 */
  #define FIRST_STAGE 1
  
  /* ステージの最大数 */
  #define STAGE_MAX 3
  
  /* ファイル名の最大数 */
  #define FILE_NAME_MAX 12
  
  /* 表示用の文字列 */
  #define GRAPHIC_PLAYER "P"
  #define GRAPHIC_FLOOR  "□"
  #define GRAPHIC_WALL   "■"
  #define GRAPHIC_GOAL   "G"
  
  /* 入力用文字 */
  #define UP    'w'
  #define RIGHT 'd'
  #define DOWN  's'
  #define LEFT  'a'
  
  /* 迷路データ */
  enum {
      FLOOR,
      WALL,
      GOAL,
  };

次に、構造体を考えます。

typedef struct {
    int x;
    int y;
} Position;

typedef struct {
    int *pField;
    int row;
    int col;
    int stage;
} Maze;

最初のPosition構造体は、プレイヤーの位置などの座標を格納するための構造体です。

2Dゲームなどでは頻繁に使う構造体ですね。

次のMaze構造体は、迷路関係のデータが詰まっています。


次はmain関数を書きます。

<sample program 171-01>

int main(void)
{
    Maze maze;

    Position player;

    return 0;
}

プログラムの流れは関数を作りながら説明しましょう。


1.最初のステージ番号をセットする

関数名 SetFirstStage
戻り値 なし
引 数 Maze構造体のポインタ
機 能 最初のステージ番号をセットする
void SetFirstStage(Maze* const pMaze)
{
    pMaze->stage = FIRST_STAGE;
}

1行の内容ですが、関数にしておけばどこからでも呼び出せます。

例えば、3ステージを終了した後で呼び出せば、最初のステージに戻すことも可能になります。

また、FIRST_STAGEの部分を2や3に変えることで、デバッグ用にも使えます。

<sample program 171-02>

int main(void)
{
    Maze maze;

    Position player;

    /* 最初のステージ番号をセットする */
    SetFirstStage(&maze);

    return 0;
}

2.ステージ番号に応じた迷路データを読み込む

関数名 LoadMaze
戻り値 成功 1、失敗 0
引 数 Maze構造体のポインタ
    Position構造体のポインタ
機 能 ステージ番号に応じた迷路データを読み込む
int LoadMaze(Maze* const pMaze, Position* const pPlayer)
{
    FILE* fpMaze;
  
    char filename[FILE_NAME_MAX];
  
    Position goal;
  
    int i;
    int j;
  
    /* ステージ番号を基にファイル名を作成する */
    sprintf(filename, "maze%d.txt", pMaze->stage);
  
    /* ファイルを開く */
    fpMaze = fopen(filename, "r");
  
    if (!fpMaze) {
        return 0;
    }
  
    /* 縦横のサイズを読み込む */
    fscanf(fpMaze, "%d", &pMaze->row);
    fscanf(fpMaze, "%d", &pMaze->col);

    /* 迷路の領域を確保する */
    pMaze->pField = AllocateMaze(pMaze->row * pMaze->col);

    if (!pMaze->pField) {
        fclose(fpMaze);
        return 0;
    }
  
    /* スタート位置をセットする */
    fscanf(fpMaze, "%d", &pPlayer->x);
    fscanf(fpMaze, "%d", &pPlayer->y);
  
    /* ゴールの座標を読み込む */
    fscanf(fpMaze, "%d", &goal.x);
    fscanf(fpMaze, "%d", &goal.y);
  
    /* 迷路のデータを読み込む */
    for (i = 0; i < pMaze->row; i++) {
        for (j = 0; j < pMaze->col; j++) {
            fscanf(fpMaze, "%d", &pMaze->pField[i * pMaze->col + j]);
        }
    }

    /* ゴールをセットする */
    pMaze->pField[goal.y * pMaze->col + goal.x] = GOAL;

    /* ファイルを閉じる */
    fclose(fpMaze);

    fpMaze = NULL;

    return 1;
}

まずは、sprintf関数を使ってステージ番号に応じたファイル名を作成します。

迷路の領域を確保するため、縦横のサイズが必要になりますので、先に読み込みます。

縦横のサイズを元に迷路に必要な領域を確保します。

領域確保は別関数になっており、これまでと異なる作り方をしています。

※領域確保用関数は次で作ります。

スタート位置をプレイヤー用構造体に読み込みます。

ゴール位置を読み込みますが、まだ迷路にはセットしません。

迷路データを確保した領域に読み込みます。

その後、ゴール位置にゴールをセットします。

最後にファイルを閉じて、ダングリングポインタ対策を行って終わりです。

<sample program 171-03>

int main(void)
{
    Maze maze;

    Position player;

    /* 最初のステージ番号をセットする */
    SetFirstStage(&maze);

    /* ステージ番号に応じた迷路データを読み込む */
    if (!LoadMaze(&maze, &player)) {
        return 1;
    }

    return 0;
}

失敗すると「0」が戻されますので、エラー対応を行っています。


3.迷路を表示する

関数名 ShowMaze
戻り値 なし
引 数 Maze構造体のポインタ
    Position構造体のポインタ
機 能 迷路を表示する
void ShowMaze(const Maze* const pMaze, const Position* const pPlayer)
{
    int i;
    int j;
  
    system("cls");
  
    for (i = 0; i < pMaze->row; i++) {
        for (j = 0; j < pMaze->col; j++) {
            if (i == pPlayer->y && j == pPlayer->x) {
                printf(GRAPHIC_PLAYER);
            }
            else {
                switch (pMaze->pField[i * pMaze->col + j]) {
                case FLOOR:
                    printf(GRAPHIC_FLOOR);
                    break;
                case WALL:
                    printf(GRAPHIC_WALL);
                    break;
                case GOAL:
                    printf(GRAPHIC_GOAL);
                    break;
                }
            }
        }
        printf("\n");
    }
}

今回は、switch文を使って表示しています。

迷路用の文字列も#defineのところで変更することが出来ます。

<sample program 171-03>

int main(void)
{
    Maze maze;

    Position player;

    /* 最初のステージ番号をセットする */
    SetFirstStage(&maze);

    /* ステージ番号に応じた迷路データを読み込む */
    if (!LoadMaze(&maze, &player)) {
        return 1;
    }

    /* 迷路を表示する */
    ShowMaze(&maze, &player);

    return 0;
}

4.迷路の領域を確保する

関数名 AllocateMaze
戻り値 成功 確保した領域の先頭アドレス、失敗 NULLポインタ
引 数 確保する領域のサイズ(バイト数)
機 能 迷路の領域を確保し、先頭アドレスを返す
int* AllocateMaze(const int size)
{
    return (int*)malloc(sizeof(int) * size);
}

今回は、これまでと異なる作りをしてみました。

関数にはサイズだけを渡し、malloc関数の戻り値をそのままAllocateMaze関数の戻り値として使っています。

色々な形を知って欲しいので、このようにしました。

<sample program 171-03>

int main(void)
{
    Maze maze;

    Position player;

    /* 最初のステージ番号をセットする */
    SetFirstStage(&maze);

    /* ステージ番号に応じた迷路データを読み込む */
    if (!LoadMaze(&maze, &player)) {
        return 1;
    }

    /* 迷路を表示する */
    ShowMaze(&maze, &player);

    return 0;
}

main関数は変更無しですが、まだ実行しないでください。

メモリリークが発生します!


5.領域を解放する

関数名 ReleaseMaze
戻り値 なし
引 数 Maze構造体のポインタ
機 能 迷路の領域を解放する
void ReleaseMaze(Maze *pMaze)
{
    if (pMaze->pField) {
        free(pMaze->pField);
        pMaze->pField = NULL;
    }
}

以前紹介した「セーフリリース」という形にしています。

関数にしたことで、どこからでも呼び出せるようになっています。

間違ったところで呼び出されても、プログラム全体が危険な状態にならないよう配慮しています。

<sample program 171-04>

int main(void)
{
    Maze maze;

    Position player;

    /* 最初のステージ番号をセットする */
    SetFirstStage(&maze);

    /* ステージ番号に応じた迷路データを読み込む */
    if (!LoadMaze(&maze, &player)) {
        return 1;
    }

    /* 迷路を表示する */
    ShowMaze(&maze, &player);

    /* 領域を解放する */
    ReleaseMaze(&maze);

    return 0;
}

<実行結果>

■■■■■■■■■■
■P■□■■□■□■
■□■□■□□■□■
■□■□□□■■□■
■□■□■□□□□■
■□□□■□■■□■
■□■□■□□□□■
■■■□□□■□■■
■□□□■□■□G■
■■■■■■■■■■
続行するには何かキーを押してください・・・

やっと実行できるようになりました。


6.迷路を移動する

関数を作る前に、定数のところに1つ追加してください。

#define QUIT  'q'

デバッグ用のキーとして'q'キーを追加します。

関数名 Walk
戻り値 通常 1、緊急終了 0
引 数 Maze構造体のポインタ
    Position構造体のポインタ
機 能 asdwキーで迷路を移動する
    途中で緊急終了する場合はqキーを押す
int Walk(const Maze* const pMaze, Position* const pPlayer)
{
    if (_kbhit()) {

        int ch = _getch();

        switch (ch) {
        case UP:
            if (pMaze->pField[(pPlayer->y - 1) * pMaze->col + pPlayer->x] != WALL) {
                pPlayer->y--;
            }
            break;
        case RIGHT:
            if (pMaze->pField[pPlayer->y * pMaze->col + (pPlayer->x + 1)] != WALL) {
                pPlayer->x++;
            }
            break;
        case DOWN:
            if (pMaze->pField[(pPlayer->y + 1) * pMaze->col + pPlayer->x] != WALL) {
                pPlayer->y++;
            }
            break;
        case LEFT:
            if (pMaze->pField[pPlayer->y * pMaze->col + (pPlayer->x - 1)] != WALL) {
                pPlayer->x--;
            }
            break;
        case QUIT:
            return 0;
            break;
        }
    }

    return 1;
}

_kbhitと_getchを使った移動です。

移動部分は説明不要かも知れませんが、緊急終了時に'q'を押せるようになっています。

プログラムのデバッグ中はゲームを最後まで確認することなく、途中で終了出来るように考えました。

後でmain関数を見てもらえば分かると思いますが、この移動関数は無限ループの中で呼び出されます。

ウィンドウの×ボタンなどで終了すると、ReleaseMaze関数が呼ばれずメモリリークが発生します。

そこで、途中で止めたくなった場合の手段を講じておきます。

<sample program 171-05>

int main(void)
{
    Maze maze;

    Position player;

    /* 最初のステージ番号をセットする */
    SetFirstStage(&maze);

    /* ステージ番号に応じた迷路データを読み込む */
    if (!LoadMaze(&maze, &player)) {
        return 1;
    }

    for (;;) {

        /* 迷路を移動する */
        if (!Walk(&maze, &player)) {
            break;
        }

        /* 迷路を表示する */
        ShowMaze(&maze, &player);
    }

    /* 領域を解放する */
    ReleaseMaze(&maze);

    return 0;
}

<実行結果>

■■■■■■■■■■
■□■□■■□■□■
■□■□■□□■□■
■□■□□□■■□■
■□■□■P□□□■
■□□□■□■■□■
■□■□■□□□□■
■■■□□□■□■■
■□□□■□■□G■
■■■■■■■■■■
続行するには何かキーを押してください・・・

途中で'q'キーを押して終了してください。


7.ゴールしたかどうかチェックする

関数名 CheckGoal
戻り値 ゴールした 1、ゴールしていない 0
引 数 Maze構造体のポインタ
    Position構造体のポインタ
機 能 ゴールしたかどうかチェックし結果を返す
int CheckGoal(const Maze* const pMaze, const Position* const pPlayer)
{
    if (pMaze->pField[pPlayer->y * pMaze->col + pPlayer->x] == GOAL) {
        return 1;
    }

    return 0;
}

単純にゴールの位置にいるかどうかチェックする関数です。

<sample program 171-06>

int main(void)
{
    Maze maze;

    Position player;

    /* 最初のステージ番号をセットする */
    SetFirstStage(&maze);

    /* ステージ番号に応じた迷路データを読み込む */
    if (!LoadMaze(&maze, &player)) {
        return 1;
    }

    for (;;) {

        /* ゴールしたかどうかチェックする */
        if (CheckGoal(&maze, &player)) {
            break;
        }

        /* 迷路を移動する */
        if (!Walk(&maze, &player)) {
            break;
        }

        /* 迷路を表示する */
        ShowMaze(&maze, &player);
    }

    /* 領域を解放する */
    ReleaseMaze(&maze);

    return 0;
}

<実行結果>

■■■■■■■■■■
■□■□■■□■□■
■□■□■□□■□■
■□■□□□■■□■
■□■□■□□□□■
■□□□■□■■□■
■□■□■□□□□■
■■■□□□■□■■
■□□□■□■□P■
■■■■■■■■■■
続行するには何かキーを押してください・・・

8.ステージを進める

関数名 NextStage
戻り値 ゲーム終了 1、次のステージへ 0
引 数 Maze構造体のポインタ
機 能 ステージ番号を進め、ゲームがクリアされたかどうか返す
int NextStage(Maze* const pMaze)
{
    pMaze->stage++;

    if (pMaze->stage > STAGE_MAX) {
        return 1;
    }

    return 0;
}

現在の無限ループの外側にもう1つ無限ループを作り、この関数を呼び出すようにします。

<sample program 171-07>

int main(void)
{
    Maze maze;

    Position player;

    /* 最初のステージ番号をセットする */
    SetFirstStage(&maze);

    for (;;) {

        /* ステージ番号に応じた迷路データを読み込む */
        if (!LoadMaze(&maze, &player)) {
            return 1;
        }

        for (;;) {

            /* ゴールしたかどうかチェックする */
            if (CheckGoal(&maze, &player)) {
                break;
            }

            /* 迷路を移動する */
            if (!Walk(&maze, &player)) {
                break;
            }

            /* 迷路を表示する */
            ShowMaze(&maze, &player);
        }

        /* 領域を解放する */
        ReleaseMaze(&maze);

        /* ステージを進める */
        if (NextStage(&maze)) {
            break;
        }
    }

    return 0;
}

<実行結果>

■■■■■■■■■■■■■■■
■□□□□□□□□□□□□□■
■□□■■■■■■■■■■□■
■□□□□□□□□□□□■□■
■□□■■■■■■■■□■□■
■□□■□□□□□□■□■□■
■□□■□■■■■□■□■□■
■□□■□■□P■□■□■□■
■□□■□■□■■□■□■□■
■□□■□■□□□□■□■□■
■□□■□■■■■■■□■□■
■□□■□□□□□□□□■□■
■□□■■■■■■■■■■□■
■□□□□□□□□□□□□□■
■■■■■■■■■■■■■■■
続行するには何かキーを押してください・・・

完成です!

ファイル分割はしていませんので、皆さんはファイルの分割も試してください。

少しずつ、大きなプログラムが作れるように慣れていきましょう。


最後に全文を載せておきます。

<sample program 171-08>

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>

/* 最初のステージ番号 */
#define FIRST_STAGE 1
  
/* ステージの最大数 */
#define STAGE_MAX 3

/* ファイル名の最大数 */
#define FILE_NAME_MAX 12

/* 表示用の文字列 */
#define GRAPHIC_PLAYER "P"
#define GRAPHIC_FLOOR  "□"
#define GRAPHIC_WALL   "■"
#define GRAPHIC_GOAL   "G"

/* 入力用文字 */
#define UP    'w'
#define RIGHT 'd'
#define DOWN  's'
#define LEFT  'a'
#define QUIT  'q'

/* 迷路データ */
enum {
    FLOOR,
    WALL,
    GOAL,
};

/* 位置構造体 */
typedef struct {
    int x;
    int y;
} Position;

/* 迷路構造体 */
typedef struct {
    int *pField;
    int row;
    int col;
    int stage;
} Maze;

/* プロトタイプ宣言 */

void SetFirstStage(Maze* const pMaze);

int LoadMaze(Maze* const pMaze, Position* const pPlayer);

void ShowMaze(const Maze* const pMaze, const Position* const pPlayer);

int* AllocateMaze(const int size);

void ReleaseMaze(Maze *pMaze);

int Walk(const Maze* const pMaze, Position* const pPlayer);

int CheckGoal(const Maze* const pMaze, const Position* const pPlayer);

int NextStage(Maze* const pMaze);

int main(void)
{
    Maze maze;

    Position player;

    /* 最初のステージ番号をセットする */
    SetFirstStage(&maze);

    for (;;) {

        /* ステージ番号に応じた迷路データを読み込む */
        if (!LoadMaze(&maze, &player)) {
            return 1;
        }

        for (;;) {

            /* ゴールしたかどうかチェックする */
            if (CheckGoal(&maze, &player)) {
                break;
            }

            /* 迷路を移動する */
            if (!Walk(&maze, &player)) {
                break;
            }

            /* 迷路を表示する */
            ShowMaze(&maze, &player);
        }

        /* 領域を解放する */
        ReleaseMaze(&maze);

        /* ステージを進める */
        if (NextStage(&maze)) {
            break;
        }
    }

    return 0;
}

/* 関数本体 */

void SetFirstStage(Maze* const pMaze)
{
    pMaze->stage = FIRST_STAGE;
}

int LoadMaze(Maze* const pMaze, Position* const pPlayer)
{
    FILE* fpMaze;
  
    char filename[FILE_NAME_MAX];
  
    Position goal;
  
    int i;
    int j;
  
    sprintf(filename, "maze%d.txt", pMaze->stage);
  
    fpMaze = fopen(filename, "r");
  
    if (!fpMaze) {
        return 0;
    }
  
    fscanf(fpMaze, "%d", &pMaze->row);
    fscanf(fpMaze, "%d", &pMaze->col);
  
    pMaze->pField = AllocateMaze(pMaze->row * pMaze->col);

    if (!pMaze->pField) {
        fclose(fpMaze);
        return 0;
    }
  
    fscanf(fpMaze, "%d", &pPlayer->x);
    fscanf(fpMaze, "%d", &pPlayer->y);
  
    fscanf(fpMaze, "%d", &goal.x);
    fscanf(fpMaze, "%d", &goal.y);
  
    for (i = 0; i < pMaze->row; i++) {
        for (j = 0; j < pMaze->col; j++) {
            fscanf(fpMaze, "%d", &pMaze->pField[i * pMaze->col + j]);
        }
    }
  
    pMaze->pField[goal.y * pMaze->col + goal.x] = GOAL;
  
    fclose(fpMaze);
  
    fpMaze = NULL;
  
    return 1;
}
    
void ShowMaze(const Maze* const pMaze, const Position* const pPlayer)
{
    int i;
    int j;

    system("cls");

    for (i = 0; i < pMaze->row; i++) {
        for (j = 0; j < pMaze->col; j++) {
            if (i == pPlayer->y && j == pPlayer->x) {
                printf(GRAPHIC_PLAYER);
            }
            else {
                switch (pMaze->pField[i * pMaze->col + j]) {
                case FLOOR:
                    printf(GRAPHIC_FLOOR);
                    break;
                case WALL:
                    printf(GRAPHIC_WALL);
                    break;
                case GOAL:
                    printf(GRAPHIC_GOAL);
                    break;
                }
            }
        }
        printf("\n");
    }
}

int* AllocateMaze(const int size)
{
    return (int*)malloc(sizeof(int) * size);
}

void ReleaseMaze(Maze *pMaze)
{
    if (pMaze->pField) {
        free(pMaze->pField);
        pMaze->pField = NULL;
    }
}

int Walk(const Maze* const pMaze, Position* const pPlayer)
{
    if (_kbhit()) {
  
        int ch = _getch();
  
        switch (ch) {
        case UP:
            if (pMaze->pField[(pPlayer->y - 1) * pMaze->col + pPlayer->x] != WALL) {
                pPlayer->y--;
            }
            break;
        case RIGHT:
            if (pMaze->pField[pPlayer->y * pMaze->col + (pPlayer->x + 1)] != WALL) {
                pPlayer->x++;
            }
            break;
        case DOWN:
            if (pMaze->pField[(pPlayer->y + 1) * pMaze->col + pPlayer->x] != WALL) {
                pPlayer->y++;
            }
            break;
        case LEFT:
            if (pMaze->pField[pPlayer->y * pMaze->col + (pPlayer->x - 1)] != WALL) {
                pPlayer->x--;
            }
            break;
        case QUIT:
            return 0;
            break;
        }
    }

    return 1;
}

int CheckGoal(const Maze* const pMaze, const Position* const pPlayer)
{
    if (pMaze->pField[pPlayer->y * pMaze->col + pPlayer->x] == GOAL) {
        return 1;
    }
  
    return 0;
}
  
int NextStage(Maze* const pMaze)
{
    pMaze->stage++;

    if (pMaze->stage > STAGE_MAX) {
        return 1;
    }

    return 0;
}

<実行結果>

■■■■■■■■■■■■■■■
■□□□□□□□□□□□□□■
■□□■■■■■■■■■■□■
■□□□□□□□□□□□■□■
■□□■■■■■■■■□■□■
■□□■□□□□□□■□■□■
■□□■□■■■■□■□■□■
■□□■□■□P■□■□■□■
■□□■□■□■■□■□■□■
■□□■□■□□□□■□■□■
■□□■□■■■■■■□■□■
■□□■□□□□□□□□■□■
■□□■■■■■■■■■■□■
■□□□□□□□□□□□□□■
■■■■■■■■■■■■■■■
続行するには何かキーを押してください・・・

次へ

戻る

目次へ