C 言語でスネークゲームを作成する

CCBeginner
今すぐ練習

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

このプロジェクトでは、ncurses ライブラリを使用して C 言語で簡単なスネークゲームを作成する方法を学びます。この古典的なゲームでは、蛇を操作して食べ物を食べ、体を伸ばしながら、壁や自分自身との衝突を避ける必要があります。ゲームの機能は、初期化、ゲームループ、蛇の移動、衝突検出などいくつかの重要なコンポーネントに分けられます。このプロジェクトの最後まで進めると、ターミナル上で実行できる基本的なスネークゲームが完成します。

👀 プレビュー

Snake Game

🎯 タスク

このプロジェクトでは、以下のことを学びます。

  • 蛇の位置を更新し、ユーザー入力を処理するためのゲームループを実装する方法。
  • ゲームを初期化し、ゲームウィンドウを描画し、ゲームオーバーメッセージを表示する関数を作成する方法。
  • 壁、蛇自身の体、食べ物との衝突をチェックする衝突検出を実装する方法。
  • 蛇が食べ物を食べたときに体を伸ばすなどの機能を開発する方法。

🏆 達成目標

このプロジェクトを完了した後、以下のことができるようになります。

  • C 言語で ncurses ライブラリを使用して、ターミナルベースのゲームを作成する。
  • ゲームの状態を更新し、ユーザー入力を処理するなどのゲームロジックを実装する。
  • 蛇や食べ物などのゲームオブジェクトを表すデータ構造を作成し、操作する。
  • ゲームのルールを提供し、ゲームが終了すべきタイミングを判断するための衝突検出を実装する。

基本知識

テレタイプ機が広く使用されていた時代には、テレタイプ機はケーブルを介して中央コンピュータに接続された出力端末として機能していました。ユーザーは、端末画面上の出力を制御するために、端末プログラムに一連の特定の制御コマンドを送信する必要がありました。たとえば、画面上のカーソル位置を変更したり、画面上の特定の領域の内容を消去したり、画面をスクロールしたり、表示モードを切り替えたり、テキストに下線を引いたり、文字の外観、色、明るさなどを変更したりすることができます。これらの制御は、エスケープシーケンスと呼ばれる文字列を通じて実装されます。これらの連続したバイトは 0x1B 文字(ESC キーを押すことで入力されるエスケープ文字)で始まるため、エスケープシーケンスと呼ばれます。現在でも、端末エミュレーションプログラムにエスケープシーケンスを入力することで、当時のテレタイプ端末の出力効果をシミュレートすることができます。ターミナル(または端末エミュレーションプログラム)上で背景色付きのテキストを表示したい場合は、コマンドプロンプトに次のエスケープシーケンスを入力することができます。

echo "^[[0;31;40mIn Color"

