TOP

C言語ゲーム制作入門

迷路を作る

列挙体、構造体、2次元配列

※2014/3/24 リニューアルしました。 プログラムの内容は変わっていませんが、関数名、変数名などが変わっているので、 以前のものを見てくださった方は注意してください。

まずは迷路を作ります。今回作るのは以下のような迷路です。

この迷路は25個のブロックから構成されています。 白が道、黒が壁、青がスタート地点、赤がゴール地点です。 最初はスタート地点とゴール地点以外の情報は隠されているものとします。

この25ブロックのそれぞれ1ブロックがもっている情報は「道、壁などの種類」と「隠されているかいないか」となります。 この1ブロックを列挙体と構造体であらわします。


//迷路の一ブロック
enum MazeKind {PATH, WALL, START, GOAL};    //ブロックの種類(道、壁、スタート、ゴール)
enum MazeFlag {FALSE, TRUE};                //ブロックが判明しているかどうか

typedef struct
{
  enum MazeKind kind;            //種類(道、壁、スタート、ゴール)
  enum MazeFlag flag;            //ブロックが判明しているかどうか
} MazeBlock;

わかりやすいように、これから自分で定義した型や関数は「Maze」からはじめることにします。 mazeは日本語で迷路のことです。 変数名や引数名なども目的をはっきりさせるためにわかりやすい名前をつけることが重要です。

列挙体

enumからはじまる行は列挙体の宣言です。 enumの次に書いてあるMazeKind、MazeFlagはそれぞれ列挙体のタグです。 {}の中に書かれたPATH, WALL, START, GOALは列挙定数で、先頭から順に0、1、2、3という整数値を表します。 そしてenum MazeKindが型名となります。 次の行も同様に、MazeFlagがタグで列挙定数はFALSEが0、TRUEが1、enum MazeFlagが型名となります。

構造体

structが構造体です。 構造体はデータの集まりです。 ここでは迷路を構成する一つのブロックそのものと考えてください。 上に書いたよう一つのブロックがもつデータは「道、壁などの種類」と「隠されているかいないか」です。 よってメンバにはenum MazeKind型の変数kindとenum MazeFlag型の変数flagを持っています。 int型でも代用することは可能ですが、列挙体を使うと列挙定数以外の値が代入されるのを防ぐことができます。

構造体はstruct (タグ) { ... }; のように宣言することもできますが、typedef宣言を利用するとタグを型名のように使うことができます。 typedef宣言を使わない場合は、上のコードの場合 struct MazeBlock が型名になります。 構造体はint型やchar型などと同じように扱うことができます。

この構造体を5行×5列ならべて構成されるのが今回作る迷路です。 したがって迷路を2次元配列で表します。


#define MAZE_ROW    5      //迷路の行数
#define MAZE_COLUMN 5      //迷路の列数

  //迷路
  MazeBlock maze[MAZE_ROW][MAZE_COLUMN] = 
    {
      { {START, TRUE } , {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE} },
      { {WALL , FALSE} , {WALL , FALSE}, {PATH , FALSE}, {WALL , FALSE}, {WALL , FALSE} },
      { {WALL , FALSE} , {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE} },
      { {PATH , FALSE} , {PATH , FALSE}, {WALL , FALSE}, {WALL , FALSE}, {WALL , FALSE} },
      { {WALL , FALSE} , {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE}, {GOAL , TRUE } },
    };

#defineを使って定数に名前を付けておくと、後でプログラムを変更するときに便利です。 例えば迷路のサイズを7×7に変更しようとしたとき、#defineの部分だけ書き換えれば後の配列の宣言は書き換える必要はありません。 (ただしこの場合は宣言と同時に初期化しているので、その値は増やす必要があります。)

2次元配列

配列の中身は最初の{}が一行目を表しています。 その中のさらに{}で囲まれた部分が、順番に一行目一列目のブロック、一行目二列目のプロック、・・・一行目五列目のプロックを表しています。

ブロックは構造体でenum MazeKind型とenum MazeFlag型のメンバを持っているので、それぞれ列挙定数で初期化しています。 上の迷路の絵と見比べて確認してください。 最初スタート地点とゴール地点以外は隠されているので、flagはスタート地点とゴール地点がTRUE、それ以外はFALSEとなります。

