Setiap tutorial pengembangan game dimulai dengan cara yang sama: persegi panjang berwarna yang bergeser di atas kanvas abu-abu. Itu tidak masalah untuk mempelajari sintaksis, tetapi tidak mengajarkan Anda apa pun tentang bagaimana mesin game sebenarnya bekerja. Saya ingin keluar dari urusan persegi panjang. Saya ingin membangun sesuatu yang terasa seperti game sungguhan—sebuah dungeon crawler top-down dengan pergerakan, pertarungan, HUD, dan suara.

Saya berkomitmen menggunakan Phaser v4 dan menetapkan satu aturan keras bagi diri saya sendiri. Nol aset eksternal. Tidak ada file gambar, tidak ada klip audio, dan tidak ada alat build seperti Webpack atau Vite. Seluruh proyek harus berada di dalam satu file HTML tunggal yang ditulis dengan JavaScript murni. Batasan tersebut bukan tentang minimalisme semata. Itu tentang menghilangkan setiap alasan dan setiap black box. Ketika Anda tidak dapat mengunduh paket sprite untuk menutupi celah dalam pengetahuan Anda, Anda terpaksa mempelajari bagaimana engine mengelola tekstur, animasi, audio, dan state di balik layar.

Menggambar Dunia dari Kode

Dalam proyek Phaser normal, Anda memanggil this.load.image() di dalam fungsi preload dan mengarahkan engine ke file PNG. Tanpa opsi tersebut, Anda beralih ke objek Graphics. Anda menginstansiasinya, menggambar bentuk primitif—persegi panjang untuk ubin lantai, garis yang lebih tebal untuk dinding, mungkin lingkaran untuk token pemain—dan kemudian memanggil generateTexture. Metode tersebut menangkap buffer grafis dan mendaftarkannya ke manajer tekstur Phaser dengan kunci yang Anda pilih.

Sejak saat itu, engine memperlakukan bitmap yang dihasilkan tersebut persis seperti file gambar yang dimuat. Anda dapat menetapkannya ke tilemap, memotongnya menjadi sprite, atau memberinya warna (tint). Untuk dungeon crawler ini, artinya saya dapat menghasilkan grid lantai secara prosedural, menempatkan segmen dinding, dan melakukan iterasi pada palet warna tanpa pernah meninggalkan editor kode saya. Pelajaran praktisnya adalah bahwa tekstur hanyalah sekumpulan data bitmap yang tersimpan di memori. Phaser tidak peduli apakah data tersebut datang melalui permintaan HTTP atau panggilan Graphics yang ditulis secara manual.

Pendekatan ini juga membuat Anda berpikir secara sengaja tentang urutan gambar (draw order) dan batching. Ketika setiap dinding dan ubin lantai berasal dari keluarga tekstur yang dihasilkan yang sama, Anda mulai memperhatikan bagaimana Phaser mengelompokkan panggilan render. Anda menyadari perbedaan antara lapisan tilemap statis dan spritemap dari objek individual karena Anda secara manual memutuskan mana yang layak ada sebagai instansi tekstur.

Animasi Tanpa Spritesheets

Kotak statis cepat terasa membosankan, tetapi sistem animasi Phaser mengharapkan sebuah spritesheet—biasanya satu file PNG dengan frame yang disusun dalam grid. Saya mereplikasi strip tersebut menggunakan elemen HTML canvas offscreen. Untuk setiap frame animasi, saya membersihkan canvas dan menggambar pose baru: ayunan pedang sederhana, siklus jalan dua langkah, atau gerakan diam (idle) musuh. Setelah strip selesai, saya mendaftarkannya ke Phaser sebagai spritesheet, menentukan lebar dan tinggi frame agar engine tahu di mana satu pose berakhir dan pose berikutnya dimulai.

Melakukan ini secara manual mengungkapkan dengan tepat apa itu animasi di balik permukaannya: sekumpulan batas frame pada tekstur yang sama. Komponen animasi Phaser meminta frame awal, frame akhir, dan frame rate. Ia kemudian memajukan pointer melalui potongan-potongan persegi panjang tersebut pada setiap detak jam game. Anda berhenti menganggap spritesheet sebagai aset ajaib yang dihasilkan oleh seniman dan mulai melihatnya sebagai matematika koordinat. Perspektif ini sangat berharga nantinya saat Anda memotong seni asli atau men-debug mengapa sebuah animasi memutar frame secara tidak berurutan.

