Membuat Game 2D di Unity dari Nol: Menambahkan Karakter, Menulis Kode, Menjalankan dan Menguji

Unity adalah salah satu game engine yang paling banyak dipakai, baik oleh pemula maupun tim profesional. Game 2D adalah tempat yang baik untuk belajar gagasan inti: scene, GameObject, komponen, fisika, dan membaca input pemain, tanpa kerumitan yang besar. Artikel ini memandu Anda membuat proyek baru, menambahkan karakter ke scene, menulis skrip agar karakter bisa berjalan dan melompat, membuat kamera mengikutinya, lalu menguji bahwa kode berjalan dengan benar.

Catatan: kode dalam artikel ini mempertahankan komentar bahasa Thailand dari versi asli. Kodenya bekerja sama di bahasa apa pun, dan Anda bisa menerjemahkan komentarnya jika mau.

Skema jendela Unity (bukan tangkapan layar asli): 1 tombol Play di atas, 2 tombol + di Hierarchy untuk membuat objek, 3 Add Component di Inspector, 4 panel Project tempat Anda membuat skrip
Skema jendela Unity (bukan tangkapan layar asli): 1 tombol Play di atas, 2 tombol + di Hierarchy untuk membuat objek, 3 Add Component di Inspector, 4 panel Project tempat Anda membuat skrip

Yang Anda butuhkan

Langkah 1: buat proyek 2D

1. Buka Unity Hub, klik **New project**, lalu pilih templat **Universal 2D** (URP) atau **2D** (Built-in), sesuai nama yang tampil di Unity Hub.

2. Beri nama proyek, misalnya `MyPlatformer`, lalu klik **Create project**.

3. Tunggu sampai Unity terbuka. Anda akan melihat scene kosong bernama `SampleScene`.

Scene adalah area bermain game. Semua yang ada di dalamnya, seperti karakter, tanah, dan kamera, adalah **GameObject**, dan komponen yang dipasang pada setiap GameObject memberinya perilaku.

Skema Edit → Project Settings → Player → Active Input Handling (bukan tangkapan layar asli): 1 menu Edit, 2 Project Settings, 3 tab Player, 4 dropdown Active Input Handling
Skema Edit → Project Settings → Player → Active Input Handling (bukan tangkapan layar asli): 1 menu Edit, 2 Project Settings, 3 tab Player, 4 dropdown Active Input Handling

Langkah 2: atur input

Skrip dalam artikel ini memakai sistem input klasik (Input Manager). Untuk mengaktifkannya:

1. Buka **Edit → Project Settings → Player**.

2. Cari **Active Input Handling** dan pilih **Both** atau **Input Manager (Old)**.

3. Setujui permintaan Unity untuk memulai ulang editor.

Sumbu **Horizontal** (kiri dan kanan) dan tombol **Jump** (Spasi) sudah tersedia dalam pengaturan default. Kiri dan kanan memakai A/D atau tombol panah.

Langkah 3: buat tanah

1. Klik kanan di Hierarchy lalu pilih **2D Object → Sprites → Square**. Beri nama `Ground`.

2. Di Inspector, atur Scale menjadi `X = 20`, `Y = 1` agar menjadi lantai yang panjang.

3. Atur Position menjadi `X = 0`, `Y = -2`.

4. Klik **Add Component** dan tambahkan **Box Collider 2D**, agar karakter bisa berdiri di atasnya.

5. Buat layer bernama `Ground`: klik dropdown **Layer** di bagian atas Inspector, pilih **Add Layer**, lalu kembali ke `Ground` dan berikan layer `Ground` padanya.

Layer memungkinkan skrip mengenali apa yang disentuh kaki karakter, sehingga bisa membedakan lantai dari dinding atau musuh.

Langkah 4: tambahkan karakter

1. Klik kanan di Hierarchy lalu pilih **2D Object → Sprites → Square**. Beri nama `Player`.

2. Atur Position menjadi `X = 0`, `Y = 1` agar karakter mulai di atas tanah.

3. Jika Anda punya gambar karakter, seret berkas PNG ke kolom **Sprite** pada Sprite Renderer.

4. Tambahkan **Rigidbody 2D**, atur **Body Type** menjadi Dynamic, dan centang **Freeze Rotation Z** agar karakter tidak terguling.

5. Tambahkan **Box Collider 2D**.

Rigidbody 2D membuat objek jatuh karena gravitasi dan bertabrakan dengan objek lain. Tanpa komponen ini, karakter akan melayang di tempatnya.

Langkah 5: buat titik pemeriksa tanah

