// 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

DOM / SVG
what Ember Keep uses
Canvas 2D
one bitmap, redrawn each frame
PixiJS / Phaser
GPU-accelerated 2D libraries
Godot / Unity
full game engines

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.

GodotUnityUnreal
Costfree, open-sourcefree tier, paid at scalefree, royalty above a revenue bar
LanguageGDScript (Python-like) or C#C#C++ / Blueprints
2D tower-defense fitvery good — real 2D pipeline, huge TD tutorial communitygood, but much tooling targets 3Dpoor fit for the effort — built for AAA 3D
Learning curvegentlemoderatesteep

(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 2DPixiJS/PhaserGodot/Unity
Renders viareal DOM elementsone bitmap, redrawnGPU / WebGLGPU (native)
Free hit-testing✅ real elements❌ manual math❌ framework-assisted✅ built-in
Performance ceilinglowestmediumhighhighest
Distributionwebwebweb (wrappable)native / Steam / mobile
td-config.js dataas-isas-isretyped, same values
Simulation logiclight editslight editsrewritten in GDScript/C#
Sprite artrasterizerasterize onceimport as source
Renderer coderewrittenreplaced by library APIreplaced 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.