C言語:迷路を一筆書きで抜けるゲーム

C言語初級

C言語を使ったコンソール画面で動くゲームです。
Pythonでつくる ゲーム開発 入門講座」という書籍で紹介されていたPythonのtkinter(GUIライブラリ)を使ったプログラムをC言語に移植したものです。

2次元配列を使った勉強にもいいのでは?と思いアップしてみました。

ゲームのイメージ

元のイメージ(Pythonで作ったGUI版。今回のC言語版はこれではないですよ!)

ルールは簡単で、迷路をキーボードの上下左右で動き回り、床を全て塗ればゲームクリアです。ただし、元のGUI画面をC言語を使ったCUI画面に置き換えているので見た目は以下の通りとなります。(わたしはこうしたものが結構好きです)

C言語版のイメージ(Windowsコマンドプロンプト上で実行)

C言語版のルール

 人 ・・・ プレイヤー
 ■ ・・・ 壁
 × ・・・ 塗った床

 矢印キーの上下左右でプレイヤーの移動。
 プレイヤーが動けなくなってしまったら ESC キーで最初の状態に戻ります。
 床を全て塗ったらゲームクリアです。

動作環境:Windows
コンパイラ:Borland C++ Compiler 5.5

動作上の注意点

動作環境をWindowsとした理由は2点あります。

1つ目は、Windows独自のシステムコマンドを使っているからです。

system("cls");

clsはコマンドプロンプト上の命令なのでLinux上では動作しません。

2つ目は、今回プログラムでキー入力にkbhit()とgetch()関数を利用していますが、これらの関数が定義されているヘッダーファイルconio.hは、DebianなどのLinuxディストリビューションに最初から入っているgccコンパイラには存在しません。したがってLinux環境ではコンパイルエラーとなります。

コンパイルエラー:C4996: ‘getch’: The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _getch.がでる場合

Visual Studioの開発環境でコンパイル時に

C4996: 'getch': The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _getch.

などの警告がでる場合はソースコード82行目のkbhit()_kbhit()に、83行目のgetch()_getch()に置き換えてみてください。それぞれ_アンダーバーが関数名の頭につきます。(マイクロソフトの開発環境ではkbhit()とgetch()関数は非推奨の関数なので_kbhit()と_getch()をそれぞれ利用しないと警告がでる仕様になっているようです。どちらも#include <conio.h>で定義されている関数です)

ソースコード全体

/*
meiro_hitofude.c
迷路を一筆書きで抜けるゲーム
Created by dianxnao.com on 2020/08/17.
*/
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#define GYO 10 /* 迷路の行数 */
#define RETU 10 /* 迷路の列数 */
/* 迷路データ */
int meiro[GYO][RETU] = {
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 0, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 1, 0, 1, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 1, 0, 0, 1},
{1, 0, 0, 0, 1, 1, 1, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 0, 1},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
};
int px, py; /* プレイヤーのxy座標 */
int goal_count; /* 塗りつぶすべき床の数 */
int count; /* 塗りつぶした床の数 */
/* 最初の状態に戻る */
void play_start(void)
{
int x, y;
count = 0;
px = 1;
py = 1;
for(y=0; y<GYO; y++)
for(x=0; x<RETU; x++)
if(meiro[y][x] == 2) meiro[y][x] = 0; /* 塗りつぶし部分をもとに戻す */
}
/* 塗りつぶすべき床の数をカウントする */
void goal_count_check(void)
{
int x, y;
goal_count = 0;
for(y=0; y<GYO; y++)
for(x=0; x<RETU; x++)
if(meiro[y][x] == 0) goal_count++; /* 移動可能な床の数をカウント */
}
/* 迷路を描く */
void draw_meiro(void)
{
int x, y;
for(y=0; y<GYO; y++){
for(x=0; x<RETU; x++){
if(x == px && y == py){ /* プレイヤーの位置のとき */
meiro[y][x] = 2; /* 塗りつぶし済みにする */
count ++; /* 塗りつぶした数をカウント */
printf("人"); /* プレイヤー */
}
else if(meiro[y][x] == 0) /* 移動可能な床 */
printf(" "); /* ← 注)全角スペース */
else if(meiro[y][x] == 1) /* 壁 */
printf("■");
else if(meiro[y][x] == 2) /* 塗った床 */
printf("×");
}
printf("\n");
}
printf("move: ←↑→↓ restart: ESC\n"); /* 操作説明 */
}
/* キー入力判定 */
void key_input(void)
{
int key;
while (1) { /* キーが押されるまで待つ */
if ( kbhit() ){
key = getch(); /* 入力されたキー番号 */
break ;
}
}
if(key == 72 && meiro[py-1][px] == 0) /* ↑キー */
py --; /* 上に移動 */
else if(key == 80 && meiro[py+1][px] == 0) /* ↓キー */
py ++; /* 下に移動 */
else if(key == 75 && meiro[py][px-1] == 0) /* ←キー */
px --; /* 左に移動 */
else if(key == 77 && meiro[py][px+1] == 0) /* →キー */
px ++; /* 右に移動 */
else if(key == 27) /* ESCキー */
play_start(); /* 最初の状態に戻る */
else /* 上記以外のキーの場合は */
key_input(); /* 再度キー入力受付 */
}
int main(void)
{
px = 1; /* プレイヤーのx座標 */
py = 1; /* プレイヤーのy座標 */
count = 0; /* 塗りつぶした床の数 */
goal_count_check(); /* 塗りつぶすべき床の数をカウントする */
/* ゲームループ */
while(1){
system("cls"); /* コンソール画面をクリア */
draw_meiro(); /* 迷路を表示 */
if(count == goal_count){ /* 床を全て塗りつぶしたかのチェック */
printf("全て塗りました!\n");
break;
}
key_input(); /* キー入力受付 */
}
return 0;
}

