C言語とWin32 APIで構築するコンソールスネークゲーム

開発の基盤となる技術要素

本プロジェクトでは、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);
    }
}

タグ: C言語 Win32 API コンソールアプリケーション ゲーム開発 連結リスト

5月16日 10:27 投稿