// part 3 of 3
Choosing a Renderer
This game is one point in a much larger space of ways to build 2D games. This part maps the whole space — and answers the question that actually matters when choosing: how much of what you build is locked to the choice?
The spectrum, in one picture
Moving right buys raw performance, built-in tooling (physics, tilemaps, animation editors) and distribution options (app stores, Steam) — and costs you more of your existing code, more learning curve, and in the engine case, leaving the browser-native world entirely.
First, the good news: most of the game is portable
Before comparing renderers, look at what in this codebase has nothing to do with SVG at all — because that turns out to be most of the actual game design. Three concrete pieces would carry to Canvas, Pixi, or even Godot nearly untouched:
1. The balance data. A watchtower "costs 70 gold, deals 24 damage, reaches 3.5 tiles, fires every 1.2 seconds" — four numbers in td-config.js with no opinion about rendering. They'd be equally at home in a Godot Resource or a Phaser config object.
2. The simulation logic. Targeting ("of the zombies in range, shoot the one furthest along the road") is a loop comparing plain numbers on plain objects. It never touches an SVG element. Port it anywhere and it makes the identical decision — only what happens after (spawn an SVG arrow vs. a GPU sprite) differs.
3. The wave generator. Waves aren't hand-authored — genWave(w) spends a rising "threat budget" on a zombie roster, unlocks heavier types as waves climb, schedules a boss every 6th wave, and uses a seeded random generator so wave 7 is identical on every replay (determinism the "next wave" preview depends on). Pure arithmetic; ports line-for-line.
Option 1 — Canvas 2D
Instead of persistent DOM nodes you keep moving, you get one bitmap (the <canvas> element) and redraw the whole scene from scratch every frame: ctx.drawImage(sprite, x, y), transforms via ctx.save()/rotate()/restore(). Assets become pre-rendered raster images. Two things you currently get free disappear: hit-testing (clicking a tower works today because it's a real element the browser knows the bounds of — on canvas you compute "is the click within this tower's radius?" yourself) and CSS animations (the ambient mist and embers are pure CSS keyframes today; on canvas you'd animate them by hand each frame). In exchange: notably more performance headroom at high entity counts, because the browser stops tracking hundreds of individual nodes.
Option 2 — PixiJS / Phaser (WebGL)
PixiJS is a 2D rendering library that drives the GPU through WebGL but hands you a friendly sprite/container API. Phaser is a full 2D game framework built on top of it — physics, input, tilemaps, scenes, asset loading. This is the biggest practical performance jump: GPU-batched sprites handle thousands of entities, far past where this game would ever stress the DOM.
Two details make this the natural "next step up" from Ember Keep's architecture. First, the vector art is reusable — Pixi can rasterize an SVG into a texture once at load time. Second, Pixi sprites have a built-in anchor property that does exactly what part 2's hand-written META[id].ax/ay math does. Concepts you learned the hard way transfer directly; you just stop writing them yourself.
Option 3 — Full engines (Godot, Unity, Unreal)
These are complete authoring environments, not libraries: a visual scene editor, component systems, animation state machines — and crucially, native export: a real installable app, mobile builds, Steam. The cost is symmetric: none of the JavaScript runs as-is. You rebuild the scene in the editor and re-express the simulation in GDScript or C#. What carries over is the design — stats, pacing, the road-as-a-path idea, the anchor concept — as knowledge, not code.
| Godot | Unity | Unreal | |
|---|---|---|---|
| Cost | free, open-source | free tier, paid at scale | free, royalty above a revenue bar |
| Language | GDScript (Python-like) or C# | C# | C++ / Blueprints |
| 2D tower-defense fit | very good — real 2D pipeline, huge TD tutorial community | good, but much tooling targets 3D | poor fit for the effort — built for AAA 3D |
| Learning curve | gentle | moderate | steep |
(There's also a no-code tier — Construct 3, GDevelop — visual event-based logic, fast for prototyping. If you already write code and already have a working game, it's a step sideways, not up.)
The whole map on one screen
| DOM/SVG (current) | Canvas 2D | PixiJS/Phaser | Godot/Unity | |
|---|---|---|---|---|
| Renders via | real DOM elements | one bitmap, redrawn | GPU / WebGL | GPU (native) |
| Free hit-testing | ✅ real elements | ❌ manual math | ❌ framework-assisted | ✅ built-in |
| Performance ceiling | lowest | medium | high | highest |
| Distribution | web | web | web (wrappable) | native / Steam / mobile |
td-config.js data | — | as-is | as-is | retyped, same values |
| Simulation logic | — | light edits | light edits | rewritten in GDScript/C# |
| Sprite art | — | rasterize | rasterize once | import as source |
| Renderer code | — | rewritten | replaced by library API | replaced entirely |
So… was DOM/SVG the right call?
For this game — a few dozen entities, one board, web-only distribution, and a portfolio context where "view source" is a feature — yes, and it wasn't close. Zero build tooling for assets, free hit-testing, free CSS ambience, and art that stays crisp at every zoom. The honest advice if a game like this ever outgrows it: the next step is Canvas 2D or PixiJS, which keep you in JavaScript and keep the config and simulation intact — not a leap straight to a full engine. Engines earn their cost when the goal becomes "an installable, store-distributed game," which is a different goal than "a better browser game."
That's the guide. You've gone from "a React page with a game in it" down to single attribute writes, and back up to the strategic view. The best next step, honestly: open the code and break something — retune a tower in td-config.js, repaint a sprite, make wave 3 unfair. That's how this whole thing was learned, too.