C言語を用いたWindowsコンソール版スネークゲームの実装手順

Windowsプラットフォーム上で標準ライブラリとWin32 APIを活用し、コンソール画面に表示されるスネークゲームを作成する方法を解説します。本手法はGUIフレームワークに依存しないため、リソース消費が小さく、アルゴリズムの理解やシステムコールの学習に適しています。

データ構造と状態管理

蛇の本体は単方向連結リストで管理します。各ノードは現在地座標と次ノードへのポインタを持ちます。頭部を先頭に固定し、移動時に新規ノードを先頭に挿入、尾側ノードを解放することで効率的な可変長リストを実現します。ゲームの状態遷移は列挙型で定義し、プレイ中・ESC終了・壁衝突・自己衝突の4つのステートを明確に分けます。

typedef enum { DIR_N = 1, DIR_S, DIR_W, DIR_E } Direction;
typedef enum { STATE_PLAY, STATE_EXIT, STATE_WALL_CRASH, STATE_SELF_CRASH } GameState;

typedef struct Coordinate { int col; int row; } Coordinate;
typedef struct Segment {
    Coordinate pos;
    struct Segment* next;
} Segment;

typedef struct Engine {
    Segment* head;
    Segment* food_node;
    Direction dir;
    GameState status;
    int score;
    int delay_ms;
    int best_score;
    const char* record_file;
} Engine;

コンソール描画と入出力制御

コンソールウィンドウのカスタマイズには以下のWin32 APIを使用します。カーソルの隠蔽、座標指定移動、フォアグラウンド/バックグラウンド色の変更が核心機能です。

  • GetStdHandle(STD_OUTPUT_HANDLE):標準出力デバイスのハンドルを取得。
  • SetConsoleCursorPositionCOORD構造体に座標を設定し、表示カーソルを移動。
  • SetConsoleTextAttribute:テキスト属性フラグを適用して色を変更。変更前属性をグローバル変数で保持し、終了時に復元する仕様にする。
  • GetAsyncKeyState:非同期キー状態を取得。戻り値の最下位ビットを確認することで、「その瞬間に入力されたか」を判定するマクロを定義可能。

ゲームループと動作原理

メイン処理は「入力検出→次位置計算→条件判定→描画更新」のサイクルで構成されます。

移動処理:現在の進行方向に基づき、頭部の仮座標を算出します。その座標に食品が存在すれば頭部追加のみを行いスコア加算、存在しなければ頭部追加後に尾部ノードを解放して長さを維持します。

衝突判定:壁の範囲外坐标に到達した場合または、仮座標が既存の体ノードと重複する場合、それぞれ該当ステートに変更しゲームループを終了させます。

難易度調整:F3/F4キー押下を検知し、delay_msの数値を加減算することでSleep関数の待機時間を変更します。これにより実行周期が短くなり、ゲーム速度が増加する仕組みです。

実装コード

以下は構造化および変数名を刷新した完全なソースコードです。リスト操作の最適化とモジュール分割を行っています。

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
#include <time.h>
#include <locale.h>

#define MAP_WIDTH  56
#define MAP_HEIGHT 26
#define INIT_X     20
#define INIT_Y     5

#define TILE_WALL  L'█'
#define TILE_BODY  L'▓'
#define TILE_FOOD  L'●'

#define KEY_CHECK(k) ((GetAsyncKeyState(k) & 0x1) != 0)

static WORD saved_attr;

/* データ構造 */
typedef enum { H_N = 1, H_S, H_W, H_E } Heading;
typedef enum { RUN = 0, EXIT_REQ, WALL_DEATH, SELF_DEATH } Event;
typedef struct Point { int x; int y; } Point;
typedef struct Cell { Point p; struct Cell* nxt; } Cell;

typedef struct GameCtrl {
    Cell* player;
    Cell* snack;
    Heading direction;
    Event phase;
    int points;
    int cycle_time;
    int record;
    const char* db_path;
} GameCtrl;

/* APIヘルパー */
static void set_cursor(short cx, short cy) {
    COORD c = { cx, cy };
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), c);
}

static void apply_color(WORD attr) {
    HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_SCREEN_BUFFER_INFO info;
    GetConsoleScreenBufferInfo(h, &info);
    saved_attr = info.wAttributes;
    SetConsoleTextAttribute(h, attr);
}

