Membuat FPS Online di Unity: Multiplayer, Otoritas Server, dan Pengujian

FPS online mempertemukan beberapa pemain dalam satu pertandingan melalui jaringan. Artikel ini memakai **Netcode for GameObjects** (NGO), paket jaringan resmi Unity, untuk membangun dasar-dasarnya: memulai sebagai host atau client, objek pemain yang tersinkron antarmesin, penembakan yang diputuskan server, kebangkitan kembali, dan skor pembunuhan.

Aturan terpenting dalam game online adalah **otoritas server**. Client boleh mengatakan bahwa ia menembak, tetapi ia tidak boleh memutuskan apakah tembakannya kena atau berapa kesehatan seorang pemain. Kode di bawah mengikuti aturan itu.

Penting: aturan pertandingan (damage, kematian, kebangkitan, skor) sudah dikompilasi dan dijalankan di lingkungan ini: 8 tes, semuanya lolos. Bagian jaringan (NGO) belum dijalankan di sini, karena memerlukan Unity Editor dan minimal dua pemain. Ikuti langkahnya dan uji sendiri sebelum mengandalkannya.

Catatan: kode dalam artikel ini mempertahankan komentar bahasa Thailand dari versi asli.

Yang Anda butuhkan

Langkah 1: pasang Netcode for GameObjects

1. Buka proyek FPS lalu pilih **Window → Package Manager**.

2. Pilih **Unity Registry**, cari **Netcode for GameObjects**, lalu klik **Install**.

3. Pasang **Unity Transport** sesuai rekomendasi NGO. Biasanya sudah terpasang otomatis.

4. Periksa **Edit → Project Settings → Player**. Active Input Handling tetap harus Both atau Input Manager (Old), seperti di artikel FPS.

Langkah 2: buat NetworkManager dan menu mulai

Skema scene (bukan tangkapan layar asli): 1 GameObject NetworkManager 2 GameObject NetworkBoot 3 tombol Start Host 4 tombol Start Client 5 kolom alamat server
Skema scene (bukan tangkapan layar asli): 1 GameObject NetworkManager 2 GameObject NetworkBoot 3 tombol Start Host 4 tombol Start Client 5 kolom alamat server

1. Klik kanan di Hierarchy, pilih **Create Empty**, dan beri nama `NetworkManager`.

2. Klik **Add Component** lalu tambahkan **Network Manager** dan **Unity Transport**.

3. Buat objek kosong lain bernama `NetworkBoot` dan tambahkan skrip `NetworkBoot` di bawah.

4. Tekan **Play**. Dua tombol muncul di pojok kiri atas.

using Unity.Netcode;
using UnityEngine;

// ปุ่มเริ่มเกม: เลือกเป็นโฮสต์ (เซิร์ฟเวอร์ + ผู้เล่น) หรือไคลเอนต์ (เข้าร่วม)
// วางบน GameObject ชื่อ NetworkBoot ในฉาก และให้มี NetworkManager อยู่ในฉากด้วย
// Tombol mulai permainan: pilih menjadi host (server + pemain) atau client (bergabung).
// Letakkan di GameObject bernama NetworkBoot di scene, dan pastikan ada NetworkManager di scene.
public class NetworkBoot : MonoBehaviour
{
    [SerializeField] private string address = "127.0.0.1";  // ที่อยู่ของโฮสต์ (ใส่ IP ของเครื่องโฮสต์เมื่อเล่นข้ามเครื่อง) / alamat host (isi IP mesin host saat bermain antarmesin)

    private void OnGUI()
    {
        if (NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer) return;

        GUILayout.BeginArea(new Rect(10, 10, 220, 120));
        if (GUILayout.Button("Start Host (server + player)")) NetworkManager.Singleton.StartHost();
        if (GUILayout.Button("Start Client (join)")) NetworkManager.Singleton.StartClient();
        address = GUILayout.TextField(address);
        GUILayout.EndArea();
    }
}

Kolom alamat di sini hanya kotak teks untuk penguji. Atur alamat sungguhan pada komponen **Unity Transport** saat bermain antarmesin.

Langkah 3: buat prefab Player

Skema Inspector prefab Player (bukan tangkapan layar asli): 1 Network Object 2 Network Transform 3 Player Network 4 Owner Setup 5 kolom Camera Object
Skema Inspector prefab Player (bukan tangkapan layar asli): 1 Network Object 2 Network Transform 3 Player Network 4 Owner Setup 5 kolom Camera Object

