Cとraylibで作るFPS:最初のウィンドウから敵への射撃まで

Cとraylibで作るFPS:最初のウィンドウから敵への射撃まで

この記事では、Cでの一人称視点シューティング(FPS)の作り方を説明します。ウィンドウ、3Dグラフィック、入力にはオープンソースのエンジンライブラリであるraylibを使います。マップ、壁との衝突、射撃、HUDといったゲームのロジックはすべてCで書きます。各部分は一つずつ実行できる手順になっており、構造図とコピーして使えるコードファイルを示します。

必要なもの

Windows 10または11のパソコン、C++(MSVC)ツールセットとWindows SDKを含むVisual Studio 2022 Build Tools、そしてGitHub Releasesからダウンロードしたraylibが必要です。raylibはzlibライセンスで公開されており、商用利用も可能です。注意:この記事のコードはraylib 5と6のドキュメントに基づいて書いたもので、記事を執筆した環境ではゲーム全体のコンパイルとテストを行っていません。記載の結果は、手順を正しく行った場合に見えるはずのものです。raylibの標準フォントはラテン文字のみ表示するため、画面の文字は英語を使っています。

図解

毎フレームのゲームループ:入力を読み、更新し、描画して、フレームを表示する
毎フレームのゲームループ:入力を読み、更新し、描画して、フレームを表示する

1. ツールとraylibのインストール

MSVCでCをコンパイルし、エンジンライブラリとしてraylibを使います。

1. MicrosoftのサイトからVisual Studio 2022 Build Toolsをダウンロードし、「C++によるデスクトップ開発」を入れる

2. スタートメニューからDeveloper Command Prompt for VS 2022を開く

3. github.com/raysan5/raylib/releasesにアクセスし、名前がwin64_msvc16.zipで終わるWindows用ファイルをダウンロードする

4. zipを解凍し、includeフォルダとlibフォルダをraylibという名前のプロジェクトフォルダにコピーする

**見えるはずの結果:** raylib/includeフォルダにraylib.hが、raylib/libフォルダにraylib.libがあります。

2. プロジェクトの構成

ゲームのコードはsrcフォルダにまとめ、raylibのライブラリは別に置きます。

**コード: project-tree.txt**

fps-c-raylib/
├── raylib/                 ไลบรารี raylib ที่ดาวน์โหลดจาก GitHub Releases
│   ├── include/raylib.h
│   └── lib/raylib.lib
├── src/
│   └── main.c              โค้ดเกมทั้งหมด
├── build.bat               คำสั่งคอมไพล์สำหรับ MSVC
└── fps-c-raylib.exe        ไฟล์ที่ได้หลังคอมไพล์

3. ウィンドウを開いて文字を描く

raylibのゲームはどれも同じループに従います。ウィンドウを開き、閉じられるまでループし、1フレームずつ描画します。

1. srcフォルダを作り、その中にmain.cを作成する

2. 下のコードをmain.cに貼り付ける

**コード: 01_window.c**

// ไฟล์: src/main.c  (ขั้นแรก เปิดหน้าต่างและวาดข้อความ)
#include "raylib.h"