プログラム解説

迷路データは単純な2次元配列で宣言します。1が壁で、0が床(動き回れる場所)です。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 迷路データ */
int meiro[GYO][RETU] = {
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 1, 0, 0, 1},
    {1, 1, 1, 1, 1, 0, 1, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 1, 0, 0, 1},
    {1, 0, 0, 0, 1, 1, 1, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 0, 1},
    {1, 1, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
};

迷路の初期状態では、0と1の数値しか入っていません。
これを以下のような関数を作って画面に表示させます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 迷路を描く */
void draw_meiro(void)
{
    int x, y;
    for(y=0; y<GYO; y++){
        for(x=0; x<RETU; x++){
            if(meiro[y][x] == 0)    /* 移動可能な床 */
                printf(" ");    /* ← 注)全角スペース */
            else if(meiro[y][x] == 1)    /* 壁 */
                printf("■");
            else if(meiro[y][x] == 2)    /* 塗った床 */
                printf("×");
        }
        printf("\n");
    }
}

実行するとこんな風に表示されます。(ただし上記のプログラムではプレイヤーが表示されないなどの問題がありますので、最終的にはもっと長くなります)

プレイヤーは配列の左上方(meiroの添え字でいうとmeiro[1][1]からスタートします。

1
2
px = 1;
py = 1;

ゲーム開始前に塗るべき床の数をカウントしておきます。これは変数 goal_count に代入しています。

1
2
3
4
5
6
7
8
9
/* 塗りつぶすべき床の数をカウントする */
void goal_count_check(void)
{
    int x, y;
    goal_count = 0;
    for(y=0; y<GYO; y++)
        for(x=0; x<RETU; x++)
            if(meiro[y][x] == 0) goal_count++;        /* 移動可能な床の数をカウント */
}

ゲーム中はゲームループと呼ばれる無限ループです。
無限ループを抜けたときがゲームクリアとなります。if(count == goal_count){ … } の部分。

1
2
3
4
5
6
7
8
9
10
11
12
/* ゲームループ */
while(1){
    system("cls");    /* コンソール画面をクリア */
    draw_meiro();    /* 迷路を表示 */
 
    if(count == goal_count){    /* 床を全て塗りつぶしたかのチェック */
        printf("全て塗りました!\n");
        break;
    }
 
    key_input();        /* キー入力受付 */
}

コンソール画面でスクロールしてしまうとどうしても見た目が損なわれます。ですから迷路を表示する前にコンソール画面のクリアをします。(Windowsでのみ動作)

1
system("cls");    /* コンソール画面をクリア */

迷路表示は関数化していますが、2重のfor文を使って行ごとに表示させます。内側ループが横方向(行ごとのすべての列)の表示。外側ループが縦方向(行を上から下に)の表示です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 迷路を描く */
void draw_meiro(void)
{
    int x, y;
    for(y=0; y<GYO; y++){
        for(x=0; x<RETU; x++){
            if(x == px && y == py){    /* プレイヤーの位置のとき */
                meiro[y][x] = 2;    /* 塗りつぶし済みにする */
                count ++;        /* 塗りつぶした数をカウント */
                printf("人");        /* プレイヤー */
            }
            else if(meiro[y][x] == 0)    /* 移動可能な床 */
                printf(" ");    /* ← 注)全角スペース */
            else if(meiro[y][x] == 1)    /* 壁 */
                printf("■");
            else if(meiro[y][x] == 2)    /* 塗った床 */
                printf("×");
        }
        printf("\n");
    }
    printf("move: ←↑→↓ restart: ESC\n");    /* 操作説明 */
}

ポイントは、if文の最初にプレイヤーの表示をしている部分です。

迷路を塗りつぶし済みにして

1
meiro[y][x] = 2;    /* 塗りつぶし済みにする */

塗りつぶした床の数をカウントし

1
count ++;        /* 塗りつぶした数をカウント */

プレイヤーを表示させます。

1
printf("人");        /* プレイヤー */

もし、このプレイヤーの「人」の表示箇所を塗りつぶしの床表示の後ろに持ってきてしまうと「人」の位置に「×」が表示されてしまうのでif文の記述順序は大事です。

迷路表示の後、ユーザからのキー入力を受け付けます。
scanfをつかってしまうといちいちEnterキーを押すことになり面倒です。今回は押した時にリアルタイムなキー入力判定が可能なkbhit関数を利用しています。(kbhit関数の利用にはconio.hのインクルードが必要です)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* キー入力判定 */
void key_input(void)
{
    int key;
    while (1) {    /* キーが押されるまで待つ */
        if ( kbhit() ){
            key = getch();    /* 入力されたキー番号 */
            break ;
        }
    }
    if(key == 72 && meiro[py-1][px] == 0)            /* ↑キー */
        py --;    /* 上に移動 */
    else if(key == 80 && meiro[py+1][px] == 0)        /* ↓キー */
        py ++;    /* 下に移動 */
    else if(key == 75 && meiro[py][px-1] == 0)        /* ←キー */
        px --;    /* 左に移動 */
    else if(key == 77 && meiro[py][px+1] == 0)        /* →キー */
        px ++;    /* 右に移動 */
    else if(key == 27)                                /* ESCキー */
        play_start();    /* 最初の状態に戻る */
    else                                            /* 上記以外のキーの場合は */
        key_input();                                /* 再度キー入力受付 */
}

kbhit関数は、キー入力されたときに真となる関数です。実際にどのキーが押されたかを知るにはgetch関数を使います。

1
2
3
4
if ( kbhit() ){
    key = getch();    /* 入力されたキー番号 */
    break ;
}

キー番号はint型で返されるのでキー番号を知りたい場合は、以下のようにすれば表示できます。(ただし、その際は system(“cls”); 命令は消してください)

1
2
3
4
5
if ( kbhit() ){
    key = getch();    /* 入力されたキー番号 */
    printf("key = %d\n", key);
    break ;
}

コンソールプログラムでもアイデア次第でGUIっぽいゲームが出来るいい例だと思います。

コメント

  1. 匿名 より:

    82行目と83行目が
    C4996 ‘getch’: The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant name: _getch. See online help for details.
    ‘kbhit’:(以下同文)とエラーを吐いて実行できないのですが原因は何でしょうか。

    • dennou より:

      匿名さんこんにちは。管理人です。
      お使いの開発環境はVisual Studioでしょうか?
      たぶんVisual StudioのCで使うマイクロソフトのコンパイラではgetchとkbhitは推奨されていない関数となっているかと思います。

      エラー内容から推測するにgetch()とhbhit()の部分は、それぞれ_getch()_kbhit()に置き換えてコンパイルするとすんなり通るかと思います。
      Visual StudioのCの環境では#include に両方とも定義されいてる関数です。

      参考
      _kbhit – MSDN
      https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/kbhit?view=msvc-170

  2. 匿名 より:

    ビジュアルスタジオです。SDLチェックをいいえにしたら正常に動きました。

    • dennou より:

      それは良かったです。
      getch()とkbhit()はマイクロソフトの環境で非推奨のようで、セキュリティを強化した_getch()と_kbhit()を使うように推奨されているみたいです。(記事に追記しておきました。ありがとうございます)

タイトルとURLをコピーしました