ここで、^[ はいわゆるエスケープ文字です。(注: この場合、^[ は 1 文字です。^[ を順番に入力するのではなく、まず Ctrl+V を押してから ESC キーを押す必要があります。)上記のコマンドを実行すると、In Color の背景が赤色に変わっているはずです。それ以降、表示されるすべてのテキストはこの効果で出力されます。この効果を終了し、元の形式に戻したい場合は、次のコマンドを使用することができます。

echo "^[[0;37;40m"

これで、これらの文字(エスケープシーケンス)の目的がわかりましたか?(セミコロン間のパラメータを変更して、どのような結果が得られるか試してみてください。)想像とは異なる結果になるかもしれません。これは、ターミナル環境が異なるためで、異なるターミナルやオペレーティングシステムに依存します。(モノクロターミナルではカラー文字を表示することはできませんよね?)このような互換性の問題を回避し、異なるターミナルで一貫した出力を実現するために、UNIX の設計者たちは termcap と呼ばれるメカニズムを考案しました。termcap は実際には、エスケープシーケンスとともに配布されるファイルです。このファイルには、現在のターミナルが正しく実行できるすべてのエスケープシーケンスがリストされており、入力されたエスケープシーケンスの実行結果がこのファイルの仕様に準拠することを保証します。しかし、このメカニズムが考案されてから数年後、terminfo と呼ばれる別のメカニズムが徐々に termcap を置き換えました。それ以来、ユーザーはプログラミングする際に termcap の複雑なエスケープシーケンス仕様を調べる必要がなくなり、terminfo データベースにアクセスするだけで画面出力を制御することができるようになりました。

terminfo を使用する状況で、すべてのアプリケーションが terminfo データベースにアクセスして出力を制御する(制御文字を送信するなど)と仮定すると、すぐにこれらのコード呼び出しがプログラム全体を制御および管理しにくくするでしょう。これらの問題の発生により、CURSES が生まれました。CURSES という名前は、cursor optimization というダブルミーニングに由来しています。

CURSES ライブラリは、端末の低レベル制御コード(エスケープシーケンス)をカプセル化することで、ユーザーに柔軟で効率的な API(アプリケーションプログラミングインターフェイス)を提供します。これにより、ユーザーはカーソルを制御したり、ウィンドウを作成したり、前景色と背景色を変更したり、マウス操作を処理したりすることができます。これにより、ユーザーは文字端末上でアプリケーションを作成する際に、面倒な低レベルメカニズムを回避することができます。

NCURSESSystem V Release 4.0 (SVr4)CURSES のクローンです。これは、古いバージョンの CURSES と完全に互換性のある自由に設定可能なライブラリです。簡単に言えば、アプリケーションが端末画面の表示を直接制御できるライブラリです。以降で CURSES ライブラリと言う場合は、NCURSES ライブラリも指します。

NCURSES は、低レベルの端末機能をカプセル化するだけでなく、美しいインターフェイスを生成するためのかなり安定した作業フレームワークを提供します。これには、ウィンドウを作成する機能が含まれています。そして、その姉妹ライブラリである MenuPanelForm は、CURSES ベースライブラリの拡張です。これらのライブラリは一般的に CURSES とともに配布されます。複数のウィンドウ、メニュー、パネル、フォームを含むアプリケーションを構築することができます。ウィンドウは独立して管理することができ、たとえばスクロールしたり隠したりすることができます。メニューは、ユーザーがコマンドオプションを作成してコマンドを簡単に実行できるようにします。フォームは、ユーザーが簡単なデータ入力と表示用のウィンドウを作成できるようにします。パネルは、NCURSES のウィンドウ管理機能の拡張であり、ウィンドウを重ねたり積み重ねたりすることができます。

✨ 解答を確認して練習

定数の定義

まず、ターミナルを開き、次のコマンドを実行して ncurses ライブラリをインストールします。

sudo apt-get update
sudo apt-get install libncurses5-dev

~/project ディレクトリに移動し、プロジェクトファイル snake.c を作成します。

cd ~/project
touch snake.c

次に、C コードを記述する必要があります。最初のステップは、ヘッダーファイルをインクルードすることです。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <ncurses.h>

main() 関数を書く前に、いくつかの基本的なタスクを完了しましょう。ターミナル文字インターフェイスを使用しているため、ASCII 文字が不可欠です。したがって、いくつかの定数を定義する必要があります。

#define TBool            int
#define True             1
#define False            0
#define SHAPE_FOOD       '@'  // 食べ物
#define SHAPE_SNAKE      '#'  // 蛇の体
#define GAMEWIN_YLEN     15
#define GAMEWIN_XLEN     60
#define LOGWIN_YLEN      7
#define LOGWIN_XLEN      (GAMEWIN_XLEN)
#define LOGBUF_NUM       (LOGWIN_YLEN-2)
#define LOGBUF_LEN       (GAMEWIN_XLEN-2)
#define MAXLEVEL         12

#define GetSnakeTail(s)  ((s)->head->front)

WINDOW *logwin; // ログウィンドウを宣言
#define INITRUNLOG()     logwin = newlogw() // カスタム関数 newlogw() を呼び出してログウィンドウを作成
#define RUNLOG(str)      runlog(logwin, str) // ログウィンドウを実行してゲームのプロンプトを表示
#define DESTROYRUNLOG()  delwin(logwin)

int g_level; // プレイヤーのレベル、グローバル変数

また、いくつかの構造体と列挙型の定義も追加しました。

enum TDirection {
    DIR_UP,
    DIR_DOWN,
    DIR_LEFT,
    DIR_RIGHT
};

struct TFood {
    int y;
    int x;
};

struct TSnakeNode {
    int y;
    int x;
    struct TSnakeNode *front;
};

struct TSnake {
    int    length;
    struct TSnakeNode *head;
    enum   TDirection  dir;
};

ここで、作成する関数を宣言します。

int refreshgamew(WINDOW *win, struct TSnake *psnake);
void movesnake(struct TSnake *psnake);
int checksnake(struct TFood *pfood, struct TSnake *psnake);
void snakegrowup(struct TFood *pfood, struct TSnake *psnake);
void gameover(WINDOW *win, char *str);
struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x);
WINDOW* newgamew();
struct TSnake* initsnake();
void destroysnake(struct TSnake *psnake);
void drawsnakew(WINDOW *win, struct TSnake *psnake);
void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake);
TBool checkfood(struct TFood *pfood, struct TSnake *psnake);
WINDOW* newlogw();
void runlog(WINDOW *win, char *str);
void cleanline(WINDOW *win, int y, int x);
✨ 解答を確認して練習

main 関数

int main()
{
    initscr();  /* 初期化し、ncurses モードに入る */
    raw();      /* 行バッファリングを無効にし、結果を即座に表示する */
    noecho();   /* ターミナルに制御文字(Ctrl+C など)を表示しない */
    keypad(stdscr, TRUE);   /* ユーザーがターミナルでキーボードを使用できるようにする */
    curs_set(0);    /* カーソルの可視性を設定する。0 は不可視、1 は可視、2 は完全に可視 */
    refresh();      /* 仮想画面の内容をディスプレイに書き込み、更新する */

    g_level = 1;

    INITRUNLOG();

    RUNLOG("  Press 'q' or 'Q' to quit.");
    RUNLOG("  Press 'w/s/a/d' or 'W/S/A/D' to move the snake.");
    RUNLOG("Info:");

    WINDOW *gwin = newgamew(); /* ゲームウィンドウを作成する。newgamew というカスタム関数で実装 */
    struct TSnake *psnake = initsnake();
    drawsnakew(gwin, psnake);

    while (refreshgamew(gwin, psnake) >= 0)
        ;

    /* getch() は getchar() とは異なる */
    getch();

    destroysnake(psnake);
    delwin(gwin);    /* ゲームウィンドウをクリアし、ウィンドウデータ構造のメモリと情報を解放する */
    DESTROYRUNLOG(); /* 情報表示ウィンドウをクリアする */
    endwin();        /* ncurses モードを終了する */

    return 0;
}

keypad(stdscr, TRUE) では、stdscr はすべての操作が最初に書き込まれる仮想ウィンドウを指し、その後 refresh 関数を使用して stdscr の内容が画面に表示されます。

printw を使用するとき、実際には stdscr と呼ばれる仮想ウィンドウにデータが印刷され、直接画面に出力されるわけではありません。printw() 関数の目的は、仮想ディスプレイにいくつかの表示マーカーと関連するデータ構造を継続的に書き込み、これらのデータを stdscr のバッファに書き込むことです。したがって、このバッファ内のデータを表示するには、refresh() 関数を使用して curses システムにバッファの内容を画面に出力するよう指示する必要があります。このメカニズムにより、プログラマは仮想画面に継続的にデータを書き込み、refresh() が呼び出されたときに一度に行われたように見せることができます。refresh() 関数はウィンドウとデータの変更された部分のみをチェックするため、この柔軟な設計は効率的なフィードバックメカニズムを提供します。

✨ 解答を確認して練習

ウィンドウメカニズム

ウィンドウメカニズムは CURSES の核心概念です。前の例で見たように、すべての関数はデフォルトで「ウィンドウ」(stdscr)上で動作します。たとえ最も単純なグラフィカルユーザーインターフェイス(GUI)を設計する場合でも、ウィンドウを使用する必要があります。ウィンドウを使用する主な理由の 1 つは、画面を異なる部分に分割し、それらの中で同時に操作できることです。これにより効率を向上させることができます。もう 1 つの理由は、プログラムで常により良く、管理しやすい設計を目指すべきだということです。大規模で複雑なユーザーインターフェイスを設計する場合、これらの部分を事前に設計することで効率が向上します。

WINDOW* newlogw()
{
    /* パラメータは: 高さ、幅、ウィンドウの開始位置 (y,x) */
    WINDOW *win = newwin(LOGWIN_YLEN, LOGWIN_XLEN, GAMEWIN_YLEN + 2, 3);

    /* パラメータは: 既知のウィンドウポインタ、0 と 0 は文字のデフォルトの行と列の開始位置 */
    box(win, 0, 0);

    mvwprintw(win, 0, 2, " LOG ");
    wrefresh(win); // 指定されたウィンドウを更新する

    return win;
}

WINDOW* newlogw() 関数では、ウィンドウの作成が newwin() から始まることがわかります。ウィンドウを作成しましたが、まだ表示されていません。これは HTML の <div> 要素に似ています。<div> 要素にスタイルを追加しないと、ウェブページ上に何も表示されません。そのため、box 関数を使用して既知のウィンドウに枠を追加する必要があります。

mvwprintw は、指定されたウィンドウ内の指定された座標 (y,x) に指定された内容を印刷します。

✨ 解答を確認して練習

ゲーム情報の表示

RUNLOG は、カスタム関数 runlog を呼び出すマクロです。

void runlog(WINDOW *win, char *str)
{
    static char logbuf[LOGBUF_NUM][LOGBUF_LEN] = {0};
    static int  index = 0;

    strcpy(logbuf[index], str);

    int i = 0;

    /* #define LOGBUF_NUM  (LOGWIN_YLEN-2); LOGBUF_NUM=5 */
    for (; i < LOGBUF_NUM; ++i) {

        /* カスタム関数、cleanline */
        cleanline(win, i+1, 1);

        /* 各行に文字列を印刷する */
        mvwprintw(win, i+1, 1, logbuf[(index+i) % LOGBUF_NUM]);
        wrefresh(win);
    }

    index = (index + LOGBUF_NUM - 1) % LOGBUF_NUM;
}

runlog は、既知のウィンドウと表示する情報をパラメータとして受け取ります。strstrcpy 関数を使用して二次元配列 (logbuf) に格納されます。次に、カスタム関数 cleanline が呼び出され、次に印刷される行がクリアされます。mvwprintw 関数は情報を印刷するために使用されます。

/* ウィンドウ win の座標 (x,y) をクリアする */
void cleanline(WINDOW *win, int y, int x)
{
    char EMPTYLINE[LOGBUF_LEN] = {0}; // LOGBUF_LEN=57

    /* 配列の 0 - 56 番目の位置を空白文字に設定する */
    memset(EMPTYLINE, ' ', LOGBUF_LEN-1);

    /* ウィンドウ win 内の位置 (y,x) にカーソルを移動し、文字列 EMPTYLINE を印刷する */
    mvwprintw(win, y, x, EMPTYLINE);

    /* 指定されたウィンドウに内容を表示する */
    wrefresh(win);
}
✨ 解答を確認して練習

蛇のキューの初期化

ここでは、蛇のデータ構造を定義し、それを初期化してゲームウィンドウに描画する関数を作成します。

struct TSnake* initsnake()
{
    struct TSnake *psnake = (struct TSnake *)malloc(sizeof(struct TSnake));

    psnake->dir    = DIR_LEFT;
    psnake->length = 4; // 蛇の長さを 4 に初期化する

    newsnakenode (
        &newsnakenode (
            &newsnakenode (
                &newsnakenode( &psnake->head, 4, 50 )->front, 4, 53
            )->front, 4, 52
        )->front, 4, 51
    )->front = psnake->head;

    return psnake;
}

struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x)
{
    *ppsnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));
    (*ppsnode)->y = y;
    (*ppsnode)->x = x;
    (*ppsnode)->front = NULL;

    return *ppsnode;
}

