開発の基盤となる技術要素
本プロジェクトでは、C言語の以下の機能を駆使して実装を行います。
- 構造体、列挙型、ポインタの活用
- 動的メモリ割り当てによる連結リストの構築
- プリプロセッサディレクティブによるマクロ定義
- Win32 APIを用いたコンソール画面の制御
コンソール画面の制御
Windowsのコマンドプロンプト上で動作するアプリケーションを作成するため、system関数を利用してコンソールウィンドウの設定を行います。mode conコマンドでウィンドウサイズを、titleコマンドでタイトルバーの文字列をそれぞれ変更可能です。
座標管理とCOORD構造体
Windows APIに定義されているCOORD構造体は、コンソール画面上の文字位置をX・Y座標で管理します。例えば、COORD pos = {10, 15};と宣言することで、座標(10, 15)を指し示すことができます。
ワイド文字の出力
コンソール上に全角文字や特殊記号を描画するためには、ロケールの設定が必要です。setlocale(LC_ALL, "")を呼び出した上で、wprintf関数を使用することで、壁や蛇の体、エサなどのワイド文字を正しく出力できます。
ゲームの実装コード
メインモジュール (main.c)
#include "game_context.h"
#include <locale.h>
void RunApplication() {
int choice = 0;
srand((unsigned int)time(NULL));
do {
GameContext ctx = {0};
InitializeGame(&ctx);
ProcessGameLoop(&ctx);
FinalizeGame(&ctx);
MoveCursorTo(20, 15);
printf("もう一度プレイしますか?(Y/N): ");
choice = getchar();
getchar(); // 改行をクリア
} while (choice == 'Y' || choice == 'y');
MoveCursorTo(0, 27);
}
int main() {
setlocale(LC_ALL, "");
RunApplication();
return 0;
}
ヘッダファイル (game_context.h)
#pragma once
#include <windows.h>
#include <time.h>
#include <stdio.h>
#define IS_KEY_DOWN(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
enum Heading {
NORTH = 1,
SOUTH,
WEST,
EAST
};
enum GameState {
PLAYING,
WALL_COLLISION,
SELF_COLLISION,
USER_QUIT
};
#define BORDER L'□'
#define SEGMENT L'●'
#define TARGET L'★'
#define START_X 24
#define START_Y 5
typedef struct Segment {
int coordX;
int coordY;
struct Segment* link;
} Segment, *SegmentPtr;
typedef struct GameContext {
SegmentPtr head;
SegmentPtr targetNode;
enum Heading direction;
enum GameState state;
int score;
int targetScore;
int stepDelay;
} GameContext, *GameContextPtr;
void InitializeGame(GameContextPtr ctx);
void ProcessGameLoop(GameContextPtr ctx);
void FinalizeGame(GameContextPtr ctx);
void MoveCursorTo(short x, short y);
void ShowWelcome();
void DisplayInstructions();
void RenderBorder();
void SetupSerpent(GameContextPtr ctx);
void SpawnTarget(GameContextPtr ctx);
void HaltGame();
int CheckTargetCollision(SegmentPtr next, GameContextPtr ctx);
void ConsumeTarget(SegmentPtr next, GameContextPtr ctx);
void AdvanceWithoutTarget(SegmentPtr next, GameContextPtr ctx);
int CheckWallCollision(GameContextPtr ctx);
int CheckSelfCollision(GameContextPtr ctx);
void MoveSerpent(GameContextPtr ctx);
ゲームロジック (game_logic.c)
#include "game_context.h"
void MoveCursorTo(short x, short y) {
COORD pos = {x, y};
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hOut, pos);
}
void ShowWelcome() {
MoveCursorTo(40, 15);
printf("スネークゲームへようこそ");
MoveCursorTo(40, 25);
system("pause");
system("cls");
MoveCursorTo(25, 12);
printf("矢印キーで操作、F3で加速、F4で減速\n");
MoveCursorTo(25, 13);
printf("速度が速いほど高得点を獲得できます。\n");
MoveCursorTo(40, 25);
system("pause");
system("cls");
}
void RenderBorder() {
int i;
MoveCursorTo(0, 0);
for (i = 0; i < 58; i += 2) {
wprintf(L"%c", BORDER);
}
MoveCursorTo(0, 26);
for (i = 0; i < 58; i += 2) {
wprintf(L"%c", BORDER);
}
for (i = 1; i < 26; i++) {
MoveCursorTo(0, i);
wprintf(L"%c", BORDER);
}
for (i = 1; i < 26; i++) {
MoveCursorTo(56, i);
wprintf(L"%c", BORDER);
}
}
void SetupSerpent(GameContextPtr ctx) {
SegmentPtr curr = NULL;
int i;
for (i = 0; i < 5; i++) {
curr = (SegmentPtr)malloc(sizeof(Segment));
if (!curr) {
perror("SetupSerpent::malloc");
return;
}
curr->link = NULL;
curr->coordX = START_X + i * 2;
curr->coordY = START_Y;
if (ctx->head == NULL) {
ctx->head = curr;
} else {
curr->link = ctx->head;
ctx->head = curr;
}
}
curr = ctx->head;
while (curr) {
MoveCursorTo(curr->coordX, curr->coordY);
wprintf(L"%c", SEGMENT);
curr = curr->link;
}
ctx->stepDelay = 200;
ctx->score = 0;
ctx->state = PLAYING;
ctx->direction = EAST;
ctx->targetScore = 10;
}
void SpawnTarget(GameContextPtr ctx) {
int x, y;
SegmentPtr curr;
SegmentPtr newTarget;
retry:
do {
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
curr = ctx->head;
while (curr) {
if (curr->coordX == x && curr->coordY == y) {
goto retry;
}
curr = curr->link;
}
newTarget = (SegmentPtr)malloc(sizeof(Segment));
if (!newTarget) {
perror("SpawnTarget::malloc");
return;
}
newTarget->coordX = x;
newTarget->coordY = y;
MoveCursorTo(newTarget->coordX, newTarget->coordY);
wprintf(L"%c", TARGET);
ctx->targetNode = newTarget;
}
void DisplayInstructions() {
MoveCursorTo(64, 15);
printf("壁や自分の体に衝突しないでください\n");
MoveCursorTo(64, 16);
printf("矢印キーで移動を操作");
MoveCursorTo(64, 17);
printf("F3: 加速 / F4: 減速\n");
MoveCursorTo(64, 18);
printf("ESC: 終了 / SPACE: 一時停止");
}
void HaltGame() {
while (1) {
Sleep(300);
if (IS_KEY_DOWN(VK_SPACE)) {
break;
}
}
}
int CheckTargetCollision(SegmentPtr next, GameContextPtr ctx) {
return (next->coordX == ctx->targetNode->coordX) && (next->coordY == ctx->targetNode->coordY);
}
void ConsumeTarget(SegmentPtr next, GameContextPtr ctx) {
next->link = ctx->head;
ctx->head = next;
SegmentPtr curr = ctx->head;
while (curr) {
MoveCursorTo(curr->coordX, curr->coordY);
wprintf(L"%c", SEGMENT);
curr = curr->link;
}
ctx->score += ctx->targetScore;
free(ctx->targetNode);
SpawnTarget(ctx);
}
void AdvanceWithoutTarget(SegmentPtr next, GameContextPtr ctx) {
next->link = ctx->head;
ctx->head = next;
SegmentPtr curr = ctx->head;
while (curr->link->link) {
MoveCursorTo(curr->coordX, curr->coordY);
wprintf(L"%c", SEGMENT);
curr = curr->link;
}
MoveCursorTo(curr->link->coordX, curr->link->coordY);
printf(" ");
free(curr->link);
curr->link = NULL;
}
int CheckWallCollision(GameContextPtr ctx) {
if (ctx->head->coordX == 0 || ctx->head->coordX == 56 ||
ctx->head->coordY == 0 || ctx->head->coordY == 26) {
ctx->state = WALL_COLLISION;
return 1;
}
return 0;
}
int CheckSelfCollision(GameContextPtr ctx) {
SegmentPtr curr = ctx->head->link;
while (curr) {
if (ctx->head->coordX == curr->coordX && ctx->head->coordY == curr->coordY) {
ctx->state = SELF_COLLISION;
return 1;
}
curr = curr->link;
}
return 0;
}
void MoveSerpent(GameContextPtr ctx) {
SegmentPtr nextStep = (SegmentPtr)malloc(sizeof(Segment));
if (!nextStep) {
perror("MoveSerpent::malloc");
return;
}
switch (ctx->direction) {
case NORTH:
nextStep->coordX = ctx->head->coordX;
nextStep->coordY = ctx->head->coordY - 1;
break;
case SOUTH:
nextStep->coordX = ctx->head->coordX;
nextStep->coordY = ctx->head->coordY + 1;
break;
case WEST:
nextStep->coordX = ctx->head->coordX - 2;
nextStep->coordY = ctx->head->coordY;
break;
case EAST:
nextStep->coordX = ctx->head->coordX + 2;
nextStep->coordY = ctx->head->coordY;
break;
}
if (CheckTargetCollision(nextStep, ctx)) {
ConsumeTarget(nextStep, ctx);
} else {
AdvanceWithoutTarget(nextStep, ctx);
}
CheckWallCollision(ctx);
CheckSelfCollision(ctx);
}
void InitializeGame(GameContextPtr ctx) {
system("mode con cols=100 lines=30");
system("title Snake Game");
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursorInfo;
GetConsoleCursorInfo(hOut, &cursorInfo);
cursorInfo.bVisible = false;
SetConsoleCursorInfo(hOut, &cursorInfo);
ShowWelcome();
RenderBorder();
SetupSerpent(ctx);
SpawnTarget(ctx);
}
void ProcessGameLoop(GameContextPtr ctx) {
DisplayInstructions();
do {
MoveCursorTo(64, 10);
printf("スコア: %d ", ctx->score);
printf("ターゲット得点: %d", ctx->targetScore);
if (IS_KEY_DOWN(VK_UP) && ctx->direction != SOUTH) {
ctx->direction = NORTH;
} else if (IS_KEY_DOWN(VK_DOWN) && ctx->direction != NORTH) {
ctx->direction = SOUTH;
} else if (IS_KEY_DOWN(VK_LEFT) && ctx->direction != EAST) {
ctx->direction = WEST;
} else if (IS_KEY_DOWN(VK_RIGHT) && ctx->direction != WEST) {
ctx->direction = EAST;
} else if (IS_KEY_DOWN(VK_SPACE)) {
HaltGame();
} else if (IS_KEY_DOWN(VK_ESCAPE)) {
ctx->state = USER_QUIT;
break;
} else if (IS_KEY_DOWN(VK_F3)) {
if (ctx->stepDelay >= 50) {
ctx->stepDelay -= 30;
ctx->targetScore += 2;
}
} else if (IS_KEY_DOWN(VK_F4)) {
if (ctx->stepDelay < 350) {
ctx->stepDelay += 30;
ctx->targetScore -= 2;
if (ctx->stepDelay == 350) {
ctx->targetScore = 1;
}
}
}
Sleep(ctx->stepDelay);
MoveSerpent(ctx);
} while (ctx->state == PLAYING);
}
void FinalizeGame(GameContextPtr ctx) {
SegmentPtr curr = ctx->head;
MoveCursorTo(24, 12);
switch (ctx->state) {
case USER_QUIT:
printf("ゲームを終了しました\n");
break;
case SELF_COLLISION:
printf("自分の体に衝突しました!\n");
break;
case WALL_COLLISION:
printf("壁に衝突しました!\n");
break;
default:
break;
}
while (curr) {
SegmentPtr del = curr;
curr = curr->link;
free(del);
}
}