Frontend
Five Browser Games, Zero Dependencies: What Building a Game Center in Vanilla JS Actually Taught Me
韦奇宁 Dev.to (EN Zone)
4 views
A free game center at wqnlll.github.io — no framework, no build step, no npm install. Just HTML files on GitHub Pages. Here are the bugs that actually taught me something.
Most "I built a game" posts show you the finished thing. This one is about the parts that broke, because those are the parts that transferred.
The constraint I chose
I wanted a small site of free browser games — the kind a kid can open on a school laptop without installing anything, and that a parent doesn't have to make an account for. So I set the rules early:
No build step. Every file is served exactly as I wrote it. Open index.html, and that's what ships.
No framework. Vanilla JS, <canvas>, and Three.js pulled from a CDN with a plain <script> tag for the one 3D game.
GitHub Pages only. Static hosting, no server, no database, no backend to pay for.
That constraint is doing a lot of work. It means the whole site is inspectable, forkable, and can't rot because a bundler config drifted. It also means I had to solve some problems by hand.
The two-layer page pattern (and where I didn't use it)
Three of the games split into two files:
A wrapper page (rts.html) — real content: what the game is, how to play, strategy, an FAQ.
A raw game page (rts-game.html) — just the canvas and the game loop, noindex, nofollow.
The wrapper embeds the raw page in an <iframe>. This sounds like over-engineering until you hit the reason: an ad on a raw game canvas is a policy violation almost everywhere (ads on a screen with no publisher content). Splitting the two means the wrapper carries the content and the ads, and the raw game stays a bare canvas that no crawler indexes and no ad network touches.
The other three games (english-brick-breaker.html, tank-battle.html, tank-battle-mobile.html) put the game inline on a content-rich page instead. That works too — as long as the page carries several hundred words of genuine content around the game, which is what I had to go back and add after getting it wrong the first time. The pattern isn't "always use an iframe." It's "never let an ad sit on a screen that has no publisher content." Two different structures satisfy that; pick whichever fits the page.
Bug 1: 5N draw calls, and the one-line fix
The RTS has tanks made of several parts — hull, turret, barrel, treads. My first pass made each part its own mesh, so 40 tanks meant 200 draw calls. The frame graph looked like a comb.
The fix was InstancedMesh: one instanced mesh per part type, with a per-instance matrix for position, rotation and team colour. Draw calls went from 5N to 5 — five total, regardless of how many tanks are on screen.
The lesson isn't "use InstancedMesh." It's that I reached for a rendering fix before checking whether I had a scene graph problem. The count of objects was never the issue; the count of unique geometries was.
Bug 2: when terrain colour is cheaper than a texture
I originally textured the terrain with a tile atlas. It looked fine and cost a texture fetch per tile plus a whole atlas pipeline (loading, mipmaps, filtering choices).
Switching to per-tile vertexColors on a single mesh removed the texture entirely — each tile just carries its own colour in the vertex buffer. Visually indistinguishable at this scale, and it deleted a whole asset pipeline. On a grid, colour is data, not art.
Bug 3: my headless tests were lying to me about shadows
This is the one that cost me real time.
I ran the 3D build in headless Chrome to check it rendered. Shadows never appeared. I spent a while debugging shadow-map code — bias values, frustum size, camera settings — before realising the headless renderer was SwiftShader, a software rasteriser, and the depth pass for shadows was failing silently. Swallowed, not reported.
The fix wasn't code. The fix was building a test that couldn't lie: render the same frame with and without the shadow pass, and compare the two images. If they're byte-identical, shadows aren't rendering — no matter what the console says. That "same/different" check is portable and works on any renderer.
A headless pass is not a GPU pass. Never let a green test on a software renderer stand in for a real one.
Bug 4: flipping Y for a 3D view over a 2D game
The 2D game uses screen coordinates, Y down. Three.js uses Y up. Converting between them, my units drifted, the camera pointed the wrong way, and objects landed mirrored.
The combination that finally worked:
Geometry position: (wx, -wy)
Camera position: (cx, -cy)
Rotation angles: negated
Orthographic camera: left = 1500 (matching the 2D world width, not the window width)
Camera up vector: (0, cos(pitch), sin(pitch)) — positive signs
Every one of those has an intuitive-looking wrong answer that produces almost correct output, which is why it took so long. Almost-correct is the expensive kind of broken.
Bug 5: layering WebGL under 2D
The minimap and fog of war are drawn on a 2D canvas layered over the WebGL scene. The WebGL canvas needed z-index: 0, and the 2D canvas had to sit above it in the stacking order.
Obvious in hindsight. Not obvious when you're staring at a black minimap wondering why globalAlpha isn't working — the 2D canvas was simply rendering underneath an opaque WebGL surface.
Bug 6: fog of war that must not change the simulation
The RTS has lockstep multiplayer: every client runs the same deterministic simulation and only exchanges commands, never state. That means the simulation must be identical on every machine, all the time.
Fog of war is a rendering concept. If I'd let visibility data touch the sim, two clients could diverge and desync.
So visibility lives entirely in the render and interaction layer: a FOV pass computed per frame, used only to decide what to draw and what's clickable. The simulation never sees it. Adding fog of war touched zero lines of simulation code.
Then came the part I didn't expect: the AI had to be given the same fog. The AI was attacking targets outside its own units' vision — it was cheating, and because it cheated deterministically, it never showed up as a bug. Giving the AI a per-team visibility check (and letting it push reconnaissance into the dark when it had no visible target) made matches both fairer and more interesting. A skirmish that used to resolve in ~100 seconds now takes ~166, because the AI has to actually go look.
If your determinism constraint is real, it will improve your design — but only if you let it apply to the AI too.
A non-game bug worth more than the game bugs
Two of my worst hours had nothing to do with code and everything to do with encodings.
Windows batch files with non-ASCII characters. A .bat file saved as UTF-8 got read by cmd.exe as the legacy codepage, the mojibake broke its parsing, and it silently failed to write a config file — producing a downstream error that looked like a permissions problem. Fix: keep .bat files pure ASCII.
PowerShell reading UTF-8 as GBK. A script that read and rewrote an HTML file interpreted the Chinese text as GBK, corrupted the file, and in the process managed to eat code lines that sat next to comment markers. The JavaScript stopped parsing. Fix: be explicit about encoding on every read and write, and keep a backup before any scripted rewrite of a source file.
Neither of these is interesting. Both cost more time than any rendering bug on this list. Toolchain encoding bugs are the most expensive class of bug, because the error message points somewhere else.
The humbling part: content was never the bottleneck
I wrote seven substantial articles for the site — 600–1,300 words each, originally written, with proper metadata. I added a 2,000-word vocabulary list. Structure, canonical URLs, sitemap, all correct.
Google indexed two pages. The homepage and the vocabulary list.
So I measured what was actually different about the unindexed articles. Word count? Fine. Duplicate content? I checked pairwise 5-gram similarity across all seven — about 1% overlap. They're genuinely distinct. Metadata? All present. JavaScript dependence? The article text is hard-coded in the HTML and fully visible without JS.
The difference wasn't on the page at all. It was off the page: the site had essentially one referring domain — a listing on itch.io. For a search engine, that reads as "nobody on the internet has ever mentioned this site." No amount of writing fixes that. The lever is other people linking to you, which is a much slower and much less comfortable thing to work on than writing another article.
If you take one thing from this post: when a page won't get indexed, measure the page before you rewrite it. I nearly rewrote good content because I assumed thin content was the problem. It wasn't. The numbers said so, and I only found that out by actually taking the measurements.
What's there now
Five games (six playable pages), all free, no download, no account, no install:
English Brick Breaker — break bricks, learn 222 English words across 8 topics: Animals (30), Food (29), Body (30), Nature (30), Colors & Shapes (18), Actions (30), School (27), Daily Items (28). The word list is browsable on its own page.
Tank Battle — top-down tank combat, plus a separate mobile build
Math Challenge — timed arithmetic across 7 levels, from single-digit addition up to word problems
Red Alert-style RTS — the lockstep one, with fog of war and an AI that respects it
3D Plane Battle — the Three.js one
Play anything at wqnlll.github.io. It's all static, so it also loads fine on a school Chromebook.
If you've fought the "crawled, but not indexed" problem on a small site, I'd genuinely like to hear what moved the needle for you — that's the wall I'm currently stuck on.
Read original: https://dev.to/_e254dc76c325d7401dc02/five-browser-games-zero-dependencies-what-building-a-game-center-in-vanilla-js-actually-taught-me-fdi
← Previous
How We Automated Google Business Profile Data Audits Across Major B2B SaaS Markets
Next →
A Report Is Not the Artifact
Related
60fps live meters in React without re-rendering the tree
Frontend
1
DEV Community
Sanity vs Hashnode vs Dev.to: Headless Blog Platform Comparison 2026
Frontend
2
DEV Community
Stop Overworking Your Code: A Guide to Debouncing
Frontend
5
Dev.to (EN Zone)
Introducing Scribe Jam: Build the Ultimate Markdown & Documentation Tools 📝🔥
Frontend
8
DEV Community
Comments0
No comments yet — be the first