これら 2 つの関数の目的は、蛇ゲームにおける蛇のデータ構造を初期化することです。これには、蛇の初期方向と長さの設定、および初期の体のノードを持つ蛇の作成が含まれます。関数 newsnakenode は個々のノードを作成するために使用され、その後、これらのノードはネストされた関数呼び出しを通じてつなぎ合わされ、初期の蛇を形成します。

✨ 解答を確認して練習

ゲームウィンドウに蛇を表示する

void drawsnakew(WINDOW *win, struct TSnake *psnake)
{
    static int taily = 0;
    static int tailx = 0;
    if (taily!= 0 && tailx!= 0) {
        mvwaddch(win, taily, tailx, ' ');
    }

    /* #define GetSnakeTail(s)  ((s)->head->front) */
    struct TSnakeNode *psnode = GetSnakeTail(psnake);

    int i = 0;
    for (; i < psnake->length; ++i) {
        mvwaddch(win, psnode->y, psnode->x, SHAPE_SNAKE);
        psnode = psnode->front;
    }

    taily = GetSnakeTail(psnake)->y;
    tailx = GetSnakeTail(psnake)->x;

    wrefresh(win);
}

各ノードの (y,x) 座標を表示するには、マクロ定義 GetSnakeTail を使用して座標を取得できます。残りの手順は、ループを使用して mvwaddch でそれらを表示することです。mvwaddch は、指定されたウィンドウ (win) 内の指定された位置 (taily,tailx) にカーソルを移動し、その後文字を出力するために使用されます。