1. Mulai dari GameObject `Player` proyek FPS (Character Controller, FPSMovement, MouseLook, dan kamera anak dengan Muzzle).

2. Tambahkan **Network Object**. Ini memberi tahu NGO bahwa objek tersebut tersinkron.

3. Tambahkan **Network Transform** untuk menyinkronkan posisi dan rotasi ke setiap mesin.

4. Hapus komponen `Weapon`. Penembakan dipindahkan ke `PlayerNetwork`.

5. Tambahkan skrip `PlayerNetwork` dan `OwnerSetup` (di bawah).

6. Seret `Player/Camera/Muzzle` ke kolom **Muzzle**, dan kamera anak ke kolom **Camera Object** pada `OwnerSetup`.

7. Pada `OwnerSetup`, tambahkan `FPSMovement` dan `MouseLook` ke **Owner Only**, sehingga hanya pemiliknya yang bisa mengendalikannya.

8. Seret `Player` dari Hierarchy ke `Assets/Prefabs/` untuk menjadikannya prefab, lalu hapus dari scene.

9. Pilih `NetworkManager` dan seret prefab ini ke kolom **Player Prefab**.

Langkah 4: aturan pertandingan dalam C# biasa

Aturan disimpan dalam class biasa yang tidak memakai `UnityEngine`, sehingga bisa diuji tanpa editor. Inilah logika yang dipakai server.

// กติกาของแมตช์ เขียนเป็น C# ธรรมดา (ไม่ใช้ UnityEngine) เพื่อให้ทดสอบได้โดยไม่ต้องเปิด Unity
// เซิร์ฟเวอร์เป็นผู้ตัดสินทุกอย่างด้วยกติกานี้ ไคลเอนต์ไม่มีสิทธิ์เปลี่ยนเลือดหรือคะแนนเอง
// Aturan pertandingan ditulis dalam C# biasa (tanpa UnityEngine) agar bisa diuji tanpa membuka Unity.
// Server memutuskan semuanya dengan aturan ini; client tidak berhak mengubah darah atau skor sendiri.
public class MatchRules
{
    public const int MaxHealth = 100;
    public const int DamagePerShot = 25;
    public const int KillsToWin = 10;
    public const float RespawnSeconds = 3f;

    public int Health { get; private set; }
    public int Kills { get; private set; }
    public int Deaths { get; private set; }
    public float RespawnTimer { get; private set; }

    public bool IsDead => Health <= 0;

    public MatchRules()
    {
        Health = MaxHealth;
    }

    // คืน true ถ้าการยิงนี้ทำให้ผู้ถูกยิงตาย / true jika tembakan ini membuat korban mati
    public bool ApplyDamage(int amount)
    {
        if (IsDead) return false;
        Health -= amount;
        if (Health <= 0)
        {
            Health = 0;
            Deaths++;
            RespawnTimer = RespawnSeconds;
            return true;
        }
        return false;
    }

    // เรียกทุกเฟรม (หรือทุกรอบของเซิร์ฟเวอร์) คืน true เมื่อครบเวลาและฟื้นคืนชีพแล้ว
    // Dipanggil setiap frame (atau setiap putaran server). true ketika waktunya habis dan sudah bangkit kembali
    public bool Tick(float deltaSeconds)
    {
        if (!IsDead) return false;
        RespawnTimer -= deltaSeconds;
        if (RespawnTimer > 0f) return false;

        Health = MaxHealth;
        RespawnTimer = 0f;
        return true;
    }

    public void AddKill()
    {
        Kills++;
    }

    public bool HasWon()
    {
        return Kills >= KillsToWin;
    }
}

Langkah 5: skrip pemain online

Skrip ini memiliki tiga bagian:

using Unity.Netcode;
using UnityEngine;

