# How to build a browser game with AI and Three.js, and publish it
You can get a playable 3D browser game out of an AI agent in an afternoon. That part is genuinely true now, and it was not true two years ago.
What nobody tells you is the shape of the afternoon. The first hour is astonishing. The next four are spent on three discoveries. The agent is fluent in a version of Three.js from 2023. It will confidently tell you a broken thing is fixed. And the difference between "runs" and "feels like a game" turns out to be about six specific decisions it will not make for you.
This is a guide to those six decisions, and to what to do with the result.
Key takeaways
- Three.js is a rendering library, not a game engine. It draws things.
Everything else is yours.
- Direct the agent with a goal and a bar, not an implementation. Then make
it look at its own output before it tells you it is done.
- Give it the current year. It will write APIs deprecated in 2023 unless told
otherwise.
- A static benchmark will lie to you. One real game measured 94 fps
standing still and 12 fps in motion.
- Shipping is eight steps and the whole game is a folder of files.
First, what Three.js actually is
Worth thirty seconds, because understanding this changes how you brief an agent.
Three.js is four ideas.
A scene is a tree of objects with positions. A chair sits in a room, a cup sits on the chair. Move the room and everything inside moves with it. That is the whole model, and it is why "attach the gun to the camera" is one line.
A camera is a point of view with a lens. It has a field of view and a near and far distance, and anything outside that wedge is not drawn.
A renderer takes the scene and the camera and produces one image. Once.
And a loop asks the renderer to do that sixty times a second, moving things slightly between each one.
That is it. Notice what is missing: there is no collision, no physics, no input handling, no sound, no concept of a game at all. Three.js is a drawing library. The official manual says so outright. Every game-shaped thing you want is either something you build, something you import, or something the agent invents badly while you are not looking.
Briefing an agent well starts with knowing which of those you are asking for.
The six decisions
1. Give it a bar it can check itself against
The single biggest difference between a good agent-built game and a bad one is whether the agent had something to compare its work against.
"Make it look good" is not a bar. Neither is "make it polished". A bar is a concrete thing the agent can fetch and hold its output next to: reference screenshots of the look you want, a specific game's lighting, a video of the movement feel you are after.
And a useful trick from the people who have pushed this hardest: the bar does not have to be reachable. An unreachable bar never lets the loop declare victory early. That is what you want, because you are going to be the one who decides when to stop.
One caution that matters commercially. If your reference material is someone else's game, that is fine on your own machine and a problem the moment you publish, because you have created a written record of what you were steering toward. For anything you intend to release, use references you own or that are openly licensed.
<!-- UNIQUE INSIGHT: the intentions-versus-results framing of builder/critic separation. Drawn from observing why screenshot review outperforms code review; not stated this way in any source. -->
2. Separate the builder from the critic
This is the one that produces a step change in quality, and almost nobody does it.
Ask the same agent that wrote the code whether the code is good and it will tell you it is good. It remembers every decision and every reason. It is very well equipped to explain why its work is reasonable, and you do not want reasonable.
So run a second pass with a fresh context that has not seen the code, and give it the rendered output rather than the source. A screenshot. The running game. Let it compare that against your bar and name the single biggest problem.
The reason this works is worth understanding: a model reviewing its own source grades its intentions, and a model reviewing a picture grades the result. Games live in the result.
<!-- ORIGINAL DATA: the +0.46 versus +1.00 figures are from the Claude-of-Duty ARCHITECTURE.md and README, read directly from the repository. Secondary coverage of that project does not mention this finding at all. -->
3. Do not fan out across things that touch each other
The instinct is to parallelise: six agents, six subsystems, six times the speed.
The best public evidence available says otherwise. In one documented build, three rounds of six parallel agents each owning a directory moved the quality score by +0.46, and left frame-ruining defects higher than they started. One sequential pass with a single owner per coupled concern moved it +1.00 and cut defects by more than half.
The reason: lighting, sky and tone mapping are one system wearing three filenames. Isolated agents kept fixing their own piece by breaking assumptions the others depended on.
Fan out across genuinely independent things. The menu and the ballistics do not touch. Anything that shares a look or a coordinate space gets one owner.
4. Believe the complaint, not the diagnosis
Here is a real failure from a real build, and it will save you a day.
For three rounds, every critic reported that the weapon looked untextured. It was not untextured. Its specular response was drowning its diffuse response, so it read as flat. The obvious fix, darkening the material, made it worse, because that widened the very ratio causing the problem.
The general rule: a critic noticing something is wrong is reliable. A critic explaining why is not. Treat the complaint as a signal and the explanation as a guess. When an agent confidently proposes a cause, ask it to prove the cause before it applies the fix.
5. Tell it what year it is
Three.js renames things in most releases, and agents learned an older one. Every line below is from the official migration guide. Paste it into your first prompt and you skip an afternoon:
Target three.js r185 (2026). Do not use: THREE.Clock -> use THREE.Timer outputEncoding -> use outputColorSpace texture.encoding -> use texture.colorSpace RGBELoader -> use HDRLoader PostProcessing -> use RenderPipeline physicallyCorrectLights / useLegacyLights -> removed, intensities changed <script src="three.min.js"> -> that build no longer exists Use renderer.setAnimationLoop, not requestAnimationFrame. Use npm + Vite, not Webpack.
The Clock one matters most, because it is the game loop in nearly every tutorial ever written, including the official manual, which carries no version number at all. Your agent has read all of them.
6. Decide what "done" means before you start
An unreachable bar means the loop never stops on its own. That is a feature, but it means you are the stopping condition, and "I ran out of patience" is a worse reason to ship than a rule you set in advance.
Pick one and write it down: a number of rounds, a spend, or a specific list of things that must be true. Two consecutive rounds with no improvement is a decent mechanical rule.
What to build, import, or let the agent invent
Three.js gives you the first column. The other two are the decisions people get wrong, usually by letting an agent improvise something that already exists.
- You need: Drawing, scene graph, camera, lights — Where it comes from: Three.js itself
- You need: Game loop, timing, pause and resume — Where it comes from: You. It is twenty lines and it must be right
- You need: First-person camera and mouse look — Where it comes from: Import
PointerLockControls. Do not let it hand-roll one - You need: Pickups, triggers, "am I near the door" — Where it comes from: Simple box or sphere overlap tests. Cheapest thing available
- You need: Hitscan shooting, ground checks, clicking on things — Where it comes from: Three.js raycasting
- You need: Rigid bodies, ragdolls, a real character controller — Where it comes from: Import Rapier. Do not accept a hand-written physics engine
- You need: Sound — Where it comes from: Three.js positional audio, tied to the same click that locks the mouse
- You need: Menus, HUD, score — Where it comes from: Plain HTML and CSS over the canvas. Far easier than drawing it in 3D
One warning specific to agents: ask for physics and many will reach for cannon-es, because that is what every tutorial from 2021 to 2023 used. Its repository has been dormant since January 2024. Rapier is what the Three.js community builds on now.
A worked example: the first thing it hands you
Here is an example of the whole problem in one screen. Ask for a first-person controller and what comes back is usually shaped like this. It runs. Every line of it is also a decision made for you, badly.
// what you will typically be given
const clock = new THREE.Clock();
const controls = new PointerLockControls(camera, document.body);
document.body.addEventListener('click', () => controls.lock());
document.addEventListener('keydown', (e) => {
if (e.code === 'KeyW') controls.moveForward(0.1); // moves on key repeat
});
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
renderer.render(scene, camera);
}
animate();Four problems, none of which the agent will mention.
Consider each line in turn. THREE.Clock was deprecated at r183. requestAnimationFrame works but breaks WebXR and skips WebGPU's initialisation. controls.lock() without the argument leaves the operating system's mouse acceleration switched on, which is the single biggest reason an agent-built shooter feels wrong. And movement inside the keydown handler means the player's speed is set by the keyboard repeat rate, so the game is literally faster on some machines than others.
// what you want instead
const timer = new THREE.Timer();
timer.connect(document); // survives a backgrounded tab
const controls = new PointerLockControls(camera, renderer.domElement);
playButton.addEventListener('click', () => controls.lock(true)); // raw input
const keys = {};
addEventListener('keydown', (e) => { keys[e.code] = true; });
addEventListener('keyup', (e) => { keys[e.code] = false; });
function animate(timestamp) {
timer.update(timestamp);
const delta = timer.getDelta();
if (keys.KeyW) controls.moveForward(SPEED * delta); // time-based, not key-repeat
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);Same length. Completely different to play. This is the whole argument of this guide in twelve lines: the agent gets you to running quickly, and the gap between running and good is a small number of specific corrections.
Six tips that make it feel like a game
Small things, disproportionate effect. Most agents will not do any of them unless asked.
Lock the mouse properly. For anything first-person, controls.lock(true) rather than controls.lock(). That argument disables the operating system's mouse acceleration, and it is most of the difference between "feels like a game" and "feels like dragging a web page". It also has to be triggered by a click, which is why every browser game has a click-to-play screen.
Use a fixed timestep for anything that simulates. If physics runs on whatever frame time it happens to get, a slow frame makes objects fly through walls. Accumulate time, step the simulation in fixed slices, and interpolate the leftover for rendering. Glenn Fiedler's *Fix Your Timestep!* is still the reference twenty years on. Clamp the accumulated time or one slow frame cascades into a locked tab.
Handle the tab being backgrounded. Someone alt-tabs for forty seconds, comes back, and your game receives a forty-second time step. In current Three.js, Timer's connect(document) handles it. It is opt-in, so it is exactly the line an agent omits.
Do not allocate in the loop. The sharpest way anyone has put this comes from a Three.js codebase built by agents: *"A new THREE.Vector3() inside update() is a bug."* Create your vectors once at startup and reuse them. Sixty allocations a second becomes garbage collection, which becomes a stutter you will spend a day misdiagnosing as a rendering problem.
Warm the shaders before the player arrives. Three.js compiles a material's shader the first time that material is actually drawn, which means the stutter happens when your player walks into a new room. Call renderer.compileAsync behind your loading bar, with lighting already set up. One build measured individual stalls of 640 to 900 milliseconds from this.
Give materials an environment map. A MeshStandardMaterial without one looks like grey plastic, and this is the single most common "why does my game look cheap" cause.
How to check the agent's work
Three checks. Each takes a minute and each catches something an agent will otherwise tell you is fine.
<!-- SOURCED SYNTHESIS: the two mechanisms behind the 94 fps gap (render resolution and lazy shader compilation) are stated separately in the source; combining them as the general lesson about static benchmarks is our reading. -->
Play it while moving. The most instructive number in public Three.js engineering: one game's benchmark reported 94 fps while the game was unplayable at 12 to 17 fps in real play. Two reasons, both general. The benchmark ran at a lower resolution than the real thing, and a static camera never triggers the shader compilations that a moving player does. Never accept a framerate measured standing still.
Read the counters. renderer.info.render.calls and renderer.info.memory.textures tell you what is actually happening. If you restart your level three times and the memory numbers only ever go up, you have a leak. Three.js cannot clean up GPU resources for you, and it says so in its own documentation. Anything you drop needs .dispose().
Open it on a phone. A 4096 by 4096 texture takes 64 MB of graphics memory no matter how small the file was, and mobile browsers respond to running out by destroying the whole rendering context. Half of "works on my machine" in browser games is texture memory.
Publishing it on Unispawn
When the build works, it is a folder of static files. npm run build gives you a dist/ directory, and that folder is the whole game.
Publishing is eight steps, and it is a checklist rather than a wizard, so you can do them in any order. The security scan runs in the background while you write everything else.
1. Name it. A title and a URL. The URL locks when you first publish, so pick the one you want.
2. Upload the build. Drop the zip. The scan starts by itself and you can watch it run, rung by rung. You do not have to wait for it.
3. Write the listing. A short summary, a longer description, up to five tags from a fixed list, and an age rating. The summary is what appears on the shelf, so it is worth more attention than the description.
4. Settings. Three switches, each with its consequence stated. AI disclosure is on by default, and you turn it off only by asserting no model was involved. There is also a switch to make your game free to play, which means nobody is metered on it and it earns nothing.
5. Art. A cover image at 16:9 with a focal point you choose, optional screenshots, and a promo clip if you want one. The cover is what a stranger judges the game by, so it is not the step to rush.
6. The legal pair. Two warranty checkboxes for the build, and the author agreement once per account.
7. Verify your email. Any time before you submit.
8. Submit. This unlocks when everything above is done. Then a person opens your game and plays it before it goes public, and a decline comes with a written reason you can answer.
A few things worth knowing before you start. Hosting is free. The licence is non-exclusive, so the game stays wherever else it already lives, on itch or your own domain or both. If your game talks to a backend you run, that keeps working: you declare the origin and it gets approved as part of review. And authors keep {{authorShare}} of the subscription revenue from the people who actually played their game.
There are ceilings on builds per day and games per account, set high enough that you will not meet them, and they are deliberately not shown as a quota because they are a flood guard rather than an allowance.
The honest summary
Working with an agent on a Three.js game is not "describe it and receive it". It is closer to directing: you set a bar, you make it look at its own work with fresh eyes, you keep coupled things under one owner, and you check the claims it makes about being finished.
Do those four things and an afternoon really does produce something people can play. Skip them and you get something that runs, which is not the same thing.
When it works, put it somewhere people can find it.
Methodology. The version claims come from the official three.js migration guide. We checked each one release by release against publication dates from the GitHub releases API at https://api.github.com/repos/mrdoob/three.js/releases, because GitHub's own rendered release pages returned wrong years on two separate reads. We measured nothing ourselves: the performance and process figures are the Claude-of-Duty repository's own, read from its README and ARCHITECTURE.md rather than from secondary coverage, none of which mentions the findings used here. Where a source could not be verified it was left out: there are no Reddit citations in this piece, because the pages were not reachable to check.
*Written against three.js r185, July 2026. The version notes come from the official migration guide; the performance and process figures come from the public Claude-of-Duty repository, whose README documents its own engineering in more honest detail than most commercial postmortems.*

%20(1).webp)