実際に画面に表示するには、wrefresh(win) を使用する必要があります。

✨ 解答を確認して練習

ゲームの核心部分を記述する

while (refreshgamew(gwin, psnake) >= 0); の後のセミコロンに注目してください。これにより、蛇の移動や長さの変更、境界検出などの問題を処理するループが確立されます。

while ループは refreshgamew 関数を呼び出します。

int refreshgamew(WINDOW *win, struct TSnake *psnake)
{
    static TBool ffood = False;
    struct TFood pfood;
    /* ゲーム開始時または食べ物が食べられたとき、ffood=False となり、drawfoodw が実行されて食べ物が再描画される */
    if (!ffood) {
        drawfoodw(win, &pfood, psnake);
        ffood = True;
    }
    int key = -1;

    fd_set set;
    FD_ZERO(&set);
    FD_SET(0, &set);

    struct timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec= (6 - (int)(g_level/3)) * 100*1000;

    // 次に、select カーネル API を使用します。これは一連のキーボードとマウスの入力を監視します。`key=getch()` と `switch` を使用して入力タイプを判断します。2 つ目の `switch` は、レベルと蛇の現在の移動速度を表示するために使用されます。
    if (select(1, &set, NULL, NULL, &timeout) < 0)
        return -1;

    if (FD_ISSET(0, &set)) {
        while ((key = getch()) == -1);

        switch (key) {
        case 'w':
        case 'W':
            (psnake->dir == DIR_DOWN)? : (psnake->dir = DIR_UP);
            break;

        case 's':
        case 'S':
            (psnake->dir == DIR_UP)? : (psnake->dir = DIR_DOWN);
            break;

        case 'a':
        case 'A':
            (psnake->dir == DIR_RIGHT)? : (psnake->dir = DIR_LEFT);
            break;

        case 'd':
        case 'D':
            (psnake->dir == DIR_LEFT)? : (psnake->dir = DIR_RIGHT);
            break;

        case 'q':
        case 'Q':
            RUNLOG("Quit Game!");
            gameover(win, "Quit Game!");
            return -1;

        default:
            break;
        }
    }
    movesnake(psnake);
    drawsnakew(win, psnake);
    switch (checksnake(&pfood, psnake)) {
    case 0:
        break;

    // 食べ物が食べられたので、ffood を 0 に設定して食べ物を再描画する。
    case 1:
        ffood = False;
        if (++g_level > MAXLEVEL) {
            RUNLOG("Win!!!");
            gameover(win, "Win!!!");
            return -1;
        }
        mvwprintw(win, GAMEWIN_YLEN-1, 2, " Level: %d ", g_level);
        mvwprintw(win, GAMEWIN_YLEN-1, 30, " Speed: %d ", (int)(g_level/3));
        wrefresh(win);
        RUNLOG("Level UP!");
        snakegrowup(&pfood, psnake);
        break;

    default:
        RUNLOG("Game over!");
        gameover(win, "Game over!");
        return -1;
    }

    return 1;
}
✨ 解答を確認して練習

