Membuat Game Penembak Orang Pertama di Unity: Gerakan Mouse, Berjalan, Menembak, dan Pengujian

Penembak orang pertama (FPS) menampilkan dunia dari mata karakter. Sistem intinya adalah gerakan mouse untuk melihat sekeliling, berjalan dengan WASD, menembak dengan sinar, dan target yang memiliki kesehatan. Artikel ini membangun setiap sistem langkah demi langkah di Unity, dari proyek baru sampai kode yang lolos pengujian.

Catatan: kode dalam artikel ini mempertahankan komentar bahasa Thailand dari versi asli. Kode belum dijalankan di Unity Editor. Pengujian dijalankan terhadap API Unity tiruan yang ditulis untuk artikel ini, yang hanya memastikan logika skrip. Berkas stub lengkapnya ada di versi Thailand artikel ini.

Yang Anda butuhkan

Langkah 1: buat proyek 3D dan atur input

1. Buka Unity Hub, klik **New project**, pilih templat **Universal 3D** (URP) atau **3D** (Built-in), lalu beri nama `MyFPS`.

2. Buka **Edit → Project Settings → Player**, atur **Active Input Handling** menjadi **Both** atau **Input Manager (Old)**, dan mulai ulang editor jika diminta.

3. Pastikan sumbu **Mouse X**, **Mouse Y**, **Horizontal**, **Vertical**, dan tombol **Jump** ada di **Edit → Project Settings → Input Manager**. Semuanya sudah termasuk dalam templat standar.

Langkah 2: buat tanah dan target

1. Klik kanan di Hierarchy lalu pilih **3D Object → Plane**. Beri nama `Ground` dan atur Scale menjadi `X = 5, Y = 1, Z = 5`.

2. Klik kanan lagi lalu pilih **3D Object → Cube**. Beri nama `Target`, letakkan sekitar 10 meter di depan, misalnya Position `Z = 10`, `Y = 1`.

3. Tambahkan komponen **Target** (ditulis di langkah 6) ke kubus. Kubus sudah memiliki Box Collider, dan sinar membutuhkannya agar bisa mengenai kubus.

Langkah 3: buat pemain

1. Klik kanan di Hierarchy, pilih **Create Empty**, beri nama `Player`, dan atur Position `Y = 1`.

2. Tambahkan **Character Controller** ke `Player`. Komponen ini menggerakkan karakter tanpa Rigidbody.

3. Klik kanan `Player` lalu pilih **Camera** agar menjadi anaknya. Atur Position-nya menjadi `Y = 0.6` agar pandangan berada di ketinggian mata.

4. Hapus Main Camera asli, karena kamera anak menggantikannya.

Strukturnya adalah `Player` → `Camera`. `Player` juga akan menampung `MouseLook`, `FPSMovement`, dan `Weapon`.

Langkah 4: gerakan mouse

Buat skrip bernama `MouseLook` dan tempelkan kode ini.

using UnityEngine;

// หมุนกล้องตามเมาส์: ก้ม-เงยที่กล้อง (pitch) และหันซ้าย-ขวาที่ตัวละคร (yaw)
// Memutar kamera mengikuti mouse: mendongak-menunduk pada kamera (pitch) dan menoleh kiri-kanan pada karakter (yaw)
public class MouseLook : MonoBehaviour
{
    [SerializeField] private float sensitivity = 2f;   // ความไวเมาส์ / sensitivitas mouse
    [SerializeField] private float minPitch = -80f;    // ก้มลงได้สุดกี่องศา / batas menunduk dalam derajat
    [SerializeField] private float maxPitch = 80f;     // เงยขึ้นได้สุดกี่องศา / batas mendongak dalam derajat
    [SerializeField] private Transform body;           // ตัวละครที่ต้องหมุนตามแนวนอน / karakter yang berputar horizontal

    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 / mouse ke atas berarti mendongak, jadi nilai mouseY dikurangkan
        pitch = Mathf.Clamp(pitch - mouseY, minPitch, maxPitch);
        transform.localRotation = Quaternion.Euler(pitch, 0f, 0f);

        body.Rotate(0f, mouseX, 0f);
    }
}

Gagasan utamanya:

Di Inspector, seret `Player` ke kolom **Body** pada `MouseLook` di kamera.

Langkah 5: gerakan WASD dan lompat

Buat skrip bernama `FPSMovement`, tempelkan kode ini, lalu pasang ke `Player`.

using UnityEngine;

// เดินแบบ FPS ด้วย WASD โดยใช้ CharacterController (ไม่ใช้ Rigidbody)
// Berjalan gaya FPS dengan WASD menggunakan CharacterController (tanpa Rigidbody)
[RequireComponent(typeof(CharacterController))]
public class FPSMovement : MonoBehaviour
{
    [SerializeField] private float speed = 6f;        // ความเร็วเดิน / kecepatan jalan
    [SerializeField] private float jumpHeight = 1.5f; // ความสูงกระโดด (เมตร) / tinggi lompatan (meter)
    [SerializeField] private float gravity = -20f;    // แรงโน้มถ่วง / gravitasi

    private CharacterController controller;
    private float verticalVelocity;

    public bool IsGrounded => controller.isGrounded;

    private void Awake()
    {
        controller = GetComponent<CharacterController>();
    }