static void restore_color(void) {
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), saved_attr);
}

static void hide_cursor(void) {
    HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO ci;
    GetConsoleCursorInfo(h, &ci);
    ci.bVisible = FALSE;
    SetConsoleCursorInfo(h, &ci);
}

/* ゲーム初期化・クリーンアップ */
static Cell* make_node(int x, int y, Cell* next) {
    Cell* n = malloc(sizeof(Cell));
    if (!n) perror("malloc fail"), exit(1);
    n->p.x = x; n->p.y = y; n->nxt = next;
    return n;
}

static void init_game(GameCtrl* g) {
    system("mode con cols=90 lines=28");
    system("title SnakeConsole_v2");
    hide_cursor();
    apply_color(FOREGROUND_GREEN | BACKGROUND_BLUE);
    set_cursor(25, 10); wprintf(L"%lc Welcome Console Snake \n", TILE_FOOD);
    set_cursor(25, 12); puts("Arrow Keys: Move | Space: Pause | F3/F4: Speed | ESC: Exit");
    Sleep(1500); system("cls");

    /* 壁描画 */
    apply_color(FOREGROUND_RED | FOREGROUND_GREEN);
    for (int i = 0; i <= MAP_WIDTH; i += 2) {
        set_cursor(i, 0); wprintf(L"%lc", TILE_WALL);
        set_cursor(i, MAP_HEIGHT); wprintf(L"%lc", TILE_WALL);
    }
    for (int j = 1; j < MAP_HEIGHT; j++) {
        set_cursor(0, j); wprintf(L"%lc", TILE_WALL);
        set_cursor(MAP_WIDTH, j); wprintf(L"%lc", TILE_WALL);
    }
    restore_color();

    /* 蛇初期化 (5節) */
    g->player = NULL;
    for (int i = 0; i < 5; i++)
        g->player = make_node(INIT_X + i * 2, INIT_Y, g->player);

    /* UI描画 */
    apply_color(FOREGROUND_GREEN);
    for (Cell* c = g->player; c; c = c->nxt) {
        set_cursor(c->p.x, c->p.y); wprintf(L"%lc", TILE_BODY);
    }
    restore_color();

    g->phase = RUN; g->direction = H_E; g->points = 0; g->cycle_time = 180; g->snack = NULL;
    g->record = 0; g->db_path = "snake_best.dat";
    
    FILE* f = fopen(g->db_path, "r");
    if (f) { fscanf(f, "%d", &g->record); fclose(f); }
    spawn_food(g);
}

/* 食事生成ロジック */
static void spawn_food(GameCtrl* g) {
    int fx, fy;
    bool overlap;
    do {
        overlap = false;
        fx = (rand() % (MAP_WIDTH - 2)) + 2;
        fy = (rand() % (MAP_HEIGHT - 2)) + 1;
        if (fx % 2 != 0) continue;
        for (Cell* c = g->player; c; c = c->nxt)
            if (c->p.x == fx && c->p.y == fy) { overlap = true; break; }
    } while (overlap);

    g->snack = make_node(fx, fy, NULL);
    apply_color(FOREGROUND_RED);
    set_cursor(fx, fy); wprintf(L"%lc", TILE_FOOD);
    restore_color();
}

/* 移動と状態更新 */
static void move_snake(GameCtrl* g) {
    int nx = g->player->p.x;
    int ny = g->player->p.y;

    switch (g->direction) {
        case H_N: ny--; break;
        case H_S: ny++; break;
        case H_W: nx -= 2; break;
        case H_E: nx += 2; break;
    }

    Cell* new_head = make_node(nx, ny, g->player);
    g->player = new_head;

    /* 食事チェック */
    bool ate = false;
    if (g->snack && g->snack->p.x == nx && g->snack->p.y == ny) {
        ate = true;
        g->points += 10;
        free(g->snack); g->snack = NULL;
    }

    /* 描画 */
    apply_color(FOREGROUND_GREEN);
    Cell* cur = g->player;
    while (cur->nxt->nxt) { /* 末尾以外を描画 */
        set_cursor(cur->p.x, cur->p.y); wprintf(L"%lc", TILE_BODY);
        cur = cur->nxt;
    }
    restore_color();

    if (ate) spawn_food(g);
    else {
        /* 尾部消去 */
        set_cursor(cur->nxt->p.x, cur->nxt->p.y);
        printf("  ");
        free(cur->nxt);
        cur->nxt = NULL;
    }

    /* 死亡判定 */
    if (nx == 0 || nx == MAP_WIDTH || ny == 0 || ny == MAP_HEIGHT) g->phase = WALL_DEATH;
    if (new_head->nxt) {
        Cell* temp = new_head->nxt;
        while (temp) {
            if (temp->p.x == nx && temp->p.y == ny) g->phase = SELF_DEATH;
            temp = temp->nxt;
        }
    }
}