Suara dari Ketiadaan

File audio dilarang, jadi saya menggunakan Web Audio API secara langsung. Beberapa baris JavaScript dapat membuat node oscillator, mengaturnya ke gelombang kotak (square wave) atau sinus, menyalurkannya melalui node gain, dan menjadwalkan ledakan suara singkat. Saya menulis helper kecil untuk peristiwa umum: bunyi bip frekuensi rendah untuk langkah kaki, bunyi kicauan yang meningkat untuk mengambil loot, dan nada gelombang kotak yang kasar untuk saat menerima damage.

Suara-suara sintetis ini dirancang sebagai placeholder, tetapi mereka memberikan umpan balik mekanis ke game secara instan. Anda dapat merasakan apakah timing-nya sudah tepat sebelum Anda memutuskan untuk merekam atau mencari audio asli. Hasil nyata yang didapat adalah pada arsitekturnya. Karena Anda membungkus pemicu suara dalam fungsi sederhana, mengganti bunyi bip sintetis dengan buffer suara yang dimuat nantinya hanya membutuhkan satu baris perubahan. Sisa dari game tersebut—peristiwa tabrakan (collision), kilatan UI, penambahan skor—tetap tidak tersentuh. Anda memprotipekan rasanya terlebih dahulu, baru kemudian meningkatkan kualitasnya.

Mengelola Banyak Dunia

A real game needs more than one screen, so I split the project into separate Phaser scenes: Menu, Game, UI, and Pause. The UI scene runs in parallel with the Game scene, launched simultaneously so the health bar and score counter live in their own sandbox while the dungeon crawls below. They communicate strictly through events. When the player takes damage, the Game scene emits a change. The UI scene listens and updates its text objects. The Game scene does not import the UI, call its methods, or even check if it exists. It simply sends data into the void. That decoupling means you can yank the HUD out for testing, or replace it entirely, without touching the core game loop.

For pausing, I used a stacked Pause scene that sits on top of the Game scene. Crucially, calling scene.pause() on the Game scene actually freezes the physics world and halts timers. The Game scene stops updating, but the Pause scene remains awake to render a menu and wait for an unpause signal. If you have only ever managed pause states with a boolean flag inside one giant update loop, this feels like discovering a light switch. The engine gives you a real pause lifecycle rather than forcing you to pepper your code with if (isPaused) return guards.

Hard Lessons from Real Bugs

Working this close to the metal exposed two habits I needed to change.

First, I tried to use a method in Phaser v4 that looked public but was not part of the documented API. It changed between versions and broke my build. I refactored to use getChildren(), which is a stable, documented public method, and the instability vanished. The lesson is blunt: if a method is not in the official documentation, do not build your game on it. Internal APIs are internal for a reason. Stick to the public surface area and your project will survive engine updates.

Second, I learned never to trust events for everything. Event callbacks work perfectly well for low-stakes interactions like picking up a coin or opening a chest. But for critical state transitions—especially Game Over—I added a redundant check inside the main update loop. Events can misfire if a listener is removed, a scene pauses at an awkward microsecond, or a race condition sneaks in between emission and handling. By checking the player’s health directly in the update loop and forcing the game-over state if it hits zero, I ensured the game could never get stuck in a limbo state if an event failed to fire. The events still handle the secondary effects—screen shake, sound cues, score submission—but the authoritative logic lives where the game clock lives.

Why You Should Try This

If you are learning game development, impose this exact constraint on your next project: no external assets, one HTML file. It sounds restrictive, but it removes every excuse. You do not need to configure a bundler, wrestle with CORS errors on local audio files, or spend an afternoon curating free asset packs. You write code, you refresh the browser, and you see results.

More importantly, you will understand why the engine behaves the way it does. You will know how a texture enters the GPU because you called generateTexture. You will know how animation frames are indexed because you registered the boundaries by hand. You will know how audio reaches the speakers because you wired the oscillator. That knowledge transfers directly to larger projects that do use external assets, because the underlying mechanics never change—the engine