1. Klik kanan `Player` di Hierarchy lalu pilih **Create Empty**. Beri nama `GroundCheck`.

2. Seret ke kaki karakter, misalnya Position `X = 0`, `Y = -0.5`.

Skrip memakai titik ini sebagai pusat lingkaran kecil yang memeriksa apakah ada tanah di bawahnya.

Langkah 6: tulis pengendali karakter

Klik kanan di jendela Project lalu pilih **Create → C# Script**. Beri nama `PlayerController2D`, buka, lalu tempelkan kode ini.

using UnityEngine;

// ควบคุมตัวละคร 2D: เดินซ้ายขวา กระโดด และหันหน้าตามทิศทาง
// Mengendalikan karakter 2D: berjalan kiri-kanan, melompat, dan menghadap sesuai arah jalan
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(SpriteRenderer))]
public class PlayerController2D : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;      // ความเร็วเดิน / kecepatan jalan
    [SerializeField] private float jumpForce = 12f;     // แรงกระโดด / gaya lompat
    [SerializeField] private Transform groundCheck;     // จุดตรวจพื้น / titik pemeriksa tanah
    [SerializeField] private float groundRadius = 0.15f;
    [SerializeField] private LayerMask groundLayer;     // เลเยอร์ที่นับว่าเป็นพื้น / layer yang dianggap tanah

    private Rigidbody2D rb;
    private SpriteRenderer sprite;
    private float moveInput;
    private bool isGrounded;
    private bool jumpRequested;

    public bool IsGrounded => isGrounded;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        sprite = GetComponent<SpriteRenderer>();
    }

    // Update ทำงานทุกเฟรม ใช้อ่านปุ่มกด แต่ยังไม่กระโดดทันที
    // เพราะค่า isGrounded ถูกอัปเดตใน FixedUpdate ถ้ากระโดดใน Update อาจใช้สถานะพื้นเก่า
    // Update berjalan setiap frame untuk membaca tombol, tetapi belum melompat langsung,
    // karena isGrounded diperbarui di FixedUpdate. Melompat di Update bisa memakai status tanah yang lama.
    private void Update()
    {
        moveInput = Input.GetAxisRaw("Horizontal");   // -1 ซ้าย, 0 ไม่กด, 1 ขวา / -1 kiri, 0 tidak ditekan, 1 kanan

        if (Input.GetButtonDown("Jump"))
        {
            jumpRequested = true;                      // จำไว้ว่าผู้เล่นกดกระโดด / catat bahwa pemain menekan lompat
        }

        if (moveInput != 0)
        {
            sprite.flipX = moveInput < 0;             // หันหน้าไปทางที่เดิน / menghadap ke arah jalan
        }
    }

    // FixedUpdate ทำงานตามจังหวะฟิสิกส์ ตรวจพื้นก่อน แล้วค่อยตัดสินใจกระโดด
    // FixedUpdate berjalan mengikuti langkah fisika: periksa tanah dulu, baru putuskan melompat
    private void FixedUpdate()
    {
        // ถ้ายังไม่ได้ผูก groundCheck ใน Inspector ให้ใช้ตำแหน่งของตัวละครแทน
        // Jika groundCheck belum dihubungkan di Inspector, gunakan posisi karakter
        Vector2 checkPosition = groundCheck != null ? groundCheck.position : transform.position;
        isGrounded = Physics2D.OverlapCircle(checkPosition, groundRadius, groundLayer);

        float verticalSpeed = rb.velocity.y;
        if (jumpRequested && isGrounded)
        {
            verticalSpeed = jumpForce;
        }
        jumpRequested = false;                         // ใช้คำขอกระโดดแล้ว / permintaan lompat sudah dipakai

        rb.velocity = new Vector2(moveInput * moveSpeed, verticalSpeed);
    }
}

Poin-poin penting:

Skema Player Inspector (bukan tangkapan layar asli): 1 skrip PlayerController2D, 2 panel Rigidbody 2D dengan Freeze Rotation Z tercentang, 3 kolom Ground Check, 4 kolom Ground Layer
Skema Player Inspector (bukan tangkapan layar asli): 1 skrip PlayerController2D, 2 panel Rigidbody 2D dengan Freeze Rotation Z tercentang, 3 kolom Ground Check, 4 kolom Ground Layer

Langkah 7: hubungkan skrip

1. Seret skrip `PlayerController2D` ke `Player`.

2. Di Inspector, seret `GroundCheck` ke kolom **Ground Check**.

3. Atur **Ground Layer** menjadi `Ground`.

