بناء لعبة تصويب من منظور الشخص الأول (FPS) في Unity: التحكم بالنظر، والحركة، والإطلاق، والاختبار
لعبة التصويب من منظور الشخص الأول (FPS) تعرض العالم من عيني الشخصية. الأنظمة الأساسية فيها هي التحكم بالنظر بالفأرة، والحركة بمفاتيح WASD، والإطلاق بالأشعة، والأهداف التي لها صحة. تبني هذه المقالة كل نظام خطوة بخطوة في Unity، من مشروع جديد حتى كود ينجح في الاختبارات.
ملاحظة: يحتفظ الكود بالتعليقات التايلاندية من النسخة الأصلية. لم يُشغَّل في محرر Unity. تعمل الاختبارات على واجهة Unity وهمية كُتبت لهذه المقالة، وتؤكد منطق النص البرمجي فقط. ملف الواجهة الوهمية كاملًا موجود في النسخة التايلاندية من المقالة.
ما الذي تحتاجه
- Unity Hub ومحرر Unity بأحدث إصدار LTS
- VS Code أو Visual Studio
- أساسيات مقالة Unity ثنائية الأبعاد (كائنات GameObject والمكونات) تساعد كثيرًا
الخطوة 1: إنشاء مشروع ثلاثي الأبعاد وإعداد المدخلات
1. افتح Unity Hub واضغط **New project**، واختر قالب **Universal 3D** (URP) أو **3D** (Built-in)، وسمِّ المشروع `MyFPS`.
2. انتقل إلى **Edit → Project Settings → Player**، واجعل **Active Input Handling** هو **Both** أو **Input Manager (Old)**، ثم أعد تشغيل المحرر عند الطلب.
3. تحقق من وجود المحاور **Mouse X** و**Mouse Y** و**Horizontal** و**Vertical** وزر **Jump** في **Edit → Project Settings → Input Manager**. هي موجودة في القوالب القياسية.
الخطوة 2: إنشاء الأرضية والهدف
1. انقر بزر الفأرة الأيمن في Hierarchy واختر **3D Object → Plane**. سمّه `Ground` واجعل Scale هو `X = 5, Y = 1, Z = 5`.
2. انقر مرة أخرى واختر **3D Object → Cube**. سمّه `Target` وضعه على بُعد نحو 10 أمتار، مثلًا Position `Z = 10` و`Y = 1`.
3. أضف مكون **Target** (وسنكتبه في الخطوة 6) إلى المكعب. يحتوي المكعب على Box Collider، وهو لازم حتى يصطدم الشعاع به.
الخطوة 3: إنشاء اللاعب
1. انقر بزر الفأرة الأيمن في Hierarchy واختر **Create Empty**، وسمّه `Player` واجعل Position هو `Y = 1`.
2. أضف **Character Controller** إلى `Player`. يحرّك الشخصية دون الحاجة إلى Rigidbody.
3. انقر بزر الفأرة الأيمن على `Player` واختر **Camera** لتصبح ابنًا له. اجعل Position هو `Y = 0.6` لتكون الرؤية على مستوى العين.
4. احذف Main Camera الأصلية، لأن الكاميرا التابعة تحل محلها.
البنية هي `Player` ← `Camera`. وسيحمل `Player` لاحقًا المكونات `MouseLook` و`FPSMovement` و`Weapon`.
الخطوة 4: التحكم بالنظر بالفأرة
أنشئ نصًا برمجيًا باسم `MouseLook` والصق فيه الكود التالي.
using UnityEngine;
// หมุนกล้องตามเมาส์: ก้ม-เงยที่กล้อง (pitch) และหันซ้าย-ขวาที่ตัวละคร (yaw)
public class MouseLook : MonoBehaviour
{
[SerializeField] private float sensitivity = 2f; // ความไวเมาส์
[SerializeField] private float minPitch = -80f; // ก้มลงได้สุดกี่องศา
[SerializeField] private float maxPitch = 80f; // เงยขึ้นได้สุดกี่องศา
[SerializeField] private Transform body; // ตัวละครที่ต้องหมุนตามแนวนอน
private float pitch;
public float Pitch => pitch;
private void Update()
{
float mouseX = Input.GetAxis("Mouse X") * sensitivity;
float mouseY = Input.GetAxis("Mouse Y") * sensitivity;
// เมาส์ขึ้นคือเงยหน้า จึงต้องลบค่า mouseY
pitch = Mathf.Clamp(pitch - mouseY, minPitch, maxPitch);
transform.localRotation = Quaternion.Euler(pitch, 0f, 0f);
body.Rotate(0f, mouseX, 0f);
}
}
الأفكار الأساسية:
- يُطبَّق الدوران لأعلى ولأسفل على الكاميرا، فلا يميل الجسم.
- يُطبَّق الدوران يمينًا ويسارًا على الجسم، فيتبع اتجاه المشي ما ينظر إليه اللاعب.
- تحدّ `Mathf.Clamp` زاوية النظر حتى لا ينقلب المنظور.
في Inspector اسحب `Player` إلى حقل **Body** الخاص بـ `MouseLook` الموجود على الكاميرا.
الخطوة 5: الحركة بـ WASD والقفز
أنشئ نصًا برمجيًا باسم `FPSMovement` والصق فيه الكود التالي، ثم ألحقه بـ `Player`.
using UnityEngine;
// เดินแบบ FPS ด้วย WASD โดยใช้ CharacterController (ไม่ใช้ Rigidbody)
[RequireComponent(typeof(CharacterController))]
public class FPSMovement : MonoBehaviour
{
[SerializeField] private float speed = 6f; // ความเร็วเดิน
[SerializeField] private float jumpHeight = 1.5f; // ความสูงกระโดด (เมตร)
[SerializeField] private float gravity = -20f; // แรงโน้มถ่วง
private CharacterController controller;
private float verticalVelocity;
public bool IsGrounded => controller.isGrounded;
private void Awake()
{
controller = GetComponent<CharacterController>();
}
private void Update()
{
// แกน x คือซ้าย-ขวา และแกน z คือหน้า-หลัง ทั้งคู่อิงทิศทางที่ตัวละครหันอยู่
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
Vector3 move = transform.right * h + transform.forward * v;
// จำกัดความยาวเวกเตอร์ไม่ให้เดินเฉียงเร็วกว่าเดินตรง
if (move.magnitude > 1f) move = move.normalized;
controller.Move(move * speed * Time.deltaTime);
// แรงโน้มถ่วงและการกระโดด
if (controller.isGrounded && verticalVelocity < 0f)
{
verticalVelocity = -2f; // กดให้ติดพื้นเล็กน้อย
}
if (Input.GetButtonDown("Jump") && controller.isGrounded)
{
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
verticalVelocity += gravity * Time.deltaTime;
controller.Move(new Vector3(0f, verticalVelocity * Time.deltaTime, 0f));
}
}
الأفكار الأساسية:
- يتعامل `CharacterController.Move` مع الاصطدام بالجدران تلقائيًا، لذلك يناسب FPS أكثر من تعديل الموقع مباشرة.
- يجب تحديد سرعة الحركة القطرية، وإلا فستكون الحركة القطرية أسرع بنحو 41 بالمئة (الجذر التربيعي للعدد 2).
- تعطي المعادلة `Sqrt(jumpHeight * -2 * gravity)` السرعة الابتدائية التي تبلغ بها الشخصية ارتفاع القفز المطلوب.
الخطوة 6: سلاح بالأشعة وهدف له صحة
يطلق السلاح الفوري (hitscan) شعاعًا إلى الأمام من فوهة السلاح. إذا اصطدم الشعاع بشيء يحمل مكون `Target`، تنقص صحة هذا الهدف.
أنشئ نصًا برمجيًا باسم `Weapon` وألحقه بـ `Player`.
using UnityEngine;
// ปืนแบบ hitscan: ยิงรังสีจากปากกระบอกปืน ถ้าโดนเป้าหมายก็ลดเลือด
public class Weapon : MonoBehaviour
{
[SerializeField] private Transform muzzle; // จุดปากกระบอกปืน
[SerializeField] private float range = 100f; // ระยะยิงสูงสุด
[SerializeField] private int damage = 25; // ดาเมจต่อนัด
[SerializeField] private float fireInterval = 0.2f; // เวลาขั้นต่ำระหว่างนัด (วินาที)
[SerializeField] private int maxAmmo = 10; // กระสุนเต็มซอง
private int ammo;
private float nextFireTime;
public int Ammo => ammo;
private void Awake()
{
ammo = maxAmmo;
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
TryShoot();
}
}
// คืน true ถ้ายิงจริง, false ถ้ากระสุนหมดหรือยังยิงไม่ได้
public bool TryShoot()
{
if (ammo <= 0 || Time.time < nextFireTime) return false;
ammo--;
nextFireTime = Time.time + fireInterval;
RaycastHit hit;
if (Physics.Raycast(muzzle.position, muzzle.forward, out hit, range))
{
Target target = hit.collider.GetComponent<Target>();
if (target != null)
{
target.TakeDamage(damage);
}
}
return true;
}
}
أنشئ كائنًا فارغًا باسم `Muzzle` كابن للكاميرا، وضعه أمام السلاح، ثم اسحبه إلى حقل **Muzzle** الخاص بـ `Weapon`.
أنشئ نصًا برمجيًا باسم `Target` وألحقه بالمكعب.
using UnityEngine;
// เป้าหมายที่ถูกยิงได้: มีเลือด เมื่อเลือดหมดจะตาย
public class Target : MonoBehaviour
{
[SerializeField] private int maxHealth = 100;
private int health;
public int Health => health;
public bool IsDead => health <= 0;
private void Awake()
{
health = maxHealth;
}
public void TakeDamage(int amount)
{
if (IsDead) return;
health -= amount;
if (health < 0) health = 0;
if (IsDead)
{
Die();
}
}
private void Die()
{
// ในเกมจริงอาจใช้ Destroy(gameObject) หรือเล่นแอนิเมชันตาย
gameObject.SetActive(false);
}
}
ملاحظة على الكود: كانت النسخة الأولى من `Target` تكتب `health = Mathf.Max(0, health - amount);`. هذا الكود لا يُترجم، لأن `Mathf.Max` تعيد قيمة من نوع float بينما `health` من نوع int. لاحظ المترجم الخطأ قبل أي اختبار تشغيل. النسخة أعلاه تطرح القيمة مباشرة وتمنع النزول دون الصفر.
الخطوة 7: الاختبارات الآلية
يستخدم إطار الاختبار واجهة Unity وهمية (`UnityStubs.cs`، وهي كاملة في النسخة التايلاندية من المقالة) وملف الاختبار `FPSTests.cs`. يتحقق الاختبار من النظر بالفأرة، والحركة، والقفز، والإطلاق، وفاصل الإطلاق، وحد الذخيرة، وموت الهدف.
التجميع والتشغيل:
1. أنشئ مجلدًا باسم `build` وانسخ إليه `UnityStubs.cs` و`FPSTests.cs` والنصوص البرمجية الأربعة.
2. في النسخ فقط احذف السطر `using UnityEngine;`. أبقه في النصوص الحقيقية داخل مشروع Unity.
3. جمّع الملفات بمترجم C# حديث (مثلًا `csc -langversion:latest -out:FPSTests.exe *.cs`) ثم شغّل `FPSTests.exe`.
نتائج الاختبار
PASS mouse up: pitch becomes -2 (looking up)
PASS mouse up for long: pitch stops at minPitch (-80)
PASS mouse right: body turns 2 degrees
PASS walk forward 1 second: moves about 6 units
PASS diagonal walk: speed capped at about 6 (not 8.5)
PASS jump from ground: rises above the ground in the first frame
PASS shoot: fires when ammo is available
PASS shoot hit: target health drops by damage (100 to 75)
PASS shoot: ammo decreases from 10 to 9
PASS fire interval: second shot within 0.2 s is blocked
PASS fire interval: ammo is not spent on the blocked shot
PASS 4 hits of 25: target is dead
PASS 4 hits of 25: target object is deactivated
PASS empty magazine: after 10 shots the 11th is refused
Passed 14 / Failed 0
هذه النتائج جاءت من الواجهة الوهمية وليس من Unity. الفيزياء والتصادمات والرسوم المتحركة وسلوك الكاميرا الحقيقي ما زالت تحتاج إلى التحقق بالضغط على Play في المحرر.
الخطوة 8: الاختبار داخل المحرر
اضغط **Play** وتحقق من:
- تحريك الفأرة يدير الكاميرا يمينًا ويسارًا ويرفعها ويخفضها، دون تجاوز -80 أو 80 درجة.
- تحرك المفاتيح W وA وS وD الشخصية في الاتجاه الذي تنظر إليه.
- يقفز اللاعب عند الضغط على Space.
- النقر بالزر الأيسر يطلق النار. يفقد الهدف صحته مع كل إصابة، ويختفي بعد أربع إصابات.
- بعد عشر طلقات، لا يطلق السلاح عند النقر مجددًا.
مشكلات شائعة
- **الكاميرا لا تدور:** تحقق من إعداد Active Input Handling، وأن حقل Body في `MouseLook` يحتوي على `Player`.
- **الشخصية تخترق الجدران:** تحقق من أن للجدران مصادمات، وأن Character Controller يناسب حجم الشخصية.
- **الطلقات تخطئ:** تحقق من أن Muzzle يتجه إلى الأمام (المحور Z)، وأن للهدف مصادمًا ومكون `Target`.
- **خطأ Mathf.Max:** لا تستخدم `Mathf.Max` مع متغير من نوع int. استخدم الطريقة الموضحة في المقالة.
الخلاصة
يتكون FPS من أربعة أنظمة رئيسية: النظر بالكاميرا، والحركة، والإطلاق بالأشعة، والأهداف التي لها صحة. كل نظام نص برمجي صغير يمكن اختباره على حدة. كشف الاختبار خطأ تجميع قبل أي اختبار تشغيل، مما يوفر وقتًا لاحقًا. الخطوات التالية تشمل أعداءً يتحركون، وعدّاد ذخيرة على الشاشة، وإعادة التعبئة.