Elke game-development tutorial begint op dezelfde manier: gekleurde rechthoeken die over een grijs canvas glijden. Dat is prima om syntaxis te leren, maar het leert je niets over hoe een game engine daadwerkelijk 'ademt'. Ik wilde weg uit de wereld van de rechthoeken. Ik wilde iets bouwen dat voelde als een echte game—een top-down dungeon crawler met beweging, gevechten, een HUD en geluid.
Ik koos voor Phaser v4 en stelde mezelf één harde regel. Nul externe assets. Geen afbeeldingsbestanden, geen audiofragmenten en geen build-tools zoals Webpack of Vite. Het hele project moest in één enkel HTML-bestand leven, geschreven in plain JavaScript. Die beperking was niet bedoeld als minimalisme om het minimalisme zelf. Het ging erom elke excuus en elke 'black box' te verwijderen. Wanneer je geen sprite pack kunt downloaden om een gat in je kennis op te vullen, word je gedwongen om te leren hoe de engine textures, animaties, audio en state beheert onder de motorkap.
De wereld tekenen vanuit code
In een normaal Phaser-project roep je this.load.image() aan binnen een preload-functie en wijs je de engine naar een PNG-bestand. Zonder die optie ben je aangewezen op het Graphics-object. Je instancieert het, tekent primitieve vormen—rechthoeken voor vloertegels, dikkere lijnen voor muren, misschien een cirkel voor een player token—en roept vervolgens generateTexture aan. Die methode legt de graphics buffer vast en registreert deze bij de texture manager van Phaser onder een door jou gekozen key.
Vanaf dat moment behandelt de engine die gegenereerde bitmap exact als een geladen afbeeldingsbestand. Je kunt het toewijzen aan tilemaps, in sprites snijden of het inkleuren (tinten). Voor de dungeon crawler betekende dit dat ik procedureel een vloergrid kon genereren, muursegmenten kon plaatsen en kon itereren op het kleurenpalet zonder ooit mijn code-editor te verlaten. De praktische les is dat een texture gewoon een brok bitmapdata is die in het geheugen staat. Phaser geeft niet om de vraag of het via een HTTP-verzoek is binnengekomen of via een handgeschreven Graphics-aanroep.
Deze aanpak dwingt je ook om bewust na te denken over de draw order en batching. Wanneer elke muur en vloertegel afkomstig is uit dezelfde familie van gegenereerde textures, ga je letten op hoe Phaser render calls groepeert. Je merkt het verschil tussen een statische tilemap-laag en een spritemap van individuele objecten, omdat je handmatig beslist welke als een texture-instantie moet bestaan.
Animeren zonder spritesheets
Statische vierkantjes worden snel saai, maar het animatiesysteem van Phaser verwacht een spritesheet—meestal een enkele PNG met frames in een raster. Ik repliceerde die strip met behulp van een offscreen HTML canvas-element. Voor elk frame van een animatie maakte ik het canvas leeg en tekende ik een nieuwe pose: een simpele zwaai met een zwaard, een loopcyclus van twee stappen, of een idle bob van een vijand. Zodra de strip voltooid was, registreerde ik deze bij Phaser als een spritesheet, waarbij ik de framebreedte en -hoogte definieerde zodat de engine wist waar de ene pose eindigde en de volgende begon.
Dit handmatig doen onthult precies wat een animatie onder de oppervlakte is: een reeks framegrenzen op een gedeelde texture. De animatiecomponent van Phaser vraagt om een startframe, een eindframe en een framerate. Vervolgens verplaatst het een pointer door die rechthoekige segmenten bij elke tick van de game clock. Je stopt met het zien van spritesheets als magische assets die door artiesten zijn gemaakt, en begint ze te zien als coördinatenwiskunde. Dat perspectief is onschatbaar wanneer je later echte art snijdt of moet debuggen waarom een animatie frames in de verkeerde volgorde afspeelt.
Geluid uit het niets
Audiobestanden waren verboden, dus ik gebruikte de Web Audio API rechtstreeks. Een paar regels JavaScript kunnen een oscillator node aanmaken, deze instellen op een blokgolf of sinusgolf, deze door een gain node sturen en een korte geluidspuls inplannen. Ik schreef kleine helpers voor veelvoorkomende gebeurtenissen: een lage piep voor voetstappen, een stijgend gepiep voor het oppakken van loot, en een harde blokgolf-toon voor het incasseren van schade.
Deze gesynthetiseerde geluiden zijn van ontwerp uit placeholders, maar ze geven de game direct mechanische feedback. Je kunt voelen of de timing goed is voordat je besluit om echte audio op te nemen of te zoeken. De echte beloning zit in de architectuur. Omdat je de geluidstrigger in een eenvoudige functie hebt verpakt, kost het later slechts één regel code om de gesynthetiseerde piep te vervangen door een geladen sound buffer. De rest van de game—het collision event, de UI-flash, de score-increment—blijft ongemoeid. Je prototypeert eerst het gevoel, en verhoogt daarna de getrouwheid.
Het beheren van meerdere werelden
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