    private void Update()
    {
        // แกน x คือซ้าย-ขวา และแกน z คือหน้า-หลัง ทั้งคู่อิงทิศทางที่ตัวละครหันอยู่
        // Sumbu x adalah kiri-kanan dan sumbu z adalah depan-belakang, keduanya mengikuti arah karakter menghadap
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        Vector3 move = transform.right * h + transform.forward * v;

        // จำกัดความยาวเวกเตอร์ไม่ให้เดินเฉียงเร็วกว่าเดินตรง / batasi panjang vektor agar jalan diagonal tidak lebih cepat
        if (move.magnitude > 1f) move = move.normalized;
        controller.Move(move * speed * Time.deltaTime);

        // แรงโน้มถ่วงและการกระโดด / gravitasi dan lompatan
        if (controller.isGrounded && verticalVelocity < 0f)
        {
            verticalVelocity = -2f;                       // กดให้ติดพื้นเล็กน้อย / tekan sedikit agar tetap menempel di tanah
        }
        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));
    }
}

Gagasan utamanya:

Langkah 6: senjata raycast dan target dengan kesehatan

Senjata hitscan menembakkan sinar ke depan dari moncong senjata. Jika sinar mengenai sesuatu yang memiliki komponen `Target`, target itu kehilangan kesehatan.

Buat skrip bernama `Weapon` dan pasang ke `Player`.

using UnityEngine;

// ปืนแบบ hitscan: ยิงรังสีจากปากกระบอกปืน ถ้าโดนเป้าหมายก็ลดเลือด
// Senjata hitscan: menembakkan sinar dari moncong senjata, jika kena target maka darahnya berkurang
public class Weapon : MonoBehaviour
{
    [SerializeField] private Transform muzzle;        // จุดปากกระบอกปืน / titik moncong senjata
    [SerializeField] private float range = 100f;      // ระยะยิงสูงสุด / jarak tembak maksimum
    [SerializeField] private int damage = 25;         // ดาเมจต่อนัด / damage per tembakan
    [SerializeField] private float fireInterval = 0.2f; // เวลาขั้นต่ำระหว่างนัด (วินาที) / jeda minimum antar tembakan (detik)
    [SerializeField] private int maxAmmo = 10;        // กระสุนเต็มซอง / peluru penuh dalam magazen

    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 ถ้ากระสุนหมดหรือยังยิงไม่ได้ / true jika benar-benar menembak, false jika peluru habis atau belum bisa menembak
    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;
    }
}

Buat objek kosong bernama `Muzzle` sebagai anak dari kamera, ditempatkan di depan senjata, lalu seret ke kolom **Muzzle** pada `Weapon`.

Buat skrip bernama `Target` dan pasang ke kubus.

using UnityEngine;

// เป้าหมายที่ถูกยิงได้: มีเลือด เมื่อเลือดหมดจะตาย
// Target yang bisa ditembak: punya darah, dan mati ketika darahnya habis
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) หรือเล่นแอนิเมชันตาย
        // Di game sungguhan bisa memakai Destroy(gameObject) atau memutar animasi mati
        gameObject.SetActive(false);
    }
}

Catatan tentang kode: versi pertama `Target` menulis `health = Mathf.Max(0, health - amount);`. Itu tidak bisa dikompilasi, karena `Mathf.Max` mengembalikan float sedangkan `health` bertipe int. Kompiler menangkapnya sebelum uji coba dijalankan. Versi di atas langsung mengurangi lalu membatasi hasilnya di nol.

Langkah 7: pengujian otomatis

Kerangka pengujian memakai API Unity tiruan (`UnityStubs.cs`, disertakan di versi Thailand artikel ini) dan berkas tes `FPSTests.cs`. Berkas tes memeriksa gerakan mouse, berjalan, lompat, menembak, jeda antar tembakan, batas amunisi, dan kematian target.

Cara mengompilasi dan menjalankan:

1. Buat folder `build` dan salin `UnityStubs.cs`, `FPSTests.cs`, dan keempat skrip ke dalamnya.

2. Hanya pada salinan, hapus baris `using UnityEngine;`. Pertahankan baris itu di skrip proyek Unity yang sebenarnya.

3. Kompilasi dengan kompiler C# terbaru (misalnya `csc -langversion:latest -out:FPSTests.exe *.cs`) lalu jalankan `FPSTests.exe`.

Hasil pengujian

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

Hasil ini berasal dari stub, bukan dari Unity. Fisika, tabrakan, animasi, dan perilaku kamera yang sebenarnya tetap harus diperiksa dengan menekan Play di editor.

Tiruan tampilan game (bukan render asli): target di tengah, bidikan, senjata di tangan, dan HUD yang menampilkan amunisi dan kesehatan target
Tiruan tampilan game (bukan render asli): target di tengah, bidikan, senjata di tangan, dan HUD yang menampilkan amunisi dan kesehatan target
Render keluaran pengujian sebenarnya: 14 lolos, 0 gagal (dijalankan terhadap stub, bukan Unity Editor)
Render keluaran pengujian sebenarnya: 14 lolos, 0 gagal (dijalankan terhadap stub, bukan Unity Editor)

Langkah 8: uji di editor

Tekan **Play** dan periksa:

Masalah umum

Ringkasan

FPS memiliki empat sistem utama: pandangan kamera, gerakan, penembakan dengan raycast, dan target dengan kesehatan. Masing-masing adalah skrip kecil yang bisa diuji secara terpisah. Pengujian menangkap error kompilasi sebelum uji coba di Play, yang menghemat waktu nanti. Langkah berikutnya mencakup musuh yang bergerak, penghitung amunisi di layar, dan pengisian ulang.