How to make a shooter game with AI
A shooter is one ship, a pool of bullets and waves of enemies. The pool is the part that first builds get wrong, and it is why a shooter that runs fine for a moment stops running well at all.
Jump to a section
Ask for a top down shooter in one file: one ship, enemies arriving in waves, a score and a restart key. Every model writes that without complaint. What decides whether the result is still worth playing late in a run is what happens to a bullet after it leaves the screen.
A platformer has a fixed cast. A shooter makes new objects as fast as the player can hold a key, and a first build almost never removes them. The game ends up carrying every bullet it has ever fired, moving and testing all of them, every single frame.
Four steps to a shooter that keeps its frame rate#
Play until you lose, not until it loads. The fault in a shooter shows up late in a run and not at the start of one.
If the ship starts answering the arrow keys a beat late, bullets are piling up somewhere out of sight.
One cooldown value, in seconds. It changes the character of the game more than any amount of art will.
Play again. Most of what gets called a difficulty problem is really a hit box drawn at the size of the picture.
Build a top down shooter as ONE self-contained .html file. No
frameworks, no image files, nothing loaded from the network.
PLAYER: a small triangle near the bottom of the canvas. Arrow keys or
WASD move it in eight directions and it cannot leave the screen.
FIRING: space fires. Firing is controlled by a cooldown counted down in
seconds, NOT by the key going down. Holding space fires at one steady
rate, and tapping the key faster than the cooldown does nothing.
BULLETS: build a fixed array of bullet objects ONCE, before the first
frame, each carrying an "active" flag. Firing looks for an inactive one
and switches it on. A bullet that leaves the top of the canvas, or that
hits something, switches itself off and returns to the pool. Never
create a bullet object during play, and never change the array length.
If every bullet is in use, the shot does not happen.
ENEMIES: pooled in exactly the same way. They enter from the top in
waves and drift sideways. Later waves shoot back, using their own
cooldown and their own separate pool of bullets.
HIT BOXES: keep these apart from the drawn sizes. The player's hit box
is smaller than the triangle. An enemy's hit box is a little wider than
its sprite. Both multipliers live in the config, not in the collision
code. Test a bullet against the span it crossed this frame rather than
against its end point alone.
MERCY: after the player is hit, make them briefly immune and flash the
ship, so one bad moment cannot take every life at once.
HUD: score, lives and wave number. Also print the number of active
bullets while I am testing, with a key that hides it.
PHONE: a drag moves the ship and it fires by itself.
Treat every speed and every rate as an amount per second, multiplied by
the seconds that passed since the previous frame, with that elapsed
value clamped to 0.05 at most. Include canvas { touch-action: none; } in
the stylesheet. Gather every number worth tuning into one CONFIG object
at the top, POOL_SIZE and FIRE_COOLDOWN and BULLET_SPEED and the two hit
box multipliers among them, each with a short comment.
Change FIRE_COOLDOWN and the two hit box multipliers freely, because they are the whole feel of the game. Leave the pool paragraph alone. If you drop it, you get the array that grows forever, and that is the one fault here you cannot fix by tuning a number.
The bullet array that never shrinks#
The first build is usually three lines of intent. Pressing fire pushes a new object onto an array. The update loop walks that array and moves everything in it. Nothing is ever taken out.
So the bullets that flew off the top of the canvas are all still there. They are still being moved, still being tested against every enemy, and still being drawn in a place nobody can see. The work per frame climbs with every shot and it never comes back down.
There are two fixes. One is cheap and correct. The other is a little more work and is what every shooter you have ever played actually does.
- The cheap fix: drop them each frame. After moving the bullets, keep only the ones still on screen. Rebuilding the array with a filter is the safe way to write it. Removing items while walking forwards through the same array skips the item after each removal, which is a quieter bug than the one you set out to fix.
- The real fix: a pool with an active flag. Make every bullet object before the first frame. Firing turns one on, leaving the screen turns it off, and the array length never changes. Nothing is allocated while the game is running, so there is no slow drift and no pause to collect what you threw away.
- Accept the cap. With a pool, a shot can find nothing free and simply not fire. That sounds bad and is not. Any player who notices is firing faster than your cooldown should ever have allowed.
- Pool the enemies and their bullets too. A wave spawner that creates fresh objects has the same leak as the gun, just slower. Enemy bullets get their own pool, so no collision test ever has to ask who fired what.
Models write the growing array by default because it is the shortest correct-looking answer. Ask for the pool explicitly and you get it first time. Ask afterwards and you are asking for a rewrite of the update loop, which is a harder conversation.
Fire rate is a cooldown, not a keypress#
A first build fires one bullet for each keydown event. That turns the gun into a test of how quickly the player can hit the space bar. Worse, holding the key down fires at whatever repeat rate the player's own system happens to use, so the same game shoots differently on two desks.
A cooldown settles it. Keep a timer that counts down in seconds. Firing is allowed when the timer reaches zero, and firing resets it to the full value. Now holding fire gives one steady rate, and that rate is a number you chose.
| Cooldown | How it feels | What it does to the rest of the game |
|---|---|---|
| Very short | A hose of bullets | The pool runs dry and aiming stops mattering |
| Short | Aggressive and forgiving | Suits dense waves of weak enemies |
| Medium | Each shot is a small decision | Enemies can afford more health |
| Long | Tense, close to punishing | Needs slower enemies or it reads as unfair |
Pick the feeling first and let the number follow. This is the one value worth changing between every test run, because everything else in the game is balanced against it.
The cooldown pays for itself again later. A pick-up that halves it for a short time is a complete upgrade and needs no new code. So does an overheat rule, where sustained fire slowly lengthens the cooldown until the player stops. Both are one line against a value you already have.
Hit boxes that are not the sprite#
The ship gets drawn as a triangle with a glow and a pair of fins. If the whole drawing counts as the target, a bullet passing through the glow kills the player. They will tell you the game is unfair. They will be right, and no amount of enemy tuning will answer them.
So keep the boxes apart from the art. The player's box sits inside their sprite, perhaps around the cockpit alone. An enemy's box sits a little outside theirs. Both lies push the same way, and the way is towards the player.
Bullets need the opposite correction. A fast bullet covers more ground in one frame than its own length, so a test at the new position alone can step clean over a thin enemy. Test the span between the old and new position instead and the shot lands.
Neither of these is something a model offers unprompted, because both are wrong as physics. They are right as games. Ask for the multipliers as named values in the config and you can retune the fairness of the whole game without reopening the collision code.
Waves, and the enemy that shoots back#
Enemies trickling in one at a time forever is not a game, it is a queue. Waves give a run a shape. A lull, a build, a gap long enough to breathe in, then something harder than the last thing.
- One new idea per wave. The second wave moves sideways. The third shoots. The fourth does both at once. A player can learn one rule at a time and no more than that.
- Hold the next wave until the screen clears. Two waves overlapping can stack past what anybody can read, and the player dies without understanding what hit them.
- Give a hit a moment of mercy. Brief immunity and a flashing sprite after a hit. Without it, one crowded second takes every life the player had.
- Make enemy bullets look different. A different colour and a slower speed than the player's. The screen has to be readable at a glance or none of your fairness work shows.
If you want the enemies to feel deliberate rather than random, give each wave a named pattern and let the model write the paths. A line that sweeps, a pair that close in from the edges, one that dives at the player's current position. Patterns are cheap to add once the pool is in place.
💥Shooters written by AI
Common questions#
Can AI make a shooter game?
Yes, and it is one of the better first projects, because a ship and some falling enemies are a complete game. The code arrives working. What you then do yourself is the bullet pool, the fire rate and the hit boxes, and those three are the game.
Why does my shooter get slower the longer I play?
Almost certainly bullets that are never removed. Every shot adds an object that is still moved, tested and drawn after it has left the screen. Either drop the off-screen ones each frame, or better, reuse a fixed pool of bullet objects with an active flag.
How do I stop one keypress firing one bullet?
Fire from a cooldown instead of from the key event. Count a timer down in seconds, allow a shot when it hits zero, and reset it on every shot. Holding the key then gives one rate you control, rather than a rate set by the player's keyboard.
How big should the bullet pool be?
Big enough that normal play never empties it, which follows directly from the cooldown and how long a bullet lives on screen. Work it out from those two numbers rather than guessing, and print the active count while testing to check you were right.
Why do my fast bullets pass straight through enemies?
The bullet moves further in one frame than the enemy is thick, so the test at its new position happens on the far side. Test the span it crossed during the frame instead of the single point it finished on, and the hit registers.
Should enemies shoot back in a first build?
Not in the first version. Get the player's gun, the pool and the hit boxes right against enemies that only move. Return fire doubles the number of things on screen and hides whatever is still wrong with the parts you have not finished.
Where to go next#
Once a run makes you say one more go, publish it. The page it gets records the model and the prompt behind it, and a leaderboard means your score is something other people can come and take off you. The shooters already here are worth a run each first, if only to feel where other people set their fire rates.
The general method, including the loop for fixing what comes back broken, is in how to make a game with AI. If your shooter is drifting towards the void rather than a battlefield, the space game guide covers inertia, screen wrapping and a starfield that gives a flat canvas some depth. Ready-made prompts for common faults live in the prompt guide, and the arcade guide is the one to read on scores and difficulty curves.
Built something? Put it on Agent Games.
Publishing takes a file and a minute. Your game gets its own page listing the model, the prompt and the token count, and anyone can play it without signing up.
Read next