Membuat musuh yang terasa hidup: state machine untuk perilaku, pathfinding dengan algoritma A*, dan behavior trees untuk AI yang kompleks, lalu praktik membangun enemy AI patrol-menyerang untuk Rimba Runner

Setelah audio membuat dunia terasa hidup, saatnya menghidupkan penduduknya: game AI. Musuh yang berjalan maju-mundur tanpa henti membuat game terasa datar; musuh yang "berpikir" — berpatroli, mendeteksi, mengejar, menyerang — membuat pemain terlibat. AI musuh adalah contoh nyata bagaimana ilmu komputer klasik (state machine, graph search) bekerja langsung di game.
Episode ini membangun enemy AI Rimba Runner dengan tiga teknik bertingkat: state machine sebagai kerangka perilaku, pathfinding A* agar musuh bisa mencari jalan, dan behavior tree untuk AI yang lebih kompleks. Akhirnya kita rakit musuh yang bisa berpatroli dan mengejar pemain.
Kita sudah mengenal state machine di episode 2 (game state) dan 8 (menu). Sekarang terapkan ke musuh. Setiap musuh punya state: PATROL, CHASE, ATTACK, DEAD. Transisi dipicu oleh kondisi:
Implementasi Godot — satu script dengan pola state yang rapi:
enum State { PATROL, CHASE, ATTACK, DEAD }
var state: State = State.PATROL
var player: Node2D
func _physics_process(delta: float) -> void:
match state:
State.PATROL:
patrol(delta)
if can_see_player():
state = State.CHASE
State.CHASE:
chase(delta)
if not can_see_player():
state = State.PATROL
if distance_to_player() < attack_range:
state = State.ATTACK
State.ATTACK:
attack()
state = State.CHASEmatch di GDScript setara switch di bahasa lain. Kunci pola ini: setiap state hanya mengurus logikanya sendiri, transisi eksplisit dan mudah dilacak. Tidak ada spaghetti if berantai yang sulit di-debug.
State machine menentukan kapan musuh mengejar; pathfinding menentukan bagaimana ia mencapai pemain. Algoritma standarnya adalah A* — pencarian jalur terpendek pada grid/graph yang memprioritaskan node yang paling menjanjikan (dengan heuristic jarak ke tujuan).
Konsep inti A*:
f = g + h — g = biaya dari titik awal, h = perkiraan jarak ke tujuan (heuristic).f terkecil sampai mencapai tujuan.Untungnya, kalian hampir tidak perlu menulis A* sendiri. Godot punya NavigationAgent2D + NavigationRegion2D; Unity punya NavMesh + NavMeshAgent; Unreal punya NavMesh + AIMoveTo. Alur pemakaiannya sama: buat navigation region/mesh, pasang agent, lalu beri target.
@onready var nav: NavigationAgent2D = $NavigationAgent2D
func chase(delta: float) -> void:
nav.target_position = player.global_position
if not nav.is_navigation_finished():
velocity = (nav.get_next_path_position() - global_position).normalized() * speed
move_and_slide()Tapi memahami A* tetap wajib, karena: (1) saat game butuh custom pathfinding (misal grid yang berubah dinamis), (2) untuk memilih heuristic yang tepat, dan (3) untuk wawancara kerja — A* adalah pertanyaan klasik. Untuk pendalaman visual yang luar biasa, buka artikel Red Blob Games: Introduction to the A* Algorithm.
State machine jadi rumit saat musuh punya banyak perilaku. Behavior tree menjawabnya dengan pohon tugas (task) berhirarki yang dievaluasi dari akar ke daun tiap frame. Saya bongkar istilah kuncinya:
Struktur behavior tree musuh Rimba Runner:
Root
└── Selector
├── Sequence [Pemain terlihat? → Serang]
│ ├── Check: can_see_player
│ └── Action: chase & attack
└── Sequence [Patrol biasa]
├── Check: has_patrol_point
└── Action: move to patrol pointKeunggulan behavior tree: ekspansi mudah (tambah cabang, bukan rombak logika), bisa divisualisasikan, dan mudah dibagikan ke designer. Tools seperti Behavior Tree Creator (Unity) atau plugin behavior tree di Godot mengubahnya jadi node visual. Untuk AI yang benar-benar kompleks — bos dengan banyak fase, NPC yang punya rutinitas harian — behavior tree adalah pilihan standar industri.
Tip
Jangan buru-buru pakai behavior tree untuk musuh kecil. Rule of thumb: state machine untuk musuh dengan 3-6 perilaku, behavior tree saat perilaku mulai bercabang banyak. Memulai dari yang sederhana menghindarkan kalian dari arsitektur berlebihan.
Rakit musuh pertama: Slime — makhluk hutan yang berpatroli, dan mengejar pemain saat terlihat. Langkah:
CharacterBody2D + Sprite2D + CollisionShape2D.NavigationRegion2D di level, bake region yang bisa dilalui musuh.PATROL → CHASE dengan NavigationAgent.RayCast2D (los) atau jarak + zone: pastikan musuh tidak menembus dinding saat mengejar.[x] Slime dengan state machine PATROL/CHASE
[x] NavigationRegion2D di-bake di level
[x] RayCast2D mendeteksi pemain (tidak tembus dinding)
[x] Musuh mengejar, kembali patrol saat pemain jauhKalian sekarang bisa membuat musuh yang terasa hidup.
Inti yang harus dibawa pulang:
Di episode 11 selanjutnya kita akan mengunci progres pemain: save, data & progression — save system, game data, dan progression design, lengkap dengan praktik membangun sistem penyimpanan untuk Rimba Runner. Sampai jumpa di episode 11!