Jedes Tutorial zur Spieleentwicklung beginnt auf die gleiche Weise: farbige Rechtecke, die über eine graue Leinwand gleiten. Das ist in Ordnung, um die Syntax zu lernen, aber es lehrt einen nichts darüber, wie eine Game Engine tatsächlich „atmet“. Ich wollte weg vom Geschäft mit den Rechtecken. Ich wollte etwas bauen, das sich wie ein echtes Spiel anfühlt – ein Top-Down-Dungeon-Crawler mit Bewegung, Kampf, einem HUD und Sound.

Ich habe mich für Phaser v4 entschieden und mir eine strikte Regel auferlegt: Null externe Assets. Keine Bilddateien, keine Audioclips und keine Build-Tools wie Webpack oder Vite. Das gesamte Projekt musste in einer einzigen HTML-Datei bestehen, die mit reinem JavaScript geschrieben wurde. Diese Einschränkung diente nicht dem Minimalismus um seiner selbst willen. Es ging darum, jede Ausrede und jede Black Box zu eliminieren. Wenn man kein Sprite-Pack herunterladen kann, um eine Wissenslücke zu kaschieren, ist man gezwungen zu lernen, wie die Engine Texturen, Animationen, Audio und den State unter der Haube verwaltet.

Die Welt per Code zeichnen

In einem normalen Phaser-Projekt ruft man this.load.image() innerhalb einer Preload-Funktion auf und verweist die Engine auf eine PNG-Datei. Ohne diese Option weicht man auf das Graphics-Objekt aus. Man instanziiert es, zeichnet primitive Formen – Rechtecke für Bodenkacheln, dickere Linien für Wände, vielleicht einen Kreis für einen Spieler-Token – und ruft dann generateTexture auf. Diese Methode erfasst den Grafikpuffer und registriert ihn beim Texture Manager von Phaser unter einem von einem gewählten Key.

Von diesem Moment an behandelt die Engine dieses generierte Bitmap exakt wie eine geladene Bilddatei. Man kann es Tilemaps zuweisen, in Sprites zerschneiden oder einfärben. Für den Dungeon-Crawler bedeutete dies, dass ich ein Bodenraster prozedural generieren, Wandsegmente „stempeln“ und die Farbpalette iterieren konnte, ohne jemals meinen Code-Editor zu verlassen. Die praktische Lektion dabei ist: Eine Textur ist einfach nur ein Block von Bitmap-Daten im Speicher. Phaser ist es egal, ob sie per HTTP-Anfrage oder durch einen handgeschriebenen Graphics-Aufruf eingetroffen ist.

Dieser Ansatz zwingt einen auch dazu, bewusst über die Zeichenreihenfolge (Draw Order) und das Batching nachzudenken. Wenn jede Wand und jede Bodenkachel aus derselben Familie generierter Texturen stammt, beginnt man darauf zu achten, wie Phaser die Render-Aufrufe gruppiert. Man bemerkt den Unterschied zwischen einer statischen Tilemap-Ebene und einem Spritemap einzelner Objekte, weil man manuell entscheidet, was als Textur-Instanz existieren soll.

Animieren ohne Spritesheets

Statische Quadrate werden schnell langweilig, aber das Animationssystem von Phaser erwartet ein Spritesheet – normalerweise eine einzelne PNG-Datei mit in einem Raster angeordneten Frames. Ich habe diesen Streifen mithilfe eines Offscreen-HTML-Canvas-Elements repliziert. Für jeden Frame einer Animation habe ich das Canvas geleert und eine neue Pose gezeichnet: ein einfacher Schwertstreich, ein zweistufiger Gehzyklus oder ein leichtes Wippen eines Gegners im Idle-Zustand. Sobald der Streifen fertig war, habe ich ihn bei Phaser als Spritesheet registriert und die Frame-Breite sowie -Höhe definiert, damit die Engine wusste, wo eine Pose endete und die nächste begann.

Dies manuell zu tun, offenbart genau, was eine Animation unter der Oberfläche ist: eine Reihe von Frame-Grenzen auf einer gemeinsamen Textur. Die Animations-Komponente von Phaser verlangt einen Start-Frame, einen End-Frame und eine Framerate. Sie bewegt dann bei jedem Tick der Spieluhr einen Pointer durch diese rechteckigen Ausschnitte. Man hört auf, Spritesheets als magische von Künstlern erstellte Assets zu betrachten, und beginnt, sie als Koordinatenmathematik zu sehen. Diese Perspektive ist später unschätzbar wertvoll, wenn man echte Kunstwerke zerschneidet oder debuggt, warum eine Animation Frames in der falschen Reihenfolge abspielt.

Sound aus dem Nichts

Audiodateien waren verboten, also habe ich die Web Audio API direkt verwendet. Ein paar Zeilen JavaScript können einen Oscillator-Node erstellen, ihn auf eine Rechteck- oder Sinuswelle einstellen, ihn durch einen Gain-Node leiten und einen kurzen Sound-Burst planen. Ich habe winzige Helfer für gängige Ereignisse geschrieben: ein niederfrequentes Piepen für Schritte, ein ansteigendes Zwitschern beim Aufheben von Beute und ein harter Rechteckwellen-Ton beim Erleiden von Schaden.

Diese synthetisierten Sounds sind absichtlich nur Platzhalter, aber sie geben dem Spiel sofort mechanisches Feedback. Man kann spüren, ob das Timing stimmt, bevor man sich festlegt, echte Audioaufnahmen zu erstellen oder zu beschaffen. Der wahre Gewinn liegt in der Architektur. Da man den Sound-Trigger in eine einfache Funktion gekapselt hat, erfordert der spätere Austausch des synthetisierten Piepens gegen einen geladenen Sound-Buffer nur eine einzige Zeile Code. Der Rest des Spiels – das Kollisionsereignis, das Aufblinken der UI, die Punktzahl-Erhöhung – bleibt unberührt. Man prototypet zuerst das Spielgefühl und verbessert dann die Klangtreue.

Verwaltung mehrerer Welten

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