/* メインゲームループ */
static void run_game(GameCtrl* g) {
    set_cursor(60, 4); printf("Score:%-5d Delay:%-4d Best:%-5d", g->points, g->cycle_time, g->record);
    
    bool paused = false;
    while (g->phase == RUN) {
        if (KEY_CHECK(VK_SPACE)) {
            paused = !paused;
            set_cursor(60, 8); printf(paused ? "[PAUSED]" : "       ");
            while (paused && g->phase == RUN) Sleep(50);
            continue;
        }

        if (KEY_CHECK(VK_ESCAPE)) { g->phase = EXIT_REQ; break; }
        if (KEY_CHECK(VK_F3) && g->cycle_time > 60) { g->cycle_time -= 20; continue; }
        if (KEY_CHECK(VK_F4) && g->cycle_time < 350) { g->cycle_time += 20; continue; }

        /* 反転防止付き方向変更 */
        if (KEY_CHECK(VK_UP) && g->direction != H_S) g->direction = H_N;
        else if (KEY_CHECK(VK_DOWN) && g->direction != H_N) g->direction = H_S;
        else if (KEY_CHECK(VK_LEFT) && g->direction != H_E) g->direction = H_W;
        else if (KEY_CHECK(VK_RIGHT) && g->direction != H_W) g->direction = H_E;

        move_snake(g);
        
        /* スコア更新 */
        set_cursor(60, 4); printf("Score:%-5d Delay:%-4d Best:%-5d", g->points, g->cycle_time, g->record);
        if (g->points > g->record) {
            g->record = g->points;
            FILE* f = fopen(g->db_path, "w");
            if (f) { fprintf(f, "%d", g->record); fclose(f); }
        }
        Sleep(g->cycle_time);
    }
}

/* 終了処理 */
static void cleanup(GameCtrl* g) {
    set_cursor(20, 10);
    switch (g->phase) {
        case EXIT_REQ:   puts("[System] Game closed manually."); break;
        case WALL_DEATH: puts("[Result] Collided with boundary."); break;
        case SELF_DEATH: puts("[Result] Collided with own body."); break;
    }
    set_cursor(20, 12); printf("Final Score: %d | Record: %d\n", g->points, g->record);

    /* メモリ開放 */
    while (g->player) {
        Cell* tmp = g->player;
        g->player = g->player->nxt;
        free(tmp);
    }
    free(g->snack);
    restore_color();
}

int main(void) {
    setlocale(LC_ALL, "");
    srand((unsigned)time(NULL));
    
    GameCtrl ctx = {0};
    bool retry = true;
    while (retry) {
        init_game(&ctx);
        run_game(&ctx);
        cleanup(&ctx);
        set_cursor(20, 14); printf("Restart? (y/n): ");
        char ch = getchar();
        while (getchar() != '\n');
        retry = (ch == 'y' || ch == 'Y');
        if (retry) system("cls");
    }
    return 0;
}

環境構築と実行に関する技術的留意点

本コードでは日本語および特殊記号を含むワイド文字列(L"")を使用しているため、コンパイル時のエンコーディング設定が重要です。MSVCデフォルトでは問題なく動作しますが、MinGWや古いIDE(例:旧版Dev-C++)を利用する場合は、プロジェクト設定で「UTF-8 without BOM」または「Shift-JIS」への変換を手動で行う必要があります。setlocale(LC_ALL, "")呼び出しはOSのロケール設定に従ってランタイム-wide文字処理を有効化する必須処理です。また、Win32コンソールアプリケーションはconhost.exeを通じて描画するため、マルチスレッド化を検討する場合はCRTロックやwprintf_sなどのスレッドセーフ関数への移行を検討してください。

タグ: C言語 Windows-API コンソール描画 リンクリスト ゲーム設計

5月13日 21:03 投稿