// ผู้เล่นออนไลน์: ผู้เล่นเจ้าของเครื่องส่งคำสั่งยิง แต่เซิร์ฟเวอร์เป็นคนตรวจการยิง คิดดาเมจ และนับคะแนน
// ต้องติดอยู่บน Player prefab พร้อม NetworkObject และ NetworkTransform
// Pemain online: pemilik mengirim perintah tembak, tetapi server yang memeriksa tembakan, menghitung damage, dan mencatat skor.
// Harus menempel pada prefab Player bersama NetworkObject dan NetworkTransform.
public class PlayerNetwork : NetworkBehaviour
{
    [SerializeField] private Transform muzzle;                 // ปากกระบอกปืน (อยู่ใต้กล้องของผู้เล่น) / moncong senjata (di bawah kamera pemain)
    [SerializeField] private float range = 100f;
    [SerializeField] private float fireInterval = 0.2f;
    [SerializeField] private LayerMask hitMask;                // เลเยอร์ที่ยิงโดน (ผู้เล่นและกำแพง) / layer yang bisa terkena (pemain dan dinding)
    [SerializeField] private Transform[] spawnPoints;          // จุดเกิดที่เซิร์ฟเวอร์เลือกตอนฟื้นคืนชีพ / titik muncul yang dipilih server saat bangkit kembali

    // เลือดและคะแนน: ทุกเครื่องอ่านได้ แต่มีเพียงเซิร์ฟเวอร์ที่เขียนค่าได้
    // Darah dan skor: semua mesin bisa membaca, tetapi hanya server yang bisa menulis nilainya
    public NetworkVariable<int> Health = new NetworkVariable<int>(
        MatchRules.MaxHealth, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
    public NetworkVariable<int> Kills = new NetworkVariable<int>(
        0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);

    private float nextFireTime;
    private float respawnTimer;

    public bool IsAlive => Health.Value > 0;

    private void Update()
    {
        // ฝั่งผู้เล่น: รับปุ่มยิง แล้วส่งคำขอไปยังเซิร์ฟเวอร์ / sisi pemain: menerima tombol tembak lalu mengirim permintaan ke server
        if (IsOwner && Input.GetMouseButtonDown(0) && Time.time >= nextFireTime && IsAlive)
        {
            nextFireTime = Time.time + fireInterval;
            FireServerRpc(muzzle.position, muzzle.forward);
        }

        // ฝั่งเซิร์ฟเวอร์: นับเวลาฟื้นคืนชีพ / sisi server: menghitung waktu bangkit kembali
        if (IsServer && Health.Value <= 0)
        {
            respawnTimer -= Time.deltaTime;
            if (respawnTimer <= 0f) Respawn();
        }
    }

    // ทำงานบนเซิร์ฟเวอร์เท่านั้น เมื่อผู้เล่นส่งคำขอยิง / hanya berjalan di server, ketika pemain mengirim permintaan tembak
    [ServerRpc]
    private void FireServerRpc(Vector3 origin, Vector3 direction)
    {
        if (!IsAlive) return;                                  // คนตายยิงไม่ได้ / pemain yang mati tidak bisa menembak

        if (Physics.Raycast(origin, direction, out RaycastHit hit, range, hitMask))
        {
            PlayerNetwork target = hit.collider.GetComponentInParent<PlayerNetwork>();
            if (target != null && target != this)
            {
                target.TakeDamageOnServer(MatchRules.DamagePerShot, OwnerClientId);
            }
        }
    }

    // เรียกได้เฉพาะบนเซิร์ฟเวอร์ คิดดาเมจและให้คะแนนผู้ยิงเมื่อฆ่าสำเร็จ
    // Hanya bisa dipanggil di server. Menghitung damage dan memberi skor kepada penembak jika berhasil membunuh
    public void TakeDamageOnServer(int amount, ulong shooterClientId)
    {
        if (!IsServer || !IsAlive) return;

        int remaining = Health.Value - amount;
        Health.Value = remaining < 0 ? 0 : remaining;   // ใช้การเช็คเอง เพราะ Mathf.Max รับ float / pakai pengecekan sendiri karena Mathf.Max menerima float
        if (Health.Value > 0) return;

        respawnTimer = MatchRules.RespawnSeconds;
        var shooter = NetworkManager.Singleton.ConnectedClients[shooterClientId].PlayerObject;
        if (shooter != null)
        {
            shooter.GetComponent<PlayerNetwork>().Kills.Value++;
        }
    }

    private void Respawn()
    {
        Health.Value = MatchRules.MaxHealth;
        if (spawnPoints != null && spawnPoints.Length > 0)
        {
            transform.position = spawnPoints[Random.Range(0, spawnPoints.Length)].position;
        }
    }
}

Poin-poin penting:

Langkah 6: kontrol khusus pemilik dan kamera

Skema (bukan tangkapan layar asli): kolom Owner Only dan Camera Object dari langkah 3
Skema (bukan tangkapan layar asli): kolom Owner Only dan Camera Object dari langkah 3

Setiap pemain memiliki salinan prefab Player. Tanpa langkah ini, satu pemain bisa mengendalikan karakter pemain lain dan melihat kamera tambahan. `OwnerSetup` menangani hal itu.

using Unity.Netcode;
using UnityEngine;

// เปิดการควบคุมและกล้องเฉพาะผู้เล่นเจ้าของตัวละครนั้น ผู้เล่นคนอื่นที่เห็นตัวละครนี้จะไม่มีกล้องและไม่รับปุ่ม
// Hanya mengaktifkan kontrol dan kamera untuk pemilik karakter itu. Pemain lain yang melihat karakter ini tidak punya kamera dan tidak menerima input.
public class OwnerSetup : NetworkBehaviour
{
    [SerializeField] private Behaviour[] ownerOnly;   // สคริปต์ที่ต้องเปิดเฉพาะเจ้าของ เช่น FPSMovement และ MouseLook / skrip yang hanya aktif untuk pemilik, misalnya FPSMovement dan MouseLook
    [SerializeField] private GameObject cameraObject; // กล้องลูกของผู้เล่น / kamera anak milik pemain