貪欲蛇の移動

貪欲蛇の移動は movesnake 関数を使用して実装されています。残りの部分は似ているため、case DIR_UP のセグメントを分析します。

/* 構造体 TSnake は、頭と尾がつながった逆リンクリストです。
 * 例: [a]<-[b]<-[c]<-[d]    a が頭
 *          |              ^     蛇が移動するとき、頭だけが d を指し、
 *          `--------------'     d の (y,x) が頭が移動する位置に変更されます。 */
void movesnake(struct TSnake *psnake)
{
    int hy = psnake->head->y;
    int hx = psnake->head->x;

    psnake->head = GetSnakeTail(psnake);

    switch (psnake->dir) {
    case DIR_UP:
        psnake->head->y = hy - 1;
        psnake->head->x = hx;
        break;

    case DIR_DOWN:
        psnake->head->y = hy + 1;
        psnake->head->x = hx;
        break;

    case DIR_LEFT:
        psnake->head->y = hy;
        psnake->head->x = hx - 1;
        break;

    case DIR_RIGHT:
        psnake->head->y = hy;
        psnake->head->x = hx + 1;
        break;

    default:
        break;
    }
}

座標原点は左上隅にあり、(y,x) の値は下方向と右方向に増加するため、座標系は x 軸を中心に 180 度反転しています。したがって、蛇を上方向に移動させるには 1 を減算します。このため、プレイヤーが w/W キーを押すと、蛇は上方向に移動します。

