صناعة لعبة FPS بلغة C وraylib: من أول نافذة إلى إطلاق النار على الأعداء
تشرح هذه المقالة كيفية صناعة لعبة تصويب بمنظور الشخص الأول (FPS) بلغة C. نستخدم raylib، وهي مكتبة محرك مفتوحة المصدر، للنافذة والرسوم ثلاثية الأبعاد والإدخال. ومنطق اللعبة، من الخريطة والتصادم مع الجدران والإطلاق وواجهة HUD، مكتوب كله بلغة C. كل جزء خطوات يمكن تنفيذها واحدة تلو الأخرى، مع مخطط للهيكل وملفات كود يمكن نسخها.
ما الذي تحتاجه
تحتاج إلى حاسوب يعمل بنظام Windows 10 أو 11، وأدوات Visual Studio 2022 Build Tools مع مجموعة C++ (MSVC) وWindows SDK، وraylib محمّلة من صفحة GitHub Releases. وraylib مرخصة برخصة zlib التي تسمح بالاستخدام التجاري. ملاحظة: الكود في هذه المقالة مكتوب وفق وثائق raylib 5 و6، لكن البرنامج الكامل لم يُجمَّع ولم يُختبر على الجهاز الذي كُتبت فيه المقالة. والنتائج المذكورة هي ما يجب أن تراه عند تنفيذ الخطوات بشكل صحيح. الخط الافتراضي في raylib يعرض الحروف اللاتينية فقط، لذلك تستخدم الأمثلة نصوصًا إنجليزية على الشاشة.
رسوم توضيحية
1. تثبيت الأدوات وraylib
نستخدم MSVC لتجميع لغة C، ونستخدم raylib كمكتبة محرك مفتوحة المصدر.
1. حمّل Visual Studio 2022 Build Tools من موقع Microsoft وثبّت حزمة Desktop development with C++
2. افتح Developer Command Prompt for VS 2022 من قائمة Start
3. ادخل إلى github.com/raysan5/raylib/releases ونزّل ملف Windows الذي ينتهي اسمه بـ win64_msvc16.zip
4. فك ضغط الملف وانسخ مجلدي 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. أنشئ مجلد 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. رسم الخريطة كجدران ثلاثية الأبعاد
نخزّن الخريطة كمصفوفة من الأرقام 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;
}
**ما يجب أن تراه:** تشكّل الجدران الرمادية الداكنة الخريطة ثلاثية الأبعاد، ويظهر عداد FPS في الزاوية العلوية اليسرى.
5. التحرك وتدوير النظر بـ WASD والفأرة
تدعم دالة UpdateCamera في raylib الوضع 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. ينخفض عدد الذخيرة مع كل طلقة.
**الكود: 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. جمع كل شيء معًا
يجمع هذا الملف الخريطة والتصادم مع الجدران وعدوين لكل منهما صحة، والإطلاق نحو أقرب إصابة، وواجهة 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;
}
**ما يجب أن تراه:** تسير في غرفة فيها جدران وتطلق النار على الأعداء الحمر. يحتاج أحدهم إلى إصابتين حتى يختفي، ويقل عدد الذخيرة مع كل طلقة.
10. التجميع والتشغيل
استخدم أمر cl في Developer Command Prompt للتجميع والربط مع 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.
الخطوات التالية
بعد ذلك أضف أصوات الإطلاق، وأعداء يتجهون نحو اللاعب، وقائمة بدء اللعبة، وتحميل الخرائط من ملف نصي. أمثلة إضافية في github.com/raysan5/raylib/tree/master/examples.
الخلاصة
استخدم raylib كمحرك مفتوح المصدر مع لغة C، واكتب الخريطة والحركة والتصادم مع الجدران والإطلاق بالأشعة وواجهة HUD بنفسك. جمّعها بـ MSVC لتحصل على ملف .exe واحد. يمكن تنفيذ كل خطوة بالترتيب.