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


次は迷路を作ってみたいと思います。

二次元配列を迷路に見立て、壁と床を作ります。

イメージは↓の通りです。

maze   0 1 2 3 4 5 6 7 8 9
   +−+−+−+−+−+−+−+−+−+−+
  0|0|0|0|0|0|0|0|0|0|0|
   +−+−+−+−+−+−+−+−+−+−+
  1|0|1|1|1|0|1|1|0|2|0|
   +−+−+−+−+−+−+−+−+−+−+
  2|0|1|0|1|1|1|1|0|1|0|
   +−+−+−+−+−+−+−+−+−+−+
  3|0|1|0|1|0|1|0|0|1|0|
   +−+−+−+−+−+−+−+−+−+−+
  4|0|1|0|1|0|1|1|0|1|0|
   +−+−+−+−+−+−+−+−+−+−+
  5|0|1|0|1|0|1|1|1|1|0|
   +−+−+−+−+−+−+−+−+−+−+
  6|0|1|0|1|0|1|1|0|1|0|
   +−+−+−+−+−+−+−+−+−+−+
  7|0|1|0|1|0|1|0|0|1|0|
   +−+−+−+−+−+−+−+−+−+−+
  8|0|1|0|1|1|1|1|0|1|0|
   +−+−+−+−+−+−+−+−+−+−+
  9|0|0|0|0|0|0|0|0|0|0|
   +−+−+−+−+−+−+−+−+−+−+
    0 壁
    1 床(通路)
    2 ゴール

数字では分かりづらいので、前回の宝探しのように記号で置き換えてみます。

■■■■■■■■■■
■□□□■□□■G■
■□■□□□□■□■
■□■□■□■■□■
■□■□■□□■□■
■□■□■□□■□■
■□■□■□□□□■
■□■□■□■■□■
■□■□□□□■□■
■■■■■■■■■■

こちらの方が分かりやすいですね。


まずはここまで作ってみましょう。

準備するものとして、#defineで ROW と COL が必要ですので両方「10」で設定してください。

列挙体で、WALL、FLOOR、GOALの順に定義しておきます。

int型配列 maze を作り、初期値として上の迷路データを入れておきます。

とりあえず、ここまで作ってみましょう。









































解答例です。


<sample program 094-01>

#include <stdio.h>

#define ROW 10
#define COL 10

enum {
    WALL,
    FLOOR,
    GOAL,
};