✨ 解答を確認して練習

蛇の体を伸ばす

蛇の体の長さを増やすには、snakegrowup 関数を実装することで達成できます。これは比較的簡単で、単に新しいノードをキューに追加するだけです。

void snakegrowup(struct TFood *pfood, struct TSnake *psnake)
{
    // 新しい蛇のノード (struct TSnakeNode) のメモリを割り当て、ポインタ pnode を新しいノードに割り当てます。この新しいノードが蛇の新しい頭になります。
    struct TSnakeNode *pnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));

    switch (psnake->dir) {
    case DIR_UP:
        pnode->y = psnake->head->y - 1;
        pnode->x = psnake->head->x;
        break;

    case DIR_DOWN:
        pnode->y = psnake->head->y + 1;
        pnode->x = psnake->head->x;
        break;

    case DIR_LEFT:
        pnode->y = psnake->head->y;
        pnode->x = psnake->head->x - 1;
        break;

    case DIR_RIGHT:
        pnode->y = psnake->head->y;
        pnode->x = psnake->head->x + 1;
        break;

    default:
        break;
    }

    // 新しいノード (pnode) の front ポインタを現在の蛇の尾に設定し、新しい頭と尾の間に接続を確立します。
    pnode->front = GetSnakeTail(psnake);

    // 現在の蛇の頭の front ポインタを新しいノードに設定し、新しいノードが新しい頭になるようにします。
    psnake->head->front = pnode;
    psnake->head = pnode;

    // 蛇の長さを増やし、蛇の体が 1 ユニット伸びたことを示します。
    ++psnake->length;
}

switch 文は、蛇の現在の移動方向 (psnake->dir) に基づいて新しい頭ノードの座標を決定します。方向に応じて、新しいノードの座標を現在の蛇の頭 (psnake->head) から上、下、左、または右に 1 セル移動するように更新します。

このコードの目的は、スネークゲームにおいて蛇の体の長さを増やすことです。蛇の現在の移動方向に基づいて新しい頭ノードを作成し、この新しい頭ノードを蛇の先頭に挿入して、蛇の体を伸ばします。これは、スネークゲームで蛇が食べ物を食べた後に蛇の体の長さを増やすための核心的なロジックです。

✨ 解答を確認して練習

食べ物の生成位置

drawfoodw 関数は、ゲームウィンドウ (win) 内に食べ物を描画するために使用されます。

void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake)
{
    do {
        pfood->y = random() % (GAMEWIN_YLEN - 2) + 1;
        pfood->x = random() % (GAMEWIN_XLEN - 2) + 1;
    } while (False == checkfood(pfood, psnake));
    checkfood(pfood, psnake);

    mvwaddch(win, pfood->y, pfood->x, SHAPE_FOOD);
    wrefresh(win);
}
  • random() 関数は、ランダムな座標値を生成するために使用され、これにより食べ物がゲームウィンドウ内のランダムな位置に出現します。
  • do-while ループを使用して、生成された食べ物の位置が蛇の体のノードと重なっているかどうかをチェックします。重なっている場合は、新しい食べ物の位置を生成し、蛇の体と重ならない位置が見つかるまで続けます。
  • mvwaddch 関数は、ゲームウィンドウ内の指定された位置に食べ物の形状を描画するために使用されます。

checkfood 関数は、食べ物が蛇の上に出現しないようにチェックするために使用されます。

TBool checkfood(struct TFood *pfood, struct TSnake *psnake)
{
    struct TSnakeNode *pnode = GetSnakeTail(psnake);

    int i = 0;
    for (; i < psnake->length; ++i, pnode = pnode->front)
        if (pfood->y == pnode->y && pfood->x == pnode->x)
            return False;

    return True;
}
  • まず、蛇の尾ノードを取得します。次に、ループを使用して蛇のすべての体のノードを反復処理し、食べ物の座標がいずれかのノードの座標と一致するかどうかをチェックします。
  • 食べ物の座標がいずれかの蛇のノードの座標と一致する場合、食べ物の位置が蛇の体のノードと重なっていることを意味し、関数は False を返します。
  • 重なりが見つからない場合、関数は True を返し、食べ物の位置が有効であることを示します。
