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.
Yang Anda butuhkan
- Komputer dengan **Unity Hub**, dari unity.com
- Unity Editor versi LTS terbaru (misalnya Unity 6 LTS atau 2022 LTS), dipasang melalui Unity Hub
- VS Code atau Visual Studio. Unity Hub dapat menawarkan pemasangan integrasinya saat Anda membuat proyek
- Gambar karakter kecil dalam format PNG. Jika tidak punya, kotak berwarna yang dibuat Unity sudah cukup
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.
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:
- `[SerializeField]` menampilkan field privat di Inspector, sehingga Anda bisa menyetel nilai tanpa mengedit kode.
- `Input.GetAxisRaw("Horizontal")` mengembalikan -1, 0, atau 1, sehingga arahnya jelas.
- `Input.GetButtonDown("Jump")` bernilai true hanya pada frame saat tombol ditekan, sehingga menahan Spasi tidak mengulang lompatan.
- `Physics2D.OverlapCircle` memeriksa apakah ada collider pada layer tanah yang bersinggungan dengan lingkaran berjari-jari `groundRadius` di sekitar titik pemeriksa tanah.
- Update membaca input dan FixedUpdate menjalankan fisika. Jika lompatan terjadi di dalam Update, karakter bisa melompat tepat setelah berjalan melewati tepi, karena `isGrounded` masih bernilai lama. Menyimpan permintaan lompat lalu menjalankannya di FixedUpdate mencegah hal ini, seperti yang ditunjukkan oleh tes di langkah 10.
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:
- Tombol A atau panah kiri: karakter berjalan ke kiri dan menghadap kiri.
- Tombol D atau panah kanan: karakter berjalan ke kanan dan menghadap kanan.
- Spasi saat di tanah: karakter melompat.
- Spasi saat di udara: karakter tidak melompat lagi.
- Kamera mengikuti dengan halus.
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
- **Karakter tidak bergerak:** periksa pengaturan Active Input Handling, dan pastikan Body Type Rigidbody 2D adalah Dynamic.
- **Karakter jatuh menembus tanah:** pastikan tanah memiliki Box Collider 2D, dan Rigidbody 2D karakter tidak berstatus Kinematic.
- **Karakter tidak bisa melompat:** pastikan Ground Layer diatur ke `Ground` dan GroundCheck berada di kaki karakter.
- **Karakter terguling:** centang Freeze Rotation Z pada Rigidbody 2D.
- **Kamera tidak mengikuti:** pastikan `Player` ada di kolom Target pada `CameraFollow2D`.
- **Muncul peringatan bahwa velocity sudah usang:** Unity 6 menyediakan `linearVelocity`. Jika versi Anda memberi peringatan untuk `velocity`, ganti namanya menjadi `linearVelocity`.
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.