4. Atur **Move Speed** menjadi 5 dan **Jump Force** menjadi 12, atau pilih nilai yang Anda suka.

Langkah 8: buat kamera mengikuti karakter

Buat skrip bernama `CameraFollow2D` dan tempelkan kode ini.

using UnityEngine;

// กล้องติดตามตัวละครแบบนุ่มนวล และไม่ให้ตกเกินขอบล่างของฉาก
// Kamera mengikuti karakter dengan halus dan tidak turun melewati batas bawah scene
public class CameraFollow2D : MonoBehaviour
{
    [SerializeField] private Transform target;          // ตัวละครที่จะติดตาม / karakter yang diikuti
    [SerializeField] private float smoothTime = 0.2f;   // ยิ่งน้อยยิ่งตามทันเร็ว / makin kecil makin cepat menyusul
    [SerializeField] private float minY = -2f;          // ขอบล่างของกล้อง / batas bawah kamera

    private Vector3 velocity;

    private void LateUpdate()
    {
        if (target == null) return;

        Vector3 desired = new Vector3(target.position.x, Mathf.Max(target.position.y, minY), transform.position.z);
        transform.position = Vector3.SmoothDamp(transform.position, desired, ref velocity, smoothTime);
    }
}

`LateUpdate` berjalan setelah semua pemanggilan Update, cocok untuk kamera yang harus mengikuti karakter yang baru bergerak pada frame itu. `Vector3.SmoothDamp` membuat gerakan halus, tidak tersentak.

Pilih **Main Camera**, tambahkan `CameraFollow2D`, seret `Player` ke kolom **Target**, dan atur posisi Z kamera menjadi `-10` agar semua objek di scene berada di depannya.

Langkah 9: jalankan game

Tekan **Play** di bagian atas editor dan coba:

Tekan Play lagi untuk berhenti. Nilai yang diubah di Inspector saat bermain akan kembali ke semula ketika Anda berhenti, jadi lakukan penyetelan saat game sedang berhenti, lalu simpan proyek.

Langkah 10: tulis pengujian otomatis

Mencoba secara manual cukup untuk game kecil, tetapi pengujian otomatis menangkap kesalahan lebih cepat ketika kode bertambah besar. Unity punya Unity Test Framework sendiri yang layak dipelajari. Artikel ini memakai pendekatan yang lebih sederhana: ia menirukan loop frame Unity dengan C# biasa, sehingga Anda bisa menguji logika skrip tanpa membuka editor.

Tes di bawah memanggil Update dan FixedUpdate secara berurutan, mengatur input dan status tanah, memindahkan objek sesuai kecepatannya, lalu memeriksa hasilnya.

using System;

public static class PlayerTests
{
    private const float Dt = 1f / 50f;   // 1 FixedUpdate sama dengan 1/50 detik / 1 FixedUpdate = 1/50 detik
    private static int passed = 0, failed = 0;

    private static void Check(string name, bool condition)
    {
        if (condition) { passed++; Console.WriteLine("PASS  " + name); }
        else { failed++; Console.WriteLine("FAIL  " + name); }
    }

    private static PlayerController2D CreatePlayer(out Rigidbody2D rb, out SpriteRenderer sprite)
    {
        var player = new PlayerController2D();
        rb = new Rigidbody2D();
        sprite = new SpriteRenderer();
        player.Components[typeof(Rigidbody2D)] = rb;
        player.Components[typeof(SpriteRenderer)] = sprite;
        player.Call("Awake");
        return player;
    }

    private static void Step(PlayerController2D player, Rigidbody2D rb)
    {
        player.Call("Update");
        player.Call("FixedUpdate");
        player.transform.position = new Vector3(
            player.transform.position.x + rb.velocity.x * Dt,
            player.transform.position.y + rb.velocity.y * Dt, 0);
    }

    public static int Main()
    {
        // 1. Tahan kanan lalu berjalan ke kanan / Hold right and walk right
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = 1f;
            for (int i = 0; i < 50; i++) Step(player, rb);
            Check("walk right: x velocity equals moveSpeed (5)", Math.Abs(rb.velocity.x - 5f) < 0.001f);
            Check("walk right: x position moves about 5 units per second", Math.Abs(player.transform.position.x - 5f) < 0.2f);
            Check("walk right: faces right (flipX = false)", sprite.flipX == false);
        }