int main(void)
{
    int maze[ROW][COL] = {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
        { 0, 1, 1, 1, 0, 1, 1, 0, 2, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    };

    return 0;
}

次に迷路を記号で表示してみましょう。

今回は、壁(■)、床(□)、ゴール(G)の3種類ありますので、switch文を使って作りましょう。

二重ループを作り、switch文でmaze[i][j]を調べ、記号を表示させてください。

<実行結果>

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









































解答例です。


<sample program 094-02>

#include <stdio.h>

#define ROW 10
#define COL 10

enum {
    WALL,
    FLOOR,
    GOAL,
};

int main(void)
{
    int maze[ROW][COL] = {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
        { 0, 1, 1, 1, 0, 1, 1, 0, 2, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    };

    int i;
    int j;
    
    for (i = 0; i < ROW; i++) {
        for ( j = 0; j < COL; j++) {
            switch (maze[i][j]) {
            case WALL:
                printf("■");
                break;
            case FLOOR:
                printf("□");
                break;
            case GOAL:
                printf("G");
                break;
            }
        }
        printf("\n");
    }

    return 0;
}

<実行結果>

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

とりあえず自作の迷路を作った方も一番外側は「壁」で囲むように作ってください。

理由は後で書きます。


これで迷路自体は出来ました。

次はプレイヤーの設定をどうするか考えなければなりません。

一つは二次元配列のデータとしてプレイヤーを設定する方法があります。

enum {
    WALL,
    FLOOR,
    GOAL,
    PLAYER,
};

として、

int maze[ROW][COL] = {
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    { 0, 1, 1, 1, 0, 1, 1, 0, 2, 0 },
    { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
    { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
    { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
    { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
    { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0 },
    { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
    { 0, 3, 0, 1, 1, 1, 1, 0, 1, 0 },
    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
};

スタート地点にプレイヤー(3)を設定し、表示する方法です。

■■■■■■■■■■
■□□□■□□■G■
■□■□□□□■□■
■□■□■□■■□■
■□■□■□□■□■
■□■□■□□■□■
■□■□■□□□□■
■□■□■□■■□■
■P■□□□□■□■
■■■■■■■■■■

出来そうな気がしますが、この方法は採りません。

何故かと言うと、この方法だとプレイヤーのいる場所が床だという情報が無くなってしまいます

例えば床と言っても色々な種類の床(トラップなど)を配置したい時もあるでしょう。

2D等のグラフィックを使ったゲームとなった場合、プレイヤーの下に床も表示する必要があるため、情報が無くなるのは困ります。


では、別の方法を説明します。

プレイヤーの座標(X座標、Y座標)を用意し、その座標にある床などを表示する際に、プレイヤーを表示する方法です。

printfは左から右へ表示することしか出来ません。

これまで学習した内容では、すでに表示された文字の上に新しい文字を表示することは出来ません。

床を表示した後でプレイヤーをその上に表示することが出来ないため、床の代わりにプレイヤーを表示しますが、座標情報を元に表示しますので、迷路データそのものは破壊されません。


では、実際に作ってみましょう。

プレイヤーの座標用にint型で px と py という変数を作ります。

初期値としてスタート地点の座標(px = 1; py = 8;)を入れておきましょう。

表示するプログラムに、プレイヤーのいる座標の場合、迷路ではなくプレイヤー(P)を表示するよう変更を加えます。









































解答例です。


<sample program 094-03>

#include <stdio.h>

#define ROW 10
#define COL 10

enum {
    WALL,
    FLOOR,
    GOAL,
};

int main(void)
{
    int maze[ROW][COL] = {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
        { 0, 1, 1, 1, 0, 1, 1, 0, 2, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    };

    int i;
    int j;

    int px = 1;
    int py = 8;

    for (i = 0; i < ROW; i++) {
        for ( j = 0; j < COL; j++) {
            if (i == py && j == px) {
                printf("P");
            }
            else{
                switch (maze[i][j]) {
                case WALL:
                    printf("■");
                    break;
                case FLOOR:
                    printf("□");
                    break;
                case GOAL:
                    printf("G");
                    break;
                }
            }
        }
        printf("\n");
    }

    return 0;
}

<実行結果>

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

二重ループのiとjが迷路の座標を表しています。

iが縦方向、jが横方向を示しています。

iと py、jと px が同じであれば、そこはプレイヤーのいる場所ということなります。

プレイヤーのいる場所では、プレイヤーの記号(P)を表示し、迷路のデータは表示しません。


プレイヤーの座標を用意しておくと、プレイヤーを動かすことも簡単に出来ます。

まず、プレイヤーを動かす準備をします。

プレイヤーが動くということは、ユーザーから何かしらの入力が無いと動けません。

動くたびに位置が変わりますので、何度も表示し直す必要があります。

そこで、表示部分をループさせましょう。

無限ループを使いますが、迷路が表示され続けますので、画面のクリアも付けておきます。

<sample program 094-04>

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

#define ROW 10
#define COL 10

enum {
    WALL,
    FLOOR,
    GOAL,
};

int main(void)
{
    int maze[ROW][COL] = {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
        { 0, 1, 1, 1, 0, 1, 1, 0, 2, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    };

    int i;
    int j;

    int px = 1;
    int py = 8;

    for (;;) {

        system("cls");

        for (i = 0; i < ROW; i++) {
            for (j = 0; j < COL; j++) {
                if (i == py && j == px) {
                    printf("P");
                }
                else{
                    switch (maze[i][j]) {
                    case WALL:
                        printf("■");
                        break;
                    case FLOOR:
                        printf("□");
                        break;
                    case GOAL:
                        printf("G");
                        break;
                    }
                }
            }
            printf("\n");
        }
    }

    return 0;
}

<実行結果>

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

画面がちらつくと思いますが、これはどうにもなりませんので諦めてください。


プレイヤーを動かす方法について考えます。

これも何通りかの方法があります。

一つはscanfなどで数値または文字を入力し、対応した方向に進む方法です。

一回一回scanfで止まりますので、ちらつくことは無くなりますが、いちいちEnterキーを押さなければならない不便さがあります。

もう一つは、スロット作成の時にやった_kbhitを使う方法です。

_kbhit であればEnterキーを押さなくても入力が可能ですが、入力のためにプログラムが止まることがありませんので、画面のちらつきが発生します。

今回は利便性を求めて _kbhit を使うことにします。


では、どのキーでどの方向に進むか決めましょう。

テンキーが付いていないPCもありますので、テンキーは除外します。

カーソルキー(矢印キー)は _kbhit では使えません。

そこで、PCゲームでプレイヤーの移動に良く使われているASDWキーを使います。

 A 左移動

 S 下移動

 D 右移動

 W 上移動

キーは決まりましたが、移動させるにはどのようなプログラムを組めば良いのでしょうか。

移動するということは、位置が変わるということです。

プレイヤーの位置を変えれば良いわけですから、座標を変更すれば良いですね。

px がX座標、py がY座標を示しています。

二次元配列上で、右に移動するといった場合、X座標を増やせば右に移動することになります。

上に移動させたい場合はY座標を減らせば良いです。

入力されたキーに合わせて座標を増やしたり減らしたりすることで移動が可能になります。

では、実際に作ってみましょう。

<sample program 094-05>

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

#define ROW 10
#define COL 10

enum {
    WALL,
    FLOOR,
    GOAL,
};

int main(void)
{
    int maze[ROW][COL] = {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
        { 0, 1, 1, 1, 0, 1, 1, 0, 2, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    };

    int i;
    int j;

    int px = 1;
    int py = 8;

    int key;

    for (;;) {

        if (_kbhit()) {

            key = _getch();

            switch (key) {
            case 'a':
                px--;
                break;
            case 's':
                py++;
                break;
            case 'd':
                px++;
                break;
            case 'w':
                py--;
                break;
            }
        }

        system("cls");

        for (i = 0; i < ROW; i++) {
            for (j = 0; j < COL; j++) {
                if (i == py && j == px) {
                    printf("P");
                }
                else{
                    switch (maze[i][j]) {
                    case WALL:
                        printf("■");
                        break;
                    case FLOOR:
                        printf("□");
                        break;
                    case GOAL:
                        printf("G");
                        break;
                    }
                }
            }
            printf("\n");
        }
    }

    return 0;
}

<実行結果>

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

プレイヤーを移動させることが出来ました。

しかし、壁も床も関係なく移動することが出来ますので、迷路の意味がありません。

次は壁との当たり判定を行います。


壁に衝突するかどうかは、プレイヤーが実際に一歩進む前にチェックした方が良さそうです。

 一歩進んでみて、プレイヤーのいる場所が壁だったら一歩戻す。

よりも、

 進む前に一歩先を調べて、壁でなければ進む。

と考えます。


上の方で、周りを壁で囲むように迷路を作って、と書きました。

色々な考え方がありますが、一歩先を調べる時に配列の添え字の範囲外を指してしまう危険性があるため、そのように書きました。

■■■■■■■■■■
■□□□■□□■G■
■□■□□□□■□■
■□■□■□■■□■
■□■□■□□■□■
■□■□■□□■□■
■□■□■□□□□P
□□□□■□■■□■
■□■□□□□■□□
■□■■■■■■□■

例えばこのように外側を壁で囲んでいない迷路があり、プレイヤーが一番外側のマスにいたとします。

ここで右へ移動を選んだ場合、配列の範囲外を調べることになり危険です。

このような迷路の場合は外に出ないようにするプログラムを別途組み込む必要があります。

外側を壁で囲んでいる場合は壁との判定さえ作っていれば範囲外に出ていくことはありません。

要は「余計なことを考えなくて楽!」ということですね。


一言で一歩先と言っても、上下左右あります。

それぞれの方向に進む前に一歩先を調べる必要があります。

例えば上に歩こうとした場合、

maze[py - 1][px]

を調べることで歩けるかどうか分かります。

具体的に書くと、

if (maze[py - 1][px] != WALL) {
    py--;
}

このような感じですね。

では、上下左右全て作ってみましょう。









































解答例です。


<sample program 094-06>

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

#define ROW 10
#define COL 10

enum {
    WALL,
    FLOOR,
    GOAL,
};

int main(void)
{
    int maze[ROW][COL] = {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
        { 0, 1, 1, 1, 0, 1, 1, 0, 2, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    };

    int i;
    int j;

    int px = 1;
    int py = 8;

    int key;

    for (;;) {

        if (_kbhit()) {

            key = _getch();

            switch (key) {
            case 'a':
                if (maze[py][px - 1] != WALL) {
                    px--;
                }
                break;
            case 's':
                if (maze[py + 1][px] != WALL) {
                    py++;
                }
                break;
            case 'd':
                if (maze[py][px + 1] != WALL) {
                    px++;
                }
                break;
            case 'w':
                if (maze[py - 1][px] != WALL) {
                    py--;
                }
                break;
            }
        }

        system("cls");

        for (i = 0; i < ROW; i++) {
            for (j = 0; j < COL; j++) {
                if (i == py && j == px) {
                    printf("P");
                }
                else{
                    switch (maze[i][j]) {
                    case WALL:
                        printf("■");
                        break;
                    case FLOOR:
                        printf("□");
                        break;
                    case GOAL:
                        printf("G");
                        break;
                    }
                }
            }
            printf("\n");
        }
    }

    return 0;
}

<実行結果>

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

これで壁との当たり判定が出来ました。

後はゴールするプログラムを作れば基本形は完成です。

とりあえず、ゴールしたらプログラムを終了するように作ってみましょう。

気を付けなければならないところは、ゴールした瞬間が見えるようにプログラムを組むことです。


これまでも色々なプログラムを紹介してきましたが、プログラムは手順が大事です。

プログラムを書く順番によって結果が異なることも多々あります。

今回もゴールするプログラム

if (maze[py][px] == GOAL) {
   break;
}

をどこに挿入するかによって見え方が変わってきます。

特に、移動前に書くのか、移動後に書くのかで大きく変わります。


移動後に書くと、

 ・移動する(ここでゴールした)

 ・ゴールしたかどうか調べる(ゴールしているので終了)

 ・表示(実行されない)

このように、表示する前に無限ループを抜けますので、ゴールした絵が見れません。


移動前に書くと、

 ・ゴールしたかどうか調べる(まだゴールしていない)

 ・移動する(ここでゴールした)

 ・表示する(ゴールの場所にプレイヤーが表示される)

 ・ゴールしたかどうか調べる(ゴールしているので終了)

きちんと表示された後で終了出来ます。


<sample program 094-07>

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

#define ROW 10
#define COL 10

enum {
    WALL,
    FLOOR,
    GOAL,
};

int main(void)
{
    int maze[ROW][COL] = {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
        { 0, 1, 1, 1, 0, 1, 1, 0, 2, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 0, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0 },
        { 0, 1, 0, 1, 0, 1, 0, 0, 1, 0 },
        { 0, 1, 0, 1, 1, 1, 1, 0, 1, 0 },
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
    };

    int i;
    int j;

    int px = 1;
    int py = 8;

    int key;

    for (;;) {

        if (maze[py][px] == GOAL) {
            printf("ゴール!\n");
            break;
        }

        if (_kbhit()) {

            key = _getch();

            switch (key) {
            case 'a':
                if (maze[py][px - 1] != WALL) {
                    px--;
                }
                break;
            case 's':
                if (maze[py + 1][px] != WALL) {
                    py++;
                }
                break;
            case 'd':
                if (maze[py][px + 1] != WALL) {
                    px++;
                }
                break;
            case 'w':
                if (maze[py - 1][px] != WALL) {
                    py--;
                }
                break;
            }
        }

        system("cls");

        for (i = 0; i < ROW; i++) {
            for (j = 0; j < COL; j++) {
                if (i == py && j == px) {
                    printf("P");
                }
                else{
                    switch (maze[i][j]) {
                    case WALL:
                        printf("■");
                        break;
                    case FLOOR:
                        printf("□");
                        break;
                    case GOAL:
                        printf("G");
                        break;
                    }
                }
            }
            printf("\n");
        }
    }

    return 0;
}

<実行結果>

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

これで基本形は出来上がりました。

次回はこれに色々付け加えてみましょう。


次へ

戻る

目次へ