    public override void OnNetworkSpawn()
    {
        foreach (Behaviour script in ownerOnly)
        {
            script.enabled = IsOwner;
        }
        if (cameraObject != null)
        {
            cameraObject.SetActive(IsOwner);
        }
    }
}

Langkah 7: alur data

Diagram alur (bukan tangkapan layar asli): 1 client A mengirim permintaan tembak 2 server memeriksa tembakan dan menerapkan damage 3 setiap client menerima darah yang baru
Diagram alur (bukan tangkapan layar asli): 1 client A mengirim permintaan tembak 2 server memeriksa tembakan dan menerapkan damage 3 setiap client menerima darah yang baru

Ketika pemain A menembak dan mengenai pemain B:

1. Pemain A mengklik. Client A memanggil `FireServerRpc` dengan posisi dan arah moncong senjata.

2. Server menerimanya, memastikan A masih hidup, lalu melakukan raycast dari posisi itu.

3. Jika sinar mengenai B, server memanggil `TakeDamageOnServer` dan mengurangi darah B.

4. Darah B adalah `NetworkVariable`, jadi NGO menyinkronkannya ke setiap mesin. Semua orang melihat angka yang sama.

5. Jika darah B mencapai nol, server memberi kill kepada A dan memulai timer kebangkitan B.

Langkah 8: pengujian

Aturan pertandingan (dijalankan di lingkungan ini)

Berkas tes `MatchRulesTests.cs` (di versi Thailand) memeriksa damage, kematian, waktu kebangkitan, dan kondisi menang tanpa Unity. Kompilasi `MatchRules.cs` dan `MatchRulesTests.cs` bersama dengan kompiler C# terbaru (misalnya `csc -langversion:latest -out:MatchRulesTests.exe *.cs`) lalu jalankan keluarannya.

PASS  one shot: health drops from 100 to 75
PASS  four shots: player dies on the fourth
PASS  four shots: death is counted
PASS  damage while dead: no extra death counted
PASS  respawn: still dead after 1 second
PASS  respawn: back to full health after 3 seconds
PASS  win: not yet at 9 kills
PASS  win: wins at 10 kills

Passed 8 / Failed 0

Permainan jaringan (lakukan di Unity Editor)

Bagian ini tidak dijalankan di sini. Lakukan sendiri:

1. Tekan **Play** di editor dan klik **Start Host**. Anda adalah satu pemain.

2. Buka jendela Unity kedua untuk proyek yang sama, atau gunakan **ParrelSync** (alat populer untuk menjalankan banyak instance editor), lalu klik **Start Client**.

3. Pastikan setiap pemain melihat yang lain dan masing-masing hanya menggerakkan karakternya sendiri.

4. Tembak pemain lain empat kali. Pastikan darah berkurang di kedua layar dan penembak mendapat kill.

5. Pastikan pemain bangkit kembali tiga detik setelah mati.

Masalah umum

Batasan artikel ini

Pelajari topik-topik ini sebelum merilis game ke banyak pemain.

Ringkasan

Game FPS online bergantung pada otoritas server: client mengirim permintaan, server yang memutuskan, dan nilai yang dilihat semua orang hanya ditulis oleh server. Aturannya berada dalam class biasa yang bisa diuji. Jaringannya memakai NGO seperti dijelaskan di atas. Tes aturan lolos di sini. Permainan jaringan harus diuji di Unity Editor.