// part 1 of 3
The Big Picture
What are the moving pieces of a game, and how do they talk to each other? One sentence first, then we unpack every word of it.
Here is the entire architecture in one breath: a React component that, once mounted, hands a chunk of the screen over to a hand-written game loop, which ticks about 60 times a second and moves plain SVG elements around the DOM by hand — React is only used for the menus and dashboards sitting on top.
Two terms in that sentence deserve an immediate definition. The DOM is the live tree of elements a browser builds from markup — and it's not just text: every node is a real object in memory you can mutate (element.setAttribute(...)), and the browser repaints to match. Every zombie, tower and arrow in this game is one such node. The HUD (heads-up display) is the info layered over the game world — gold, lives, the wave counter, build buttons. It shows the game's state but isn't part of the simulated world, the way a monitoring dashboard shows your cluster but isn't the cluster.
Why the game is split in two regimes
React's whole model is declarative: you describe what the UI should look like given some state, and React computes the minimal DOM changes to get there. That is perfect for a HUD that changes a few times per second at most. It is a bad fit for animating 50+ entities at 60fps — re-running a render pass and diffing a virtual DOM every 16 milliseconds is real, measurable overhead spent asking a question ("what changed?") the game already knows the answer to.
So the code splits itself deliberately:
| Regime | Owns | How it updates |
|---|---|---|
| Declarative — React | menu, HUD numbers, build buttons, upgrade popup, end screen | normal React: state changes → component re-renders |
| Imperative — hand-rolled | the playfield: zombies, towers, projectiles, effects | a requestAnimationFrame loop directly sets transform attributes on real SVG nodes; React mounted them once and got out of the way |
What happens in a single frame
Everything the game does funnels through one callback that the browser fires before every repaint. One tick looks like this:
Steps 2–4 are plain functions in ZombieTowerDefense.js called back to back. Steps 1 and 5 are other people's machinery — the browser's timer API kicks the frame off, and React's renderer reacts at the end. Getting clear on that boundary is most of understanding the architecture.
Two details are easy to miss and important to steal for your own projects:
The world state is a plain mutable object. Every zombie, tower and projectile lives in g — a normal JS object held in a React ref, not React state. That is why it can be mutated 60 times a second without triggering 60 re-renders: nothing about it passes through React's state machinery.
pushHud() diffs before it publishes. It compares the new HUD snapshot field-by-field against the last one and only calls setState when something a human would notice changed — gold went up, lives dropped, a wave started. React is told when to care instead of polling. It's the same courtesy you'd extend to any downstream consumer: don't publish an event when nothing happened.
const frame = (now) => {
raf = requestAnimationFrame(frame) // book the next tick first
const rawDt = Math.min(0.05, (now - prev) / 1000)
prev = now
const dt = g.status === 'playing' && !g.paused ? rawDt * g.speed : 0
step(g, dt, rawDt) // simulate + render
// ...pushHud() etc.
}The pieces and how they connect
Part 0 introduced the files; here is the relationship that matters. ZombieTowerDefense.js is the engine — logic plus renderer. Everything else is data it reads: td-config.js for the numbers (tower damage, zombie HP, wave budgets, gold), the sprite files for the art, td-sfx.js for synthesized sound, the CSS for HUD styling and ambient animation. The config file's own header says it plainly: "Balance/map edits happen here, nowhere else."
That separation is a general game-dev principle worth keeping: numbers a designer would tweak (cost, damage, HP) stay completely apart from the code that enforces the rules — the same reason you keep config out of application code. When part 3 asks "what survives a switch to another engine?", the answer is: exactly the parts that respected this boundary.
Vocabulary — game-dev terms, minus the gatekeeping
| Term | What it means here |
|---|---|
| Game loop | Code that re-runs ~60 times a second for as long as the page is open. Each run nudges the world a tiny bit and redraws it — a flipbook, just faster. |
| Scene graph | The tree of on-screen elements that exist right now, split into layers (ground effects, entities, air) so things draw in the right order. Contains only what is currently alive. |
| Sprite | The reusable artwork for one kind of thing. There is one Shambler sprite, but a dozen Shambler zombies can be walking around, all wearing it. |
| Entity | One specific, currently-alive instance — its own position, its own HP. Kill it and the next wave’s Shambler is a different entity in the same sprite. |
| Anchor point | The one pixel inside a sprite that counts as its true position — a zombie’s feet, a tower’s base. The star of part 2. |
| Painter’s algorithm | In a ¾-from-above view, "closer to the bottom of the screen" must draw on top. The game re-sorts entities by screen Y every frame so a zombie walks behind a tower, then in front of it. |
| HUD | The dashboard over the world: gold, lives, wave counter, buttons. The "+8" coin that pops out of a dying zombie is not HUD — it lives in the game world, spawned and animated by the engine. |
Next: how a tower goes from artwork to a positioned, health-bar-wearing thing on screen — and the single most important idea in 2D rendering.