Di episode ini kita membedah arsitektur proyek Godot yang matang: pola ECS-inspired untuk entity management, decoupled systems berbasis signal, event bus sebagai messaging global, scene modular berbasis plugin, serta mekanik game yang bisa dipakai ulang antar proyek.

Di episode 17 kalian sudah belajar mengemas proyek ke Android, iOS, dan desktop, lengkap dengan penyesuaian input dan tuning performa untuk tiap platform. Nah, saat game kalian mulai di-release ke banyak perangkat, satu pertanyaan akan datang lebih cepat dari yang kalian bayangkan: bagaimana proyek ini bisa bertahan saat fitur terus bertambah?
Satu file script berisi 800 baris masih terasa "aman" di minggu pertama. Setelah tiga bulan, setiap perubahan berisiko memecah sesuatu yang tidak kalian ingat pernah kalian sentuh. Episode 18 ini menjawabnya lewat arsitektur: pola ECS-inspired untuk entity management, sistem yang saling terlepas lewat signal, event bus sebagai messaging global, scene modular, dan mekanik yang dirancang untuk dipakai ulang.
Godot menganut composition over inheritance — kalau di OOP klasik kita menurunkan class, di Godot kita menyusun node. Scene raksasa yang menampung semuanya, dari movement sampai UI skor, adalah sumber utama sakit kepala. Solusinya: pecah menjadi komponen kecil yang masing-masing punya satu tanggung jawab.
Ambil contoh sebuah player. Alih-alih satu node dengan script 500 baris, buatlah hierarki seperti ini:
Player (CharacterBody2D)
├── Health (HealthComponent)
├── Movement (MovementComponent)
├── Visual
│ └── Sprite2D
└── CollisionShape2DScript utama hanya mengoordinasikan komponen, bukan menangani semua logika:
extends CharacterBody2D
@export var move_speed: float = 220.0
@onready var health: HealthComponent = $Health
@onready var visual: Sprite2D = $Visual/Sprite2D
func _physics_process(delta: float) -> void:
var direction := Input.get_axis("move_left", "move_right")
velocity.x = direction * move_speed
move_and_slide()Kunci di sini adalah @export — nilai yang bisa diubah dari editor tanpa menyentuh kode. Komponen HealthComponent bisa dipasang ke player, musuh, ataupun kuda; tinggal atur nilai max_health di inspector. Itulah kenapa komponen membuat proyek tetap kecil bahkan saat mekaniknya membesar.
ECS murni (Entity-Component-System) jarang menjadi pilihan di Godot karena sistemnya sendiri sudah berbasis node. Tapi prinsip intinya — entitas itu data, perilaku datang dari komponen — bisa kita tiru dengan pola komponen node. Entitas adalah node kosong, komponen adalah child node yang membawa data dan perilaku kecil.
Untuk entitas yang muncul terus-menerus, seperti musuh yang di-spawn, gunakan object pooling. Menghancurkan dan membuat node terus-menerus memicu alokasi memori yang berisiko stutter:
extends Node
var pool: Array[Node] = []
var enemy_scene: PackedScene = preload("res://enemies/enemy.tscn")
func spawn() -> Node:
for item in pool:
if not item.is_inside_tree():
return item
var enemy := enemy_scene.instantiate()
pool.append(enemy)
add_child(enemy)
return enemyEntitas yang mati kita sembunyikan, bukan hancurkan. Saat spawn berikutnya, entitas itu dipakai ulang dari pool. Hasilnya: zero allocation di saat-saat paling sibuk, dan gameplay tetap mulus di ponsel sekalipun.
Signal adalah bahasa pemersatu antar node di Godot. Kekuatannya: pengirim tidak perlu tahu siapa penerimanya. Musuh cukup berteriak died saat kesehatannya habis; entah itu score UI, audio manager, atau quest system yang mendengarkan — semua tidak saling mengenal.
extends CharacterBody2D
signal died
@export var max_health: int = 3
var health: int
func _ready() -> void:
health = max_health
func take_damage(amount: int) -> void:
health -= amount
if health <= 0:
died.emit()Di sisi lain, sistem yang menambahkan skor tinggal menghubungkan signal died ke metodenya sendiri:
extends Node
func _ready() -> void:
var enemy := get_tree().get_first_node_in_group("enemies")
enemy.died.connect(_on_enemy_died)
func _on_enemy_died() -> void:
ScoreManager.add(10)Perhatikan bahwa ScoreManager tidak pernah dipanggil langsung dari musuh. Kalau besok musuh menambah dua nyawa saat mati, kita cukup menghubungkan signal ke sistem lain — tanpa mengubah satu baris pun di enemy.gd.
Terkadang signal per-node tidak cukup — beberapa peristiwa bersifat global: game over, skor berubah, level berganti. Solusinya adalah event bus: autoload sederhana yang hanya berisi signal, didaftarkan lewat Project Settings > Autoload.
extends Node
signal score_changed(points: int)
signal player_hurt(health: int, maximum: int)
signal game_overSekarang sistem mana pun bisa mengirim pesan ke bus:
func _on_enemy_died() -> void:
EventBus.score_changed.emit(10)Dan sistem lain mendengarkan di satu titik:
func _ready() -> void:
EventBus.score_changed.connect(_on_score_changed)
func _on_score_changed(points: int) -> void:
score_label.text = "Skor: %d" % ScoreManager.totalBus ini bukan tempat sampah untuk semua signal — hanya untuk peristiwa lintas sistem. Kalau dua node selalu berpasangan (misalnya health bar dan komponen health), sambungkan langsung tanpa bus. Kebijakan kecil ini mencegah bus berubah menjadi spaghetti raksasa.
Info
Beri nama signal yang menunjukkan kejadian lampau (past tense) seperti died, score_changed, level_loaded. Ini kebiasaan kecil yang membuat pembaca kode langsung paham bahwa signal menandakan sesuatu yang sudah terjadi, bukan perintah yang harus dijalankan.
Mekanik yang baik bukan fitur satu game, melainkan sistem yang bisa dipindah ke game lain. Pola yang paling ampuh di Godot: feature scene + exported configuration. Buat mekanik sebagai scene mandiri, lalu konfigurasikan lewat inspector.
Contoh sistem interaksi yang bisa dipakai di mana saja:
extends Area2D
@export var prompt: String = "Press E"
func _ready() -> void:
body_entered.connect(func(_body): show_prompt())
body_exited.connect(func(_body): hide_prompt())Dengan @export, prompt bisa diubah per-instance. Scene ini disimpan di folder systems/ dan di-instance ke pintu, switch, atau item. Mekanik "pickup", "damage zone", dan "checkpoint" semua mengikuti pola yang sama. Setelah beberapa proyek, kalian akan punya perpustakaan sistem yang tinggal disusun ulang seperti Lego — itu kekuatan sebenarnya dari arsitektur yang baik.
Pola plugin-based scenes melangkah lebih jauh: selain scene reusable, Godot punya mekanisme plugin (addon) yang memuat fitur penuh dengan satu tombol — dari inspector kustom sampai panel editor. Buat mekanik kalian sebagai scene mandiri di folder addons/, dan fitur itu bisa dihidupkan atau dimatikan per proyek tanpa mengubah kode inti. Kebijakan refactornya konsisten: petakan satu tanggung jawab per blok kode, pindahkan ke komponen node, hubungkan dengan signal, lalu uji ulang. Kerjakan satu mekanik per sesi kerja — jangan semua sekaligus.
Episode 18 membekali kalian dengan empat pilar arsitektur Godot: scene modular yang memecah monolit menjadi komponen, pola ECS-inspired dengan object pooling untuk entity management, decoupled systems lewat signal, event bus untuk messaging global, dan feature systems yang bisa dipakai ulang antar proyek.
Inti yang harus dibawa pulang:
@export agar konfigurasi hidup di inspector.@export, dan pindahkan yang matang ke folder addons/ agar bisa dipakai ulang di proyek lain.Arsitektur yang baik tidak membuat game lebih cepat selesai minggu ini — ia membuat proyek kalian bisa bertahan sampai rilis dan seterusnya. Di episode 19 berikutnya kita beranjak dari menulis kode ke Operational Readiness: build pipeline untuk rilis, QA dan regression testing, strategi packaging dan distribusi, hingga analytics dan crash reporting. Sampai jumpa!