✨ 解答を確認して練習

境界検出

checksnake は、蛇がゲームの境界に衝突したかどうかをチェックするために使用されます。これには、ゲームの上、下、左、右の境界の検出が含まれます。また、蛇自身の体との衝突もチェックされ、これらのいずれの状況でもゲームが終了します。

int checksnake(struct TFood *pfood, struct TSnake *psnake)
{

	/* 蛇の頭の座標がゲームの上、下、左、右の境界に衝突したかどうかをチェックする */
    if ( psnake->head->y <= 0 || psnake->head->y >= GAMEWIN_YLEN
      || psnake->head->x <= 0 || psnake->head->x >= GAMEWIN_XLEN)
    {
        return -1;
    }

    struct TSnakeNode *pnode = GetSnakeTail(psnake);
    int i = 0;

	/* 蛇の頭が自身の体のどの部分とも衝突しないことをチェックする */
    for (; i < psnake->length - 1; ++i, pnode = pnode->front)
        if (psnake->head->y == pnode->y && psnake->head->x == pnode->x)
            return -1;

  	/* もちろん、食べ物との衝突は許可されている */
    if (psnake->head->y == pfood->y && psnake->head->x == pfood->x)
        return 1;

    return 0; // 衝突は発生していない
}
✨ 解答を確認して練習

ゲームオーバー

gameover 関数は、スネークゲームが終了したときに指定されたウィンドウ (win) にゲームオーバーメッセージを表示するために使用されます。

void gameover(WINDOW *win, char *str)
{
    mvwprintw(win, (int)(GAMEWIN_YLEN/2), (GAMEWIN_XLEN/2 - strlen(str)/2), str);
    mvwprintw(win, (int)(GAMEWIN_YLEN/2 + 1), 20, "Press any key to quit...");
    wrefresh(win);
}
  • mvwprintw 関数を使用して、ゲームオーバーメッセージのテキスト str をウィンドウの中央に表示します。これにより、ゲームオーバーメッセージがウィンドウの中央に表示されます。
  • 次に、ウィンドウの中央の次の行に "Press any key to quit..." を表示し、プレイヤーに任意のキーを押してゲームを終了するように促します。
  • 最後に、wrefresh 関数を使用してウィンドウを更新し、ゲームオーバーメッセージと終了プロンプトが画面に正しく描画されるようにします。

メモリリークを避けるために、destroysnake 関数を使用して、スネークゲームで占有されているメモリ(蛇のノードや蛇の構造体自体を含む)を解放します。これは、ゲームが終了または再起動するときの一般的なクリーンアップ手順です。

void destroysnake(struct TSnake *psnake)
{
    struct TSnakeNode *psnode = GetSnakeTail(psnake);
    struct TSnakeNode *ptmp   = NULL;

    int i = 0;
    for (; i < psnake->length; ++i) {
        ptmp   = psnode;
        psnode = psnode->front;
        free(ptmp);
    }

    free(psnake);
    psnake = NULL;
}

関数内では、まず psnodeptmp という 2 つのポインタを宣言し、蛇のノードのリンクリストを反復処理するために使用します。

蛇の長さ(ノードの数)と同じ回数だけ反復するループに入ります。各反復では、以下の操作を実行します。

  • ptmp を現在のノード psnode を指すように設定し、後で現在のノードのメモリを解放できるようにします。
  • psnode を次のノード(front が指すノード)に移動します。

ループが終了すると、すべての蛇のノードが解放されます。

最後に、関数は蛇の構造体自体のメモリを解放し (free(psnake) )、蛇の構造体を指すポインタ psnakeNULL に設定して、解放されたメモリが再利用されないようにします。

✨ 解答を確認して練習

コンパイルと実行

コンパイルコマンドは通常と少し異なります。gcc に -l オプションを追加して、ncurses ライブラリを含める必要があります。

cd ~/project
gcc -o snake snake.c -l ncurses
./snake
Snake Game
✨ 解答を確認して練習

まとめ

あなたは ncurses ライブラリを使用して、C 言語で簡単なスネークゲームを作成することに成功しました。このゲームには、ゲームウィンドウ、蛇の移動、食べ物の生成、および衝突検出が含まれています。上記の手順に従うことで、ターミナル上でゲームを実行し、楽しむことができます。ゲームを楽しんでください!