int main(void)
{
    InitWindow(960, 540, "FPS ด้วย C และ raylib");
    SetTargetFPS(60);

    while (!WindowShouldClose())
    {
        BeginDrawing();
        ClearBackground(RAYWHITE);
        DrawText("Hello FPS", 40, 40, 32, DARKGRAY);
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

**見えるはずの結果:** 960x540のウィンドウが開き、背景は白で、左上に Hello FPS と表示されます。

4. マップを3Dの壁として描く

マップは1と0の配列として保存します。1は壁、0は空きスペースです。配列を順に調べ、壁のマスごとにボックスを描きます。

**コード: 02_map_render.c**

// ไฟล์: src/main.c  (ขั้นที่สอง แผนที่เป็นตารางตัวเลข แล้ววาดกำแพงเป็นกล่อง 3 มิติ)
#include "raylib.h"

#define MAP_W 8
#define MAP_H 8
#define CELL 4.0f // ขนาดของหนึ่งช่องในโลก 3 มิติ

// 1 = กำแพง, 0 = พื้นที่ว่าง
static const int map[MAP_H][MAP_W] = {
    {1, 1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 1, 1, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 1, 0, 1},
    {1, 0, 0, 1, 0, 1, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1},
    {1, 1, 1, 1, 1, 1, 1, 1},
};

static void DrawMap(void)
{
    for (int row = 0; row < MAP_H; row++)
    {
        for (int col = 0; col < MAP_W; col++)
        {
            if (map[row][col] == 1)
            {
                // ช่อง (col, row) ครอบคลุมพื้นที่ [col*CELL, (col+1)*CELL] ดังนั้นศูนย์กลางอยู่ที่ +CELL/2
                Vector3 pos = { (col + 0.5f) * CELL, CELL / 2.0f, (row + 0.5f) * CELL };
                DrawCube(pos, CELL, CELL, CELL, DARKGRAY);
                DrawCubeWires(pos, CELL, CELL, CELL, BLACK);
            }
        }
    }
}

int main(void)
{
    InitWindow(960, 540, "แผนที่ FPS");
    SetTargetFPS(60);

    Camera3D camera = { 0 };
    camera.position = (Vector3){ 6.0f, 2.0f, 6.0f };
    camera.target = (Vector3){ 10.0f, 1.0f, 10.0f };
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.fovy = 60.0f;
    camera.projection = CAMERA_PERSPECTIVE;

    while (!WindowShouldClose())
    {
        BeginDrawing();
        ClearBackground(SKYBLUE);

        BeginMode3D(camera);
        DrawPlane((Vector3){ 16.0f, 0.0f, 16.0f }, (Vector2){ 32.0f, 32.0f }, LIGHTGRAY);
        DrawMap();
        EndMode3D();

        DrawFPS(10, 10);
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

**見えるはずの結果:** 濃いグレーの壁が3Dのマップを作り、左上にFPSカウンターが表示されます。

5. WASDとマウスで移動と視点の回転

raylibのUpdateCameraはCAMERA_FIRST_PERSONモードに対応しており、WASDで移動し、マウスで視点を変えられます。角度の計算を自分で書く必要はありません。

**コード: 03_movement.c**

// ไฟล์: src/main.c  (ขั้นที่สาม การเดินและหมุนกล้องด้วยคีย์บอร์ดและเมาส์)
#include "raylib.h"

int main(void)
{
    InitWindow(960, 540, "เดินในแผนที่");
    SetTargetFPS(60);
    DisableCursor(); // ซ่อนเคอร์เซอร์เพื่อให้หมุนกล้องด้วยเมาส์ได้

    Camera3D camera = { 0 };
    camera.position = (Vector3){ 6.0f, 2.0f, 6.0f };
    camera.target = (Vector3){ 10.0f, 1.0f, 10.0f };
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.fovy = 60.0f;
    camera.projection = CAMERA_PERSPECTIVE;

    while (!WindowShouldClose())
    {
        // WASD เดิน และเมาส์หมุนมอง ทำงานตามโหมด CAMERA_FIRST_PERSON
        UpdateCamera(&camera, CAMERA_FIRST_PERSON);

        BeginDrawing();
        ClearBackground(SKYBLUE);
        BeginMode3D(camera);
        DrawPlane((Vector3){ 16.0f, 0.0f, 16.0f }, (Vector2){ 32.0f, 32.0f }, LIGHTGRAY);
        EndMode3D();
        DrawText("WASD move, mouse look", 10, 40, 20, DARKGRAY);
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

**見えるはずの結果:** W A S Dでカメラが動き、マウスで視点が変わります。カーソルは非表示になります。

6. 壁との衝突判定

新しい位置を受け入れる前に、プレイヤーの周囲の四隅を調べます。壁に当たったら、ぶつかった軸だけを元に戻すので、壁に沿って滑ることができます。見ている方向がずれないよう、注視点も同じ量だけずらします。

**コード: 04_collision.c**

// ไฟล์: src/main.c  (ขั้นที่สี่ ตรวจชนกำแพงก่อนยอมรับการขยับ)
#include "raylib.h"
#include "raymath.h"

#define MAP_W 8
#define MAP_H 8
#define CELL 4.0f
#define PLAYER_RADIUS 0.4f

static const int map[MAP_H][MAP_W] = {
    {1, 1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 1, 1, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 1, 0, 1},
    {1, 0, 0, 1, 0, 1, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1},
    {1, 1, 1, 1, 1, 1, 1, 1},
};

// ตรวจว่าจุด (x, z) ในโลกอยู่ในช่องกำแพงหรือไม่
static bool IsWall(float x, float z)
{
    int col = (int)(x / CELL);
    int row = (int)(z / CELL);
    if (col < 0 || col >= MAP_W || row < 0 || row >= MAP_H) return true;
    return map[row][col] == 1;
}

// ตรวจทั้งสี่มุมรอบตัวผู้เล่น ถ้ามีมุมใดชนกำแพงให้ถือว่าไปไม่ได้
static bool CanMoveTo(Vector3 p)
{
    float r = PLAYER_RADIUS;
    return !IsWall(p.x - r, p.z - r) && !IsWall(p.x + r, p.z - r) &&
           !IsWall(p.x - r, p.z + r) && !IsWall(p.x + r, p.z + r);
}

int main(void)
{
    InitWindow(960, 540, "ชนกำแพง");
    SetTargetFPS(60);
    DisableCursor();

    Camera3D camera = { 0 };
    camera.position = (Vector3){ 6.0f, 2.0f, 6.0f };
    camera.target = (Vector3){ 10.0f, 1.0f, 10.0f };
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.fovy = 60.0f;
    camera.projection = CAMERA_PERSPECTIVE;

    while (!WindowShouldClose())
    {
        // เก็บตำแหน่งก่อนขยับ แล้วให้ UpdateCamera คำนวณตำแหน่งใหม่
        Vector3 before = camera.position;
        UpdateCamera(&camera, CAMERA_FIRST_PERSON);

        // ถ้าตำแหน่งใหม่ชนกำแพง ให้ย้อนกลับไปเฉพาะแกนที่ชน เพื่อให้เลื่อนไปตามกำแพงได้
        Vector3 wanted = camera.position;
        Vector3 fixed = wanted;
        if (!CanMoveTo((Vector3){ fixed.x, fixed.y, before.z })) fixed.x = before.x;
        if (!CanMoveTo(fixed)) fixed.z = before.z;

        // ย้ายจุดมองตามไปด้วย เพื่อให้ทิศทางการมองไม่เพี้ยน
        Vector3 shift = Vector3Subtract(fixed, wanted);
        camera.position = fixed;
        camera.target = Vector3Add(camera.target, shift);

        BeginDrawing();
        ClearBackground(SKYBLUE);
        BeginMode3D(camera);
        DrawPlane((Vector3){ 16.0f, 0.0f, 16.0f }, (Vector2){ 32.0f, 32.0f }, LIGHTGRAY);
        for (int row = 0; row < MAP_H; row++)
            for (int col = 0; col < MAP_W; col++)
                if (map[row][col] == 1)
                {
                    Vector3 pos = { (col + 0.5f) * CELL, CELL / 2.0f, (row + 0.5f) * CELL };
                    DrawCube(pos, CELL, CELL, CELL, DARKGRAY);
                }
        EndMode3D();
        DrawText("Walls block you", 10, 40, 20, DARKGRAY);
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

**見えるはずの結果:** 壁に突っ込むと止まり、壁をすり抜けません。ただし壁に沿って滑ることはできます。

光線による射撃:画面中央からの光線が敵のボックスに当たり、最も近い命中が対象になる
光線による射撃:画面中央からの光線が敵のボックスに当たり、最も近い命中が対象になる

7. 画面中央からの光線で射撃する

GetMouseRayは画面上の点から光線を作ります。この例では画面中央を使い、GetRayCollisionBoxで光線が敵のボックスに当たったかを調べます。

**コード: 05_shoot.c**

// ไฟล์: src/main.c  (ขั้นที่ห้า ยิงด้วยรังสีจากเมาส์ไปชนกล่องของศัตรู)
#include "raylib.h"

// กล่องขอบเขตของศัตรูหนึ่งตัว
static BoundingBox enemyBox = {
    { 14.0f, 0.0f, 14.0f }, // มุมต่ำสุด
    { 15.5f, 2.5f, 15.5f }, // มุมสูงสุด
};
static bool enemyAlive = true;

int main(void)
{
    InitWindow(960, 540, "ยิงศัตรู");
    SetTargetFPS(60);
    DisableCursor();

    Camera3D camera = { 0 };
    camera.position = (Vector3){ 6.0f, 2.0f, 6.0f };
    camera.target = (Vector3){ 14.0f, 1.0f, 14.0f };
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.fovy = 60.0f;
    camera.projection = CAMERA_PERSPECTIVE;

    while (!WindowShouldClose())
    {
        UpdateCamera(&camera, CAMERA_FIRST_PERSON);

        // คลิกซ้ายแล้วยิงรังสีจากกลางจอไปข้างหน้า
        if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && enemyAlive)
        {
            Ray ray = GetMouseRay((Vector2){ GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f }, camera);
            RayCollision hit = GetRayCollisionBox(ray, enemyBox);
            if (hit.hit)
            {
                enemyAlive = false; // ยิงโดนแล้วศัตรูหายไป
            }
        }

        BeginDrawing();
        ClearBackground(SKYBLUE);
        BeginMode3D(camera);
        DrawPlane((Vector3){ 16.0f, 0.0f, 16.0f }, (Vector2){ 32.0f, 32.0f }, LIGHTGRAY);
        if (enemyAlive)
        {
            Vector3 center = { 14.75f, 1.25f, 14.75f };
            DrawCubeV(center, (Vector3){ 1.5f, 2.5f, 1.5f }, RED);
        }
        EndMode3D();

        DrawLine(GetScreenWidth() / 2 - 10, GetScreenHeight() / 2, GetScreenWidth() / 2 + 10, GetScreenHeight() / 2, BLACK);
        DrawLine(GetScreenWidth() / 2, GetScreenHeight() / 2 - 10, GetScreenWidth() / 2, GetScreenHeight() / 2 + 10, BLACK);
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

**見えるはずの結果:** 赤いボックスを狙って左クリックすると消えます。別の場所を撃っても何も起こりません。

8. 弾薬と体力を表示するHUD

HUDはEndMode3Dを呼んだ後に、2Dの文字とバーを描きます。弾薬は1発撃つごとに減ります。

**コード: 06_hud.c**

// ไฟล์: src/main.c  (ขั้นที่หก หน้าจอข้อมูล HUD แสดงกระสุนและพลังชีวิต)
#include <stdio.h>
#include "raylib.h"

typedef struct
{
    int ammo;
    int health;
} Player;

static void DrawHud(const Player *p)
{
    char text[64];

    // แถบพลังชีวิตมุมซ้ายล่าง
    DrawRectangle(20, GetScreenHeight() - 40, 200, 20, DARKGRAY);
    DrawRectangle(20, GetScreenHeight() - 40, (int)(200 * p->health / 100.0f), 20, RED);

    // จำนวนกระสุนมุมขวาล่าง
    snprintf(text, sizeof(text), "Ammo %d", p->ammo);
    DrawText(text, GetScreenWidth() - 180, GetScreenHeight() - 40, 24, BLACK);
}

int main(void)
{
    InitWindow(960, 540, "HUD");
    SetTargetFPS(60);

    Player player = { .ammo = 12, .health = 100 };

    while (!WindowShouldClose())
    {
        if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && player.ammo > 0)
        {
            player.ammo--; // ยิงหนึ่งนัดจะลดกระสุน
        }

        BeginDrawing();
        ClearBackground(SKYBLUE);
        DrawHud(&player);
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

**見えるはずの結果:** 右下に Ammo 12 が表示され、左下の赤いバーは満タンの体力を示します。クリックするたびに弾薬が1ずつ減ります。

9. すべてをまとめる

このファイルは、マップ、壁との衝突、体力を持つ敵2体、最も近い命中への射撃、HUDをまとめたものです。

**コード: 07_full_main.c**

// ไฟล์: src/main.c  (รวมทุกขั้นตอนเป็นไฟล์เดียว)
#include <stdio.h>
#include "raylib.h"
#include "raymath.h"

#define MAP_W 8
#define MAP_H 8
#define CELL 4.0f
#define PLAYER_RADIUS 0.4f
#define ENEMY_COUNT 2

static const int map[MAP_H][MAP_W] = {
    {1, 1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 1, 1, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 1, 0, 1},
    {1, 0, 0, 1, 0, 1, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1},
    {1, 1, 1, 1, 1, 1, 1, 1},
};

typedef struct
{
    BoundingBox box;
    int health;
} Enemy;

typedef struct
{
    int ammo;
    int health;
} Player;

static bool IsWall(float x, float z)
{
    int col = (int)(x / CELL);
    int row = (int)(z / CELL);
    if (col < 0 || col >= MAP_W || row < 0 || row >= MAP_H) return true;
    return map[row][col] == 1;
}

static bool CanMoveTo(Vector3 p)
{
    float r = PLAYER_RADIUS;
    return !IsWall(p.x - r, p.z - r) && !IsWall(p.x + r, p.z - r) &&
           !IsWall(p.x - r, p.z + r) && !IsWall(p.x + r, p.z + r);
}

static void ApplyCollision(Camera3D *camera, Vector3 before)
{
    Vector3 wanted = camera->position;
    Vector3 fixed = wanted;
    if (!CanMoveTo((Vector3){ fixed.x, fixed.y, before.z })) fixed.x = before.x;
    if (!CanMoveTo(fixed)) fixed.z = before.z;
    Vector3 shift = Vector3Subtract(fixed, wanted);
    camera->position = fixed;
    camera->target = Vector3Add(camera->target, shift);
}

static void DrawMap(void)
{
    for (int row = 0; row < MAP_H; row++)
    {
        for (int col = 0; col < MAP_W; col++)
        {
            if (map[row][col] == 1)
            {
                Vector3 pos = { (col + 0.5f) * CELL, CELL / 2.0f, (row + 0.5f) * CELL };
                DrawCube(pos, CELL, CELL, CELL, DARKGRAY);
                DrawCubeWires(pos, CELL, CELL, CELL, BLACK);
            }
        }
    }
}

int main(void)
{
    InitWindow(960, 540, "FPS ด้วย C และ raylib");
    SetTargetFPS(60);
    DisableCursor();

    Camera3D camera = { 0 };
    camera.position = (Vector3){ 6.0f, 2.0f, 6.0f };
    camera.target = (Vector3){ 10.0f, 1.0f, 10.0f };
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.fovy = 60.0f;
    camera.projection = CAMERA_PERSPECTIVE;

    Player player = { .ammo = 12, .health = 100 };
    Enemy enemies[ENEMY_COUNT] = {
        { { { 14.0f, 0.0f, 14.0f }, { 15.5f, 2.5f, 15.5f } }, 50 },
        { { { 18.0f, 0.0f, 4.0f }, { 19.5f, 2.5f, 5.5f } }, 50 },
    };

    while (!WindowShouldClose())
    {
        Vector3 before = camera.position;
        UpdateCamera(&camera, CAMERA_FIRST_PERSON);
        ApplyCollision(&camera, before);

        if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && player.ammo > 0)
        {
            player.ammo--;
            Vector2 center = { GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f };
            Ray ray = GetMouseRay(center, camera);

            // ยิงหาศัตรูที่ใกล้ที่สุดที่รังสีชน
            int target = -1;
            float best = 1e9f;
            for (int i = 0; i < ENEMY_COUNT; i++)
            {
                if (enemies[i].health <= 0) continue;
                RayCollision hit = GetRayCollisionBox(ray, enemies[i].box);
                if (hit.hit && hit.distance < best)
                {
                    best = hit.distance;
                    target = i;
                }
            }
            if (target >= 0) enemies[target].health -= 25;
        }

        BeginDrawing();
        ClearBackground(SKYBLUE);
        BeginMode3D(camera);
        DrawPlane((Vector3){ 16.0f, 0.0f, 16.0f }, (Vector2){ 32.0f, 32.0f }, LIGHTGRAY);
        DrawMap();
        for (int i = 0; i < ENEMY_COUNT; i++)
        {
            if (enemies[i].health <= 0) continue;
            Vector3 c = {
                (enemies[i].box.min.x + enemies[i].box.max.x) / 2.0f,
                1.25f,
                (enemies[i].box.min.z + enemies[i].box.max.z) / 2.0f,
            };
            DrawCubeV(c, (Vector3){ 1.5f, 2.5f, 1.5f }, RED);
        }
        EndMode3D();

        // จุดเล็งกลางจอ
        DrawLine(GetScreenWidth() / 2 - 10, GetScreenHeight() / 2, GetScreenWidth() / 2 + 10, GetScreenHeight() / 2, BLACK);
        DrawLine(GetScreenWidth() / 2, GetScreenHeight() / 2 - 10, GetScreenWidth() / 2, GetScreenHeight() / 2 + 10, BLACK);

        // HUD
        char text[64];
        snprintf(text, sizeof(text), "Ammo %d", player.ammo);
        DrawText(text, GetScreenWidth() - 180, GetScreenHeight() - 40, 24, BLACK);
        DrawRectangle(20, GetScreenHeight() - 40, 200, 20, DARKGRAY);
        DrawRectangle(20, GetScreenHeight() - 40, (int)(200 * player.health / 100.0f), 20, RED);
        DrawFPS(10, 10);
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

**見えるはずの結果:** 壁のある部屋を歩き、赤い敵を撃ちます。1体は2発当てると消えます。射撃のたびに弾薬が減ります。

10. コンパイルと実行

Developer Command Promptでclコマンドを使い、raylib.libとリンクしてコンパイルします。

1. Developer Command Prompt for VS 2022を開き、プロジェクトフォルダに移動する

2. 下のブロックのコンパイルコマンドを実行する

3. できた fps-c-raylib.exe を実行する

cl /nologo /W3 /I raylib\include src\main.c /link /LIBPATH:raylib\lib raylib.lib winmm.lib gdi32.lib user32.lib shell32.lib

**見えるはずの結果:** コンパイル中にエラーは出ず、.exeを実行するとゲームのウィンドウが開きます。

次のステップ

次は、射撃音、プレイヤーに向かって歩く敵、スタートメニュー、テキストファイルからのマップ読み込みを追加しましょう。raylibのサンプルは github.com/raysan5/raylib/tree/master/examples にあります。

まとめ

オープンソースのエンジンとしてraylibをCで使い、マップ、移動、壁との衝突、光線による射撃、HUDは自分で書きます。MSVCでコンパイルすると1つの.exeになります。各手順は順番どおりに実行できます。