Kila mafunzo ya uundaji wa michezo huanza kwa njia ile ile: mstatili wenye rangi unaoteleza kwenye turubai ya kijivu. Hiyo ni sawa kwa ajili ya kujifunza sintaksi, lakini haikufundishi chochote kuhusu jinsi injini ya mchezo inavyofanya kazi kihalisia. Nilitaka kuacha mambo ya mstatili. Nilitaka kujenga kitu ambacho kingehisi kama mchezo halisi—mchezo wa kutafuta njia kwenye gereza (top-down dungeon crawler) wenye mwendo, mapambano, HUD, na sauti.
Niliamua kutumia Phaser v4 na nikajiwekea sheria moja kali. Hakuna rasilimali (assets) za nje. Hakuna faili za picha, hakuna vipande vya sauti, na hakuna zana za ujenzi (build tools) kama Webpack au Vite. Mradi mzima ulikuwa lazima uishi ndani ya faili moja la HTML lililoandikwa kwa JavaScript ya kawaida. Kizuizi hicho hakikuwa kuhusu upunguzaji (minimalism) kwa ajili yake tu. Lilikuwa kuhusu kuondoa kila kisingizio na kila "black box". Unaposhindwa kupakua pakiti ya sprite ili kufunika pengo katika ujuzi wako, unalazimika kujifunza jinsi injini inavyosimamia texture, michoro (animations), sauti, na hali (state) ndani yake.
Kuchora Ulimwengu Kutoka kwenye Code
Katika mradi wa kawaida wa Phaser, unaita this.load.image() ndani ya kazi ya preload na kuielekeza injini kwenye faili la PNG. Bila chaguo hilo, unageukia kitu cha Graphics. Unakifanya kianze (instantiate), unachora maumbo ya msingi—mstatili kwa ajili ya vigae vya sakafu, mistari minene kwa ajili ya kuta, labda duara kwa ajili ya alama ya mchezaji—na kisha unaita generateTexture. Njia hiyo inakamata buffer ya michoro na kuirekodi kwenye meneja wa texture wa Phaser chini ya jina (key) unalochagua.
Kuanzia wakati huo, injini inachukulia bitmap hiyo iliyotengenezwa kama faili la picha lililopakuliwa. Unaweza kuipatia tilemaps, kuigawa katika sprites, au kuibadilisha rangi (tint). Kwa mchezo wa dungeon crawler, hii ilimaanisha kuwa ningeweza kutengeneza gridi ya sakafu kwa njia ya kiprogramu (procedurally), kuweka vipande vya kuta, na kubadilisha rangi bila hata kuondoka kwenye edita yangu ya code. Somo la kivitendo ni kwamba texture ni kipande tu cha data ya bitmap kilichopo kwenye kumbukumbu (memory). Phaser haijali kama imefika kupitia ombi la HTTP au kupitia wito wa Graphics uliowekwa kwa mkono.
Mtindo huu pia unakulazimisha kufikiria kwa makini kuhusu mpangilio wa kuchora (draw order) na uunganishaji (batching). Unapokuwa na kila ukuta na vigae vya sakafu vinavyotoka katika familia moja ya texture zilizotengenezwa, unaanza kuzingatia jinsi Phaser inavyokusanya wito wa uchoraji (render calls). Unagundua tofauti kati ya tabaka ya tilemap tuli na spritemap ya vitu binafsi kwa sababu unaamua mwenyewe ni kipi kinastahili kuwepo kama mfano wa texture.
Kuunda Michoro (Animations) Bila Spritesheets
Mstatili tuli huchosha haraka, lakini mfumo wa michoro wa Phaser unatarajia spritesheet—kawaida ni PNG moja yenye fremu zilizopangwa kwenye gridi. Nilirudia mfululizo huo kwa kutumia kipengele cha HTML canvas kilichopo nje ya skrini (offscreen). Kwa kila fremu ya michoro, nilisafisha canvas na kuchora mkao mpya: mzunguko rahisi wa upanga, mzunguko wa hatua mbili wa kutembea, au mchezo wa adui anaposubiri. Mara tu mfululizo huo ulipokamilika, niliurekodi kwenye Phaser kama spritesheet, nikifafanua upana na urefu wa fremu ili injini ijue pale mkao mmoja unapoishia na mwingine unapoanza.
Kufanya hivi kwa mkono kunafichua kile ambacho michoro ni hasa chini ya uso: seti ya mipaka ya fremu kwenye texture inayoshirikiwa. Kitu cha michoro cha Phaser kinahitaji fremu ya kuanzia, fremu ya mwisho, na kasi ya fremu (frame rate). Kisha kinasogeza kiashiria (pointer) kupitia vipande hivyo vya mstatili katika kila mzunguko wa saa ya mchezo. Unaacha kufikiria spritesheets kama rasilimali za ajabu zinazotengenezwa na wasanii na unaanza kuziona kama hesabu ya kuratibu (coordinate math). Mtazamo huo ni wa thamani kubwa baadaye unapokuwa unakata sanaa halisi au unatafuta kosa (debugging) la kwa nini michoro inacheza fremu kwa mpangilio usio sahihi.
Sauti Kutoka Kwenye Kitu Chochote
Faili za sauti zilikuwa zimepigwa marufuku, hivyo nilitumia Web Audio API moja kwa moja. Mistari michache ya JavaScript inaweza kutengeneza oscillator node, kuiweka kwenye mawimbi ya square au sine, kuipitisha kupitia gain node, na kupanga mlipuko mfupi wa sauti. Niliandika msaada mdogo (helpers) kwa matukio ya kawaida: mlio wa masafa ya chini kwa ajili ya hatua za miguu, mlio unaopanda kwa ajili ya kuchukua mali (loot), na sauti kali ya square-wave kwa ajili ya kupata madhara.
Sauti hizi zilizotengenezwa (synthesized) ni sehemu za muda (placeholders) kwa mpango, lakini zinatoa mrejesho wa kimekanika kwa mchezo mara moja. Unaweza kuhisi ikiwa wakati ni sahihi kabla ya kuamua kurekodi au kutafuta sauti halisi. Faida halisi inakuja katika usanifu (architecture). Kwa sababu uliweka kichocheo cha sauti kwenye kazi rahisi, kubadilisha mlio uliotengenezwa na buffer ya sauti iliyopakuliwa baadaye kunachukua mabadiliko ya mstari mmoja tu. Sehemu nyingine ya mchezo—tukio la mgongano, mwangaza wa UI, ongezeko la alama—inabaki bila kuguswa. Unatengeneza mfano wa hisia kwanza, kisha unaongeza ubora.
Kusimamia Ulimwengu Nyingi
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
