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):標準出力デバイスのハンドルを取得。SetConsoleCursorPosition:COORD構造体に座標を設定し、表示カーソルを移動。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などのスレッドセーフ関数への移行を検討してください。