では、これまでやってきたことを踏まえて「迷路ゲーム」を作ってみます。
また、迷路?と言わずに付き合ってください(笑
実際全く新しいジャンルとなると、説明も非常に長くなりますし、コンソールでは限界がありますからね・・・
色々と復習しながら作っていきましょう。
今回は、極力マジックナンバーも使わずに作ろうと思います。
「マジックナンバー」についてはリンクからコラムを見てください。
まずは、ゲーム概要を書きます。
・迷路はファイルから読み込む ・ファイルの先頭に、 迷路のサイズ(縦、横) スタート位置(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■□■□■□■ ■□□■□■□■■□■□■□■ ■□□■□■□□□□■□■□■ ■□□■□■■■■■■□■□■ ■□□■□□□□□□□□■□■ ■□□■■■■■■■■■■□■ ■□□□□□□□□□□□□□■ ■■■■■■■■■■■■■■■ 続行するには何かキーを押してください・・・