この迷路を文字で表示していきます。 スタートと道は空白、壁は"口"、ゴールは"G"で表します。 迷路を表示する関数MazeDrawを以下のように定義します。


//迷路表示
void MazeDraw(MazeBlock maze[MAZE_ROW][MAZE_COLUMN])
{
  int i, j;

  for(i = 0; i < MAZE_ROW; i++) //行
  {
    for(j = 0; j < MAZE_COLUMN; j++ ) //列
    {
      if(maze[i][j].flag == FALSE) //ブロックが判明していない場合
      {
        printf("?");
      }
      else
      {
        switch(maze[i][j].kind)
        {
        case WALL:
          printf("口"); break; //壁
        case GOAL:
          printf("G"); break; //ゴール
        default:
          printf(" "); break; //その他(道、スタート)
        }
      }
    }
    printf("\n");
  }
}

関数 MazeDraw
返り値
void
引数
MazeBlock型 maze[MAZE_ROW][MAZE_COLUMN] 迷路の情報
動作
引数mazeの各kindの値によって迷路のブロックをMAZE_ROW×MAZE_COLUMNの形に表示する。

返り値はvoidです。 すなわち返す情報は特に無いです。

外側のfor文が行番号、内側のfor文が列番号を表します。 内側のfor文を終了すると次の行に表示するために改行します。

ifの条件式で使われているmaze[i][j].flag、maze[i][j].kindという表現は構造体のメンバを表しています。 「構造体MazeBlock型の変数maze[i][j]がメンバとして持っている変数flag」という意味です。

switch文によって壁とゴールの場合はそれぞれ"口"と"G"を表示し、その他の場合は空白を表示します。 if文で書くこともできますが、列挙定数とあわせてswitch文で書くことによって、処理の内容がわかりやすくなります。

以上をまとめます。


#include <stdio.h>

#define MAZE_ROW    5      //迷路の行数
#define MAZE_COLUMN 5      //迷路の列数

//迷路の一ブロック
enum MazeKind {PATH, WALL, START, GOAL};    //ブロックの種類(道、壁、スタート、ゴール)
enum MazeFlag {FALSE, TRUE};                //ブロックが判明しているかどうか

typedef struct
{
  enum MazeKind kind;            //種類(道、壁、スタート、ゴール)
  enum MazeFlag flag;            //ブロックが判明しているかどうか
} MazeBlock;

//迷路表示
void MazeDraw(MazeBlock maze[MAZE_ROW][MAZE_COLUMN])
{
  int i, j;

  for(i = 0; i < MAZE_ROW; i++) //行
  {
    for(j = 0; j < MAZE_COLUMN; j++ ) //列
    {
      if(maze[i][j].flag == FALSE) //ブロックが判明していない場合
      {
        printf("?");
      }
      else
      {
        switch(maze[i][j].kind)
        {
        case WALL:
          printf("口"); break; //壁
        case GOAL:
          printf("G"); break; //ゴール
        default:
          printf(" "); break; //その他(道、スタート)
        }
      }
    }
    printf("\n");
  }
}

int main(void)
{
  //迷路
  MazeBlock maze[MAZE_ROW][MAZE_COLUMN] = 
    {
      { {START, TRUE } , {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE} },
      { {WALL , FALSE} , {WALL , FALSE}, {PATH , FALSE}, {WALL , FALSE}, {WALL , FALSE} },
      { {WALL , FALSE} , {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE} },
      { {PATH , FALSE} , {PATH , FALSE}, {WALL , FALSE}, {WALL , FALSE}, {WALL , FALSE} },
      { {WALL , FALSE} , {PATH , FALSE}, {PATH , FALSE}, {PATH , FALSE}, {GOAL , TRUE } },
    };
  
  //迷路表示
  MazeDraw(maze);

  return 0;
}

実行結果は以下のようになります。


 ????
?????
?????
?????
????G

また、ブロックのメンバflagをすべてTRUEにすると以下のようになります。


     
口口 口口
口    
  口口口
口   G

次はプレイヤーを表示します。

inserted by FC2 system