        // 2. Tahan kiri dan menghadap kiri / Hold left and face left
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = -1f;
            Step(player, rb);
            Check("walk left: x velocity is negative", rb.velocity.x < 0);
            Check("walk left: faces left (flipX = true)", sprite.flipX == true);
        }

        // 3. Tanpa input karakter tetap diam / No input means the character stays still
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = 0f;
            Step(player, rb);
            Check("no input: x velocity is 0", rb.velocity.x == 0f);
        }

        // 4. Lompat dari tanah / Jump from the ground
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = 0f;
            Step(player, rb);                    // biarkan FixedUpdate memeriksa tanah dulu / let FixedUpdate check the ground first
            Input.JumpPressedThisFrame = true;
            Step(player, rb);
            Input.JumpPressedThisFrame = false;
            Check("jump on ground: y velocity equals jumpForce (12)", Math.Abs(rb.velocity.y - 12f) < 0.001f);
        }

        // 5. Menekan lompat di udara tidak boleh melompat lagi / Pressing jump in mid-air must not jump again
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = 0f;
            Step(player, rb);
            Physics2D.GroundBelowPlayer = false; // karakter sudah meninggalkan tanah / the character has left the ground
            rb.velocity = new Vector2(0, -3f);   // sedang jatuh / falling
            Input.JumpPressedThisFrame = true;
            Step(player, rb);
            Input.JumpPressedThisFrame = false;
            Check("jump in air: y velocity unchanged (still falling)", Math.Abs(rb.velocity.y + 3f) < 0.001f);
        }

        Console.WriteLine();
        Console.WriteLine("Passed " + passed + " / Failed " + failed);
        return failed == 0 ? 0 : 1;
    }
}

Berkas stub yang disebut di versi Thailand artikel ini (`UnityStubs.cs`) menyediakan `Vector2`, `Rigidbody2D`, `Input`, `Physics2D`, dan tipe terkait lainnya, sehingga tes berjalan di luar Unity. Stub tidak menyimulasikan fisika atau rendering yang sebenarnya. Stub hanya memeriksa perhitungan dan urutan eksekusi skrip, dan itulah tujuan tes ini. Kompilasi keempat berkas bersama dengan kompiler C# terbaru, lalu jalankan executable-nya. Pada skrip yang dipakai di Unity, pertahankan `using UnityEngine;`. Hapus baris itu hanya pada salinan yang dipakai untuk tes stub.

Hasil menjalankan tes

Dikompilasi dengan kompiler C# Roslyn dan dijalankan, keluarannya:

PASS  walk right: x velocity equals moveSpeed (5)
PASS  walk right: x position moves about 5 units per second
PASS  walk right: faces right (flipX = false)
PASS  walk left: x velocity is negative
PASS  walk left: faces left (flipX = true)
PASS  no input: x velocity is 0
PASS  jump on ground: y velocity equals jumpForce (12)
PASS  jump in air: y velocity unchanged (still falling)

Passed 8 / Failed 0

Bug yang ditemukan tes

Versi pertama pengendali melompat langsung di dalam Update. Tes "jump in air" gagal: Update membaca `isGrounded` dari frame sebelumnya, yang masih bernilai true, sehingga karakter melompat lagi saat sudah di udara. Dalam game sungguhan, ini terjadi ketika karakter berjalan melewati tepi platform. Perbaikannya adalah menyimpan permintaan lompat di `jumpRequested` dan membiarkan FixedUpdate memeriksa tanah serta memutuskan melompat pada langkah yang sama. Itulah versi di langkah 6.

Inilah alasan pengujian otomatis penting: kesalahan ini mudah terlewat jika diperiksa dengan mata, tetapi tes yang jelas dan mencakup kasus tepi langsung menemukannya.

Langkah 11: build game

1. Buka **File → Build Profiles** (disebut **Build Settings** di versi lama).

2. Pilih platform Anda, misalnya Windows atau WebGL, lalu klik **Build**.

3. Pilih folder. Unity membuat berkas `.exe` atau build web yang bisa dimainkan orang lain.

Masalah umum dan solusinya

Langkah berikutnya

Setelah karakter bisa berjalan dan melompat, Anda bisa menambahkan koin dengan Trigger Collider 2D dan `OnTriggerEnter2D`, musuh yang berpatroli, kesehatan dan kematian, serta latar belakang yang dibuat dengan Tilemap. Semuanya memakai gagasan yang sama dengan artikel ini.

Ringkasan

Membuat game 2D di Unity dimulai dengan memahami bahwa semua hal adalah GameObject yang tersusun dari komponen. Pengendali membaca input di Update dan menangani fisika di FixedUpdate, dan pengujian otomatis menangkap kesalahan yang sulit terlihat dengan mata. Kerjakan langkah demi langkah, sesuaikan nilai di Inspector, dan buat game itu menjadi milik Anda.