VoxelStrike · how it's built

A combat game on a 1992 terrain trick

VoxelStrike is a war game with helicopters, jets, tanks, ships and giant walking robots, fighting over a huge world you can blow holes in. Its ground is drawn with the same basic trick the 1992 helicopter game Comanche used, rebuilt in Python and moved onto the graphics card. This article explains how it works, what made it slow, how each problem was found and fixed, and what we'd tell anyone building something similar.

September 2026 · written in Python · played on an RTX 3090 desktop, a Ryzen mini PC and an Intel Arc laptop (section 15)
106,815lines of Python in the game itself. No game engine like Unity or Unreal underneath: it's all written from scratch.
03D models, textures, maps or sound effects stored as files. The game builds them all from code when it starts. Only the spoken voice lines are recordings.
334 → 44 msthe longest single frame while flying a jet over new ground, before and after the stutter fixes
1,044automatic tests, each one proven to catch the problem it's meant to catch

00What it looks like

Every picture here is a real frame from the game, drawn by the graphics card at full HD (1920×1080). They come from the camera shots the trailer tool records (section 14), with no colour correction or retouching. You can watch the finished trailer on YouTube and browse every vehicle on the showcase page. Click any picture to see it full size.

01What the game is made of

VoxelStrike is an arcade war game. You can fly helicopters and jets, drive tanks and boats, pilot giant walking robots, and land on an aircraft carrier and take off again. You fight a computer-controlled army across open country, cities and sea, or play against friends over a home network.

It is written in Python, a programming language known for being easy to write and slow to run. A few free libraries do the heavy lifting: pygame opens the window and reads the keyboard, moderngl talks to the graphics card, and numba translates the busiest parts of the code into fast machine code, close to the speed of C. A one-click launcher sets everything up on a new PC.

There's no game engine underneath. Most games are built on Unity or Unreal, which provide the drawing, physics and sound. Here, all of that was written for this game: 54 files and 106,815 lines. The biggest pieces are the code that builds the vehicles (23,880 lines), the code that runs a game session (16,957) and the graphics code (8,773). There are 80 vehicles you can use, 85 weapons and 47 kinds of enemy.

Because Python is slow, the whole design follows one rule: Python makes the decisions, and the heavy, repetitive work goes to fast compiled code or to the graphics card. Drawing the ground, building the world, moving thousands of bullets and checking what can see what all run as compiled code. Everything drawn in bulk, like the ground, trees, vehicles, smoke and power lines, goes to the graphics card in about 25 batches per frame. Python keeps the parts that need thinking: the rules, the enemy's tactics, the on-screen display and the missions.

The project started on 31 May 2026. By 29 September it had 1,267 saved changes ("commits") across 71 working days. It was built by one developer working with an AI coding assistant, and that working method gets as much space here as the game (section 12).

A few terms used in this article

Frame: one picture on screen. Games draw 60 or more a second, so each frame has about 16.7 milliseconds (ms). A frame that takes much longer is a visible stutter.

CPU and GPU: the computer's main processor, and the graphics card. The GPU is very fast at doing the same simple job for millions of pixels at once.

Height map: a grid of numbers, one height per square of ground, like a topographic map. A colour map is the matching grid of colours.

"99th percentile" (p99): the frame time that 99 frames out of 100 beat. It measures the bad frames, not the typical ones.

02How the ground is drawn: the 1992 trick

Most 3D games build their landscapes from millions of small triangles. VoxelStrike doesn't. Its ground is just two images: a height map (how high each spot is) and a colour map (what colour it is). The 1992 game Comanche turned those two images into a 3D view with a clever shortcut called Voxel Space.

Here is the idea. Take one vertical strip of the screen, one pixel wide. Walk outward from the camera across the map in that direction, a step at a time. At each step, look up the ground's height, work out where that height would appear on screen, and paint the strip up to there in the ground's colour. Because you walk from near to far, anything closer has already been painted, so a hill automatically hides whatever is behind it. You only paint the part that sticks up above what's already there. Once the strip is full to the top, you stop. Do that for every strip across the screen, and you have a landscape.

VoxelStrike's first renderer did exactly this on the main processor, and it's still in the game as a backup and for the automatic tests. Each screen strip is independent, so every processor core takes its own strips at the same time. The steps start small, so the ground right under a low tank is drawn in fine detail, and get longer with distance, where detail doesn't show. Far away, the program switches to a lower-detail copy of the map and blends the two, so there's no visible line where the detail changes.

Most of what makes it look good came from fixing specific things players noticed:

  • Buildings "shape-shifted" as you approached. At a sharp edge, like a cliff or a wall, the program picked whichever map square was closest, and that choice flipped back and forth as the camera crept forward. Now it always uses the tallest square, so edges stay still.
  • The ground seemed to crawl under a slow tank. Colours snapped from one square to the next. Now smooth ground blends the four nearest colours, and only sharp edges stay crisp.
  • Looking down from high up looked warped. The original 1992 method assumes the camera looks straight ahead. The maths was extended to handle tilting the view down, without changing the picture when you fly level.
  • Haze and fog thicken with distance the way real air does, plus low mist pools over water and valleys. The graphics-card version uses the exact same formula, so both versions agree on the weather.

The limit of the trick is that each spot on the map has just one height. So there can be no overhangs, no bridges you fly under, and no caves. Buildings are simply tall spots on the map with their own colours. Anything that must be a true 3D object, like vehicles, trees and flying debris, is drawn separately on top.

03Moving it to the graphics card

Painting every pixel of the landscape on the main processor, then sending the finished picture to the screen, was too slow. The graphics card is built for exactly this kind of work: the same small job for every pixel, millions of times, all at once. The move started on 6 June, and four days later the ground was being drawn by a small program that runs on the graphics card for every pixel (a shader).

The scene drawn by the original main-processor rendererMain processor · the original method
The same scene drawn by the graphics cardGraphics card · today's method

The same world, helicopter and camera angle, drawn by each version. The ground and scenery match. The graphics-card version adds finer ground detail, properly shaded vehicles and a soft glow around bright spots.

The view from a jet's cockpit between flat-topped hills. Every pixel of ground here is worked out separately by the graphics card, many times a second.

How the graphics card finds the ground

Instead of one walk per screen strip, the graphics card now does one walk per pixel. Each pixel sends an imaginary line out from the camera and steps along it until it hits the ground. The trick that keeps this fast: if the line is, say, 100 metres above the ground, it can safely jump ahead a good distance, because it can't hit anything sooner. So it takes big leaps through open air and small careful steps near the ground. A special check near walls stops it jumping straight through the side of a skyscraper; before that fix, distant towers had strange blue slices cut out of them.

Each pixel is allowed up to 1,200 steps. In practice a jet at height uses about 45 steps per pixel on average, and a helicopter flying low about 146. No pixel has ever needed the full 1,200.

The graphics card also records how far away each pixel of ground is. That lets it draw vehicles, trees, smoke and power lines afterwards and automatically hide any part that's behind a hill. This matters again in section 11.

A well-known speed-up that made it slower

A standard technique called "hi-Z" keeps a simplified copy of the map to help the lines skip empty space faster. It was built and measured, and it made drawing slower in every test, by 2% to 27%. The big leaps through open air were already doing the same job, so the extra copy only added work. It's switched off, and it's written down so nobody turns it on expecting a gain.

Only send what changed

The first version of the endless world sent the entire map, about 112 MB, to the graphics card every frame. The frame rate fell from 40–50 frames per second to 1–4. Now only the new strip of land is sent as it arrives, and an explosion sends only the small patch it changed. A later bug brought the old problem back in disguise: once you had flown far enough from the start, the game thought every new crater crossed the edge of the map, and sent the whole 100 MB map again for each one. Fixing that made those frames about three times faster.

04Where the time goes in each frame

A measuring tool plays the real game and times each step the graphics card takes to draw a frame. Measured on 16 September, in milliseconds per frame (remember that 60 frames a second allows about 16.7 ms in total):

What the graphics card drawsBig fight, full HDBattle, full HDJet, full HDBig fight, 4K
the ground and sky3.411.490.997.67
the on-screen display, sent as a full-screen image1.131.131.104.38
clouds and sun, sent as a full-screen image–1.141.13–
all vehicles, trees, smoke and power linesunder 0.05under 0.02under 0.02under 0.05
total4.73.83.312.1

Three things stand out. First, the graphics card wasn't the problem at full HD: it needed 3–5 ms, while the main processor needed 10–13 ms for its share. Second, all the vehicles, trees and smoke together cost almost nothing, because each kind is sent to the graphics card as one batch. Third, a surprising share of the graphics card's time went on full-screen images the main processor painted and sent over every frame. At 4K, those cost more than drawing the whole landscape.

Taking the full-screen images off the main processor

A storm over the lakes. Rain, the darkened sky and lightning are now all drawn by the graphics card.

An explosion's flash, a storm darkening the sky, lightning and the red flash when you're hit were each a full-screen picture that the main processor painted pixel by pixel, at 3.75 ms each at full HD. Now they're simple settings the graphics card applies as it draws. There was a bug hiding here too: the sun had been painted onto a see-through layer in a way that left it completely invisible, so there had never been a visible sun in the graphics-card version. It's drawn properly now, and clouds sit correctly behind mountains. The on-screen display is only re-sent where it changed.

The result in a thunderstorm: drawing time fell from 16–20 ms to about 10 ms at full HD, and from 38–56 ms to 10–14 ms at 4K. Data sent to the graphics card each frame fell from 8–17 MB to 1–2 MB, and stuttering frames (over 33 ms) fell from 214 to 42.

Lower resolution that still looks sharp

Since the ground's cost depends on the number of pixels, a slower graphics card can draw at a lower resolution and scale the picture up. The game uses FSR 1, AMD's free upscaler, which keeps far more fine detail than simple stretching and costs under half a millisecond at full HD. A built-in manager now watches how busy the graphics card is and lowers the resolution only when the graphics card is actually the bottleneck. Its first version didn't check that and lowered the resolution in a battle where the main processor was the slow part, which made the picture worse for no gain.

Found along the way

Once the graphics card had less work, it started saving power by slowing itself down, and the same drawing step measured 3.2 ms at one moment and over 7 ms at another. A graphics card that's resting can look like a slow one. Any automatic quality setting has to tell the difference.

Why not DirectX or Vulkan?

Newer graphics systems like DirectX 12 and Vulkan mainly reduce the cost of sending each batch of drawing work. This game sends only about 25 batches a frame, so there's little to save. The real costs, drawing the ground's pixels and running Python, would be the same. Switching would mean rewriting all the graphics code for almost no speed gain, so the game stays on OpenGL, which runs on nearly every PC.

05A world that doesn't end

In the streamed worlds, you can fly in any direction for hundreds of kilometres without reaching an edge or seeing the land repeat. The ground is made from layered random noise, the standard way games create natural-looking terrain, with extra shaping for sharp ridges and stepped mesas. Slow-changing "dials" across the world decide where it's flat, mountainous, forested or cut into mesas, in regions about 9 km across, so a long flight crosses real changes of scenery.

The game only keeps the land around you in memory, in a window that moves with you. That window wraps around at its edges (leaving on the right brings you back in on the left), which makes it cheap to slide along. Vehicles and soldiers, though, must not wrap. Before that was fixed, a unit placed far out could be "teleported to the far side of the world".

Why helpers, and not threads

Python has a well-known limitation: within one program, only one piece of Python code can run at a time. Building land in a background "thread" of the same program was hopeless. One strip of land took 4.8 seconds there, against 0.14 seconds when the game did it itself. So the land is built by a separate helper program running alongside the game, and the two share a block of memory. An earlier version passed data through a channel where each side waited for the other, and once both waited at the same time, freezing the game for 97 seconds. With the helper, a jet flying at full speed got 135 of 136 new strips of land ready in advance, and picking one up cost less than a fifth of a millisecond. Before, the same flight had frozen for 1.3 seconds at a time.

The horizon is real

Beyond the detailed area around you, a low-detail copy of the same land stretches out to the horizon. It uses only 7.3 MB, and its outline differs from the real mountains by less than a third of a pixel. Reaching that far with full detail would have needed 793 MB and about 45 seconds to build. So the mountains in the distance aren't a painted backdrop: they're the actual mountains you're about to fly over.

A dive from high up. The land on the horizon is the low-detail copy, and it's the real terrain ahead.

The fixed maps

Alps map from above
alps
Archipelago map from above
archipelago
Megacity map from above
megacity
Atoll map from above
atoll
Dunes map from above
dunes
Badlands map from above
badlands
Jungle map from above
jungle
Lava map from above
lava

Eight of the fixed maps seen from directly above. These are the actual colour maps the game draws the ground from. The grey circles are cities.

Alongside the endless worlds there are fixed maps like these, each built from a recipe: random noise, shaped into islands, then water, landscape types by height and slope, forests and cities. They also get a smoothing pass that wears down spiky peaks, a bit like erosion. In multiplayer the map itself is never sent over the network. Each player's computer receives the recipe and builds the identical world itself.

All tests passed, and the mountains were missing

On 19 August, a change let the land be built in the background with a speed limit of about 4 strips per frame. A fast jet needs about 27. So the jet flew off the edge of the built land, and the developer reported "I couldn't even see a mountain". Yet all 348 automatic tests passed. Every test checked that the land was built correctly and quickly; none actually flew a jet and asked "is there ground in front of me?". That test exists now, and it was run against the broken version to prove it catches the problem.

06Hunting down stutters

Most frames in a jet were fine. The trouble came in hitches every time the jet crossed into new land: frames of 63–111 ms, when 17 ms was normal, and 103 of them in one flight. A dedicated tool plays the game at a steady 60 frames a second, records every slow frame and lists exactly what the game was doing during it. It found eleven separate causes:

What was happening during the slow framemsHow it was fixed
the list of all 19,105 visible trees thrown away and rebuilt from scratch26–50add and remove only the trees that changed
building the trees, houses and roads of new land17–55moved to a helper program, 2 seconds ahead
extending the horizon, then re-sending all 7.3 MB of it5–15 + 3–5.5built by the helper; only new parts sent
shading new land3–6.4one fast compiled step
traffic looking up nearby roads8–12looked up once per area, in advance
filling a town with peopleup to 14at most 64 new people per frame
drawing the picture of a new kind of tree or prop the first time it appears7–17prepared while the land is on its way
the collection of those pictures outgrowing its space and being rebuilt25 + 125new pages added; nothing is rebuilt
building a 3D model the first time it's seen28.7built on the loading screen
creating an enemy robot's footstep sound the first time it walks~50created on the loading screen
compiled code being loaded the first time it's used7.4–159loaded on the loading screen

The same tests, run twice each before and after (900 frames each, full HD):

Scene1 frame in 100 is slower than (ms)slowest frame (ms)frames over 33 msover 50 ms
jet, before74.4 / 72.0334.9 / 328.345 / 4132 / 29
jet, after21.9 / 22.044.0 / 27.22 / 00 / 0
battle, before70.7 / 66.6915.4 / 176.964 / 6321 / 20
battle, after34.3 / 34.062.6 / 57.611 / 112 / 1

Not every idea worked. One well-known trick for sending images to the graphics card faster was tried here, measured as slower (22.5 ms per frame instead of 17.3), and removed. An idea that sounds right still has to prove it.

When Python gives two things the same name tag

Python can hand out a quick ID number for any object, but once an object is deleted, a new one can get the same number. A test counting missiles by those IDs reported 15 alarms for 2 missiles, when really 8 missiles had been merged into 2. The same mistake later nearly made 12,791 trees draw with the wrong pictures. Everything now gets its own permanent serial number.

07Vehicles built from boxes

An Abrams tank rotating
An attack helicopter rotating
An aircraft carrier rotating
A giant robot walking

A tank, a helicopter, a carrier and a giant robot, rendered from the game's current models. Each is built from code every time the game starts.

Every vehicle is made of small boxes, a bit like LEGO, and each one is described by a short piece of code rather than a model file. The model code holds 441 of these recipes. A recipe lays out the main panels; the game then joins them up, splits them into smaller boxes and tidies up thin slivers, up to a limit per vehicle type (about 1,400 boxes for most vehicles, up to 6,000 for big ships and 9,000 for the giant robots). A final pass adds the small details each kind of vehicle is recognised by: tank tracks, rails, canopy frames, navigation lights. A Falcon jet goes from 84 boxes as written to 1,400 in the game.

A giant robot, seen from its feet. Every panel is a shaded box, the smoke is drawn by the graphics card, and arms and legs can be shot off.

The graphics card draws all of a vehicle's boxes in one go. At first each box was drawn as a flat square facing the camera, which looked like a cardboard cut-out up close. Now each box is drawn as a real box with three lit sides, which still costs only a fifth of a millisecond for 54 vehicles. Moving parts like rotors, propellers, wheels, tracks, radar dishes and recoiling gun barrels are moved by telling the graphics card a few numbers each frame, not by rebuilding the model. Skipping numbers that hadn't changed cut that traffic from 240 updates to 29.

08Damage you can see

The developer's request was about feel: "when we're hitting other vehicles … there should be some more impact type feel to it … I want to have a little more oomph". To turn "more oomph" into something that could be checked, a hit was filmed and the number of pixels it changed on the target was counted:

WeaponPixels changed on the target, beforeAfter
rifle0 of 30,905light rounds: 6.2% of the target, 150 pixels of debris
autocannon105 (0.3%)
tank shell1,381 (4.5%)19.3%, 1,125 pixels of debris
armour-piercing dart1,547 (5.0%)19.3%, 1,855 pixels of debris

So a rifle hit used to leave no visible mark at all. Now each vehicle remembers up to six hits. At each one the paint is scorched off, the plate is dented, a hole is punched through, and the vehicle rocks on its springs: 1.5° for a light round, 3.9° for a tank shell. Before this, damage was a single number that darkened the whole vehicle evenly. The giant robots can lose arms and legs as they take damage.

Tanks fighting on a beach. Hits scorch, dent and hole the armour, and the burning wreck lights up the sand around it.

Two days later, the measurement itself was checked. Part of the change counted in the table was debris flying off, not the scar on the vehicle. With the debris switched off, the scar alone accounted for 160 pixels. The lesson: to measure something partly see-through, like smoke or a scorch mark, compare the picture with it and without it.

Destruction that stays

A guided bomb on a big city, in slow motion. The buildings are part of the ground, so a hit lowers them into rubble, and the rubble stays.

Explosions dig craters into the ground, set the trees and buildings on them to match, and write down what happened, so the damage is still there when you fly away and come back. A bug once brought a destroyed hangar back to life: after you left and returned, it stood whole on top of its own rubble and could be blown up "again, and again". The game had remembered it by one of Python's reusable ID numbers. Now it's remembered by what it is and exactly where it stood. The developer's first guess, duplicated buildings, was checked and ruled out: 2,506 buildings, 2,506 different positions.

09Flying, driving, floating, and the enemy

Each kind of vehicle moves by its own rules:

  • Helicopters use a real rotor model, built from the same published formulas engineers use. It covers the extra lift near the ground, the dangerous "vortex ring" state when you descend too fast into your own downwash, and autorotation, where a helicopter with a dead engine glides down on its freely spinning rotor. It gives the same result at 30 or 480 frames a second; the model it replaced drifted 34 units apart over 10 seconds depending on the frame rate.
  • Planes generate lift from their angle to the airflow and stall when too slow. The slower you are, the harder the nose drops.
  • Ground vehicles sit on four springy corners, so they pitch when braking, roll in turns, bottom out over bumps, and grip differently on grass, sand, rock and road.
  • Ships float on the same three sets of waves that are drawn on the water. A long ship averages out short waves, so a destroyer barely notices chop that throws a small patrol boat around.
  • Giant robots walk, and can fold up into vehicles.
Hugging the hills. The rotor model includes the extra lift you get close to the ground, which fades as the ground drops away.
A naval battle among the islands. Ships rise and fall on the same waves drawn on the water.

Most bugs here were mix-ups of units, not bad physics. A boat's acceleration was accidentally tied to its speed, and it topped out at 58% of its real top speed. A landing rocket's firing height was written as a speed, so when gravity was changed, the rocket fired at the wrong height. That happened five times before it became a written rule. And a gunship once started the game 80 units inside a mountain and flipped over on the very first frame.

How the enemy thinks

A review of the enemy's behaviour on 3 September started by measuring it: 2 of 18 ground vehicles were permanently stuck. Each had exactly one direction it could choose, and if that pointed up a steep hill, it pushed against the hill forever. Giant robots stood up once and never folded back down. Nothing decided to flank, hold a ridge or retreat; every unit simply drove at its target.

Now each enemy has a plan: advance, flank, hold, withdraw, regroup or reposition. It rethinks that plan every half-second to second and a half, and units take turns so they don't all think on the same frame. When moving, a unit tries nine directions and takes the first good one. Afterwards, stuck units fell from 5 of 53 to 1 of 53, and units switched plans more often (1.89 different plans each on average, up from 1).

The enemy also plays fair. Every unit, on both sides, can only shoot what it could really see: you must be in front of it, not behind a hill, and in range, for long enough for it to react. So there are no shots through mountains and no eyes in the back of its head. The difficulty levels change how sharp the enemy is, not just how hard it hits: an easy enemy rethinks every 1.6 seconds and takes 1.4 seconds to react, a hard one every 0.6 seconds and 0.5 seconds. Badly damaged, 40% of easy enemies retreat, and none of the hard ones do.

10Sound made from scratch

Almost every sound is generated by code when the game starts, not played from a recording: every vehicle's engine, every weapon according to its size, and every hit according to what was hit, whether the shot bounced off or went through, and how big the gun was. The rumble of tyres and tracks changes with the ground, and footsteps change with weight. The one exception is speech: 127 voice lines (mission briefings, radio calls) were generated with text-to-speech ahead of time and ship as recordings.

Two sound bugs changed the rules for the whole project. First, every engine sounded the same. The code meant to keep only the deep, low sounds couldn't actually reach low enough, so every "rumble" came out as a mid-range hum. It was replaced with a proper filter. Second, loading the music changed the world. The music and the world builder shared one random number generator, so building music at the same time as land changed the land: the same world setting produced 2,250, 1,906 and 2,070 particles on three runs. Now anything that needs repeatable randomness has its own generator.

11Let the graphics card decide what's hidden

The original version had to work out itself whether a vehicle was hidden behind a hill before drawing it, by tracing a line from the camera to the vehicle. That check was kept when drawing moved to the graphics card, even though the graphics card already knew exactly how far away every pixel of ground was. Keeping it was not only wasted effort, it was wrong:

  • Vehicles blinked in and out as they came over hills. The single line was aimed at the middle of the vehicle. So a tank whose top was clearly visible over a ridge was skipped on 121 of 660 frames, flickering six times in eleven seconds.
  • Power lines disappeared behind trees that were behind them. Cables were painted first and every tree was painted over them, near or far. Drawn by the graphics card with proper distance checks, 20–60% more cable correctly shows, and it takes 0.2 ms instead of 3.

The rule now: when the graphics card already knows what's in front, let it decide what's hidden. The sister project, the block-building game VoxelBlox, came to the same conclusion and explains it from its side.

12How the work gets done

The code is only half of this project. The other half is how one developer and an AI coding assistant work together. The developer describes what they want in their own words, and the assistant writes the code. The way of working below was written down because simpler ways kept going wrong.

Keep the request in the developer's own words

Every request goes into a to-do list exactly as it was said, and stays there until it works when the developer plays the game, not just until a test passes. It isn't rewritten into a formal specification, because that rewriting is where meaning gets lost. Instead, it's turned into something that can be measured, like "more oomph" becoming pixel counts in section 8. Keeping the exact words has another benefit: if the same request comes back word for word, it's obvious the earlier fix never made it into the game. The list currently has 138 entries.

Prove every test can fail

A test that has never failed might not be testing anything. So every test is checked by deliberately breaking the thing it protects, running it, confirming it fails, and then putting everything back exactly as it was. A small tool does this in one step. It found real problems:

  • A test that checked things were drawn in the right order passed even on a version that never sorted anything, because they happened to come out in the right order anyway.
  • A test for dust kicked up by walking robots still passed after the dust was switched off, because the dust it saw was coming from somewhere else.

Suspect the measurement first

Again and again, a surprising number turned out to be a mistake in how it was measured, not a problem in the game. A test track had its bumps laid in the wrong direction, so the vehicle drove along the top of a ridge. A test ramp drove a truck underwater. A "three times slower" result was just the computer being busy with something else. A reported worst frame was a single frame out of 540 (44, 18 and 14 ms on three identical runs). A profiling tool lumped two different functions with the same name together and blamed the ground for 31.4 ms, when the ground was really 9.1 ms. So now any dramatic number is reproduced before anyone acts on it.

Test the way a player plays

Tests press real keys, run the real game loop and check what actually appears on screen, by reading back the pixels, not by asking the code whether it thinks it did its job. This rule came from experience. Three times, the developer reported "the restart menu doesn't appear", and each time the assistant tested the code in a way that skipped the real game loop and said it worked. Another time, a menu counted four options as "shown" that were actually placed below the bottom of the window.

Slow code is usually repeated work

Nearly every speed-up in the table below came from finding work being done more often than needed, like rebuilding a list that hadn't changed. That's why most fixes produced exactly the same picture, just faster. One lesson went the other way: splitting a task across all the processor cores made that task twice as fast and the whole frame 3.8 ms slower, because the idle cores kept spinning while waiting. Only comparing whole frames, back to back, revealed it. The game now uses a number of cores based on how many the PC has.

13The tools

The project has 60 small tools, each for answering one kind of question and saving the answer where it can be found later. The main ones:

ToolWhat it's for
The test suite1,044 automatic checks covering every part of the game. A full run takes about 42 minutes; you can re-run just the ones that failed last time.
The "prove it fails" toolBreaks one thing on purpose, runs one test, and puts everything back exactly as it was.
The measuring kitShared building blocks for every measurement, so two tools can't measure the same thing differently.
Graphics and stutter labsHow long each drawing step takes on the graphics card, and what was happening during the slowest frames of a flight.
The frame-log readerReads the timing log the game writes while the developer plays, and shows what the slow frames have in common.
Status and code mapSummaries worked out automatically from the code itself, so they can't go out of date.
Trailer and website buildersFilm the trailer and render the showcase site's pictures from the real game.

Each tool exists because of a mistake

  • A test once quietly saved a lower view distance into the developer's own game settings. Now no tool can change the player's settings.
  • A "random" test map was different every time, so a test gave three different answers on three identical runs. Now test maps are fixed.
  • A test that moved a robot by calling the code directly skipped the controls, and reported the robot fine while the player was actually still getting the jet's controls. Now tests press real keys.
  • Screenshots read back from the graphics card come out upside down, and once "proved" that the world was being drawn upside down. It wasn't. Now every captured picture is flipped the right way before anyone sees it.

Making the tests faster

On 22 September the test suite was profiled. Just two tests took 544 seconds between them. One built an entire game session for each of 80 vehicles just to listen to an engine sound; it went from 282 seconds to 66. The other used a very slow method to check whether an engine sound repeated noticeably; it went from 262 seconds to under one. Total test time fell by 22%. Separately, some tests failed only when run in parallel. The cause was one test leaving the game's settings changed for whichever test ran next (for example, allowing 350 particles instead of 2,800). A tool that records what each test leaves behind found it, and after the fix the whole parallel run passed cleanly for the first time.

The developer's own logs beat the test lab

While you play, the game writes down how long each frame took. On 22 September, the developer's own game was running at 16 frames a second while the test lab said 49–60. The lab had never used the developer's settings. Once it did, the cause showed up: the shock rings drawn around bomb blasts were costing 4–12 ms per frame. Moved to the graphics card, they cost at most 0.7 ms.

14Filming the trailer inside the game

The trailer isn't a screen recording. A tool films it from inside the game: each shot is the real game running on the graphics card, on a fixed map, at full HD and 60 frames a second, and every frame is captured straight from the graphics card.

  • All camera angles come from one run. Running the same shot twice doesn't give the same result, because new land arrives from the helper programs at slightly different moments. Enemies ended up in different places within half a second. So every camera (chase, cockpit, orbit, fly-by, wingtip, target) films the same run at once. Slow motion comes from running the game in smaller time steps, not from repeating frames.
  • Computer "pilots" press the real keys. The canyon and low-flying shots are flown through the same controls a player uses, low enough to crash if they get it wrong.
  • The game's sound is recorded as instructions. Instead of recording audio, the tool notes every sound the game plays and when, then mixes it all afterwards, so slow-motion shots get correctly slowed sound.
  • The music is generated too, and the edit is cut to its beat, with each shot's most exciting moment found automatically from how much is happening on screen.
Real slow motion. The game runs in tiny time steps, so every bit of flying debris is actually simulated between frames.
An AC-130 gunship at dusk, firing its 25 mm cannon. Every round in the air is moved by fast compiled code.
The trailer. Every shot was filmed from inside the game by the trailer tool.

15Which PCs it runs on

Most figures in this article were measured on the developer's desktop, which is a fast machine. But the game isn't only played there. It's also played on a mini PC with an AMD Ryzen 7 7730U, a laptop-class processor whose graphics are built into the chip, and on a laptop with Intel Arc graphics, also built into its processor. The developer reports that it runs fine on both.

"Runs fine" isn't a number, so the project now has a benchmark anyone can run: double-click bench.bat in the game's folder. It records the machine (processor, memory, graphics and driver, whether a laptop is on battery), asks the game which quality setting it would choose there, then plays three scenes with the real game loop at full HD: a battle, a jet at full speed over new land, and 36 units fighting on one map. Every machine runs the game's default settings with the same "balanced" quality preset, so the results compare directly.

MachineProcessorGraphicsBattleJet over new land36-unit fight
Developer's desktopAMD Ryzen 9 5950X (16 cores)NVIDIA RTX 3090 card60 fps (the cap)60 fps (the cap)13.5 ms · 74 fps
LaptopIntel Core Ultra 7 258V (8 cores)Intel Arc 140V, built inbeing re-measured with the corrected benchmark (see below)
Mini PCAMD Ryzen 7 7730U (8 cores)AMD Radeon, built inplays fine (developer's report) · benchmark to come

The typical frame as the game plays it, on the "balanced" preset at 1920×1080 on default settings, measured 30 September 2026 by machine_bench.py. The battle and the jet run with the game's usual 60 frames-a-second pace, so a machine that keeps up shows 60; the 36-unit fight runs flat out.

We corrected our own benchmark

The first version of the benchmark waited for the graphics chip to finish every frame before starting the next one. That adds the processor's work and the graphics chip's work together, but the real game overlaps them: the graphics chip draws one frame while the processor prepares the next. Measured both ways on the desktop's 36-unit fight: 19.2 ms waiting each frame, 11.7 ms overlapped. The first numbers published here, like "37 frames a second in the big fight", were too pessimistic for every machine. The benchmark now reports the overlapped frame, and uses the old waiting mode only to split the time between processor and graphics.

That split is still the most useful thing the benchmark found, because the two machines measured so far are limited by opposite things:

Time per frame, 36-unit fight, "balanced"Processor (logic + preparing the frame)Graphics chip
Desktop13.4 ms5.7 ms
Laptop, plugged inabout 11 ms18–24 ms
Laptop, on batteryabout 13 ms44 ms
  • On the desktop, the processor sets the pace. The RTX 3090 finishes its part of the frame in well under half the processor's time.
  • On the laptop, the graphics chip sets the pace. Its modern processor does the game's logic faster than the desktop, but the graphics built into it take longer than the processor does.
  • Plugging the laptop in more than doubles its graphics speed (44 ms of graphics work a frame on battery, 18–24 ms plugged in). Power saving is the biggest single factor measured on it.

The benchmark also times each drawing step on the graphics side. In the 36-unit fight on battery, the laptop spent 35.9 of its 44 ms drawing the ground, against 3.2 ms on the desktop; the vehicles took 4.5 ms (0.1 on the desktop) and the finishing effects 2.8 (0.35). Plugged in, two tuning runs switched graphics effects off one at a time and then in groups (all shadows and effects off, lower resolution, both together). None made a difference larger than the chip's own drift as it warmed up: its unchanged baseline wandered between 18.2 and 24.1 ms. So on this laptop, plugged in, the graphics settings are not what holds it back, and a special settings profile for it isn't justified yet.

The game now also recognises what kind of graphics it's running on: built into the processor, like the laptop's, or a separate graphics card, like the desktop's. It shows this in the Options screen. Built-in graphics never start above the "balanced" preset, because their speed swings with power saving: the laptop's own quick test read 12.7 ms on one launch and 21.5 ms on the next. If the game folder moves to a different PC, or a laptop switches between its built-in graphics and its card, the game notices the change and re-tunes its settings on the next launch. It also notices a laptop on battery: Options says so, and if the settings were first tuned on battery, they're re-tuned the next time it starts plugged in. And on a laptop that has a separate card sitting idle, it says so and explains how to switch.

16All the numbers

What was measuredBeforeAfterWhat changed
Slowest frame, jet flying over new land334.9 / 328.3 ms44.0 / 27.2 mseleven causes of stutter moved to helpers or the loading screen
Slowest frame, battle915.4 / 176.9 ms62.6 / 57.6 msthe same
Map data sent to the graphics card~112 MB every frameonly new stripssend only what changed
Frame rate while that happened40–50 → 1–4 fpsback to normalthe same
Building land for a fast jetfreezes up to 1.3 s0.18 ms typicala separate helper program
Drawing a thunderstorm at 4K38–56 ms10–14 msflashes and weather done by the graphics card
Stuttering frames in a storm21442the same
Preparing each finished frame for the screen9.02 ms0.01 msno more copying the picture
Typical frame in a big fight29.6 ms18.9 / 17.5 msthe same
Drawing soft smoke puffs16.4 ms1.8 msgrouped, instead of 14,170 separate drawings
Stutter when a new kind of tree first appears221.8 ms16.9 msno longer rebuilds every tree picture
Frames with a new crater14.8 ms4.2 msno longer re-sends the whole map
Anti-aircraft fire into a forest16.3 ms2.2 msupdate only the damaged trees (was rebuilding 145 times in 20 s)
Power lines3.0 ms0.2 msdrawn by the graphics card
Bullets in flight2.26 ms1.13 ms172 visibility checks done as one batch
Bomb shock rings, on the developer's settings+4 to +12 msat most +0.7 msdrawn by the graphics card
"Hi-Z" speed-up technique–2% to 27% slowermeasured, and left switched off
Visible mark from a rifle hit0 of 30,905 pixels6.2%scorch, dent, hole and rock
Enemy ground units permanently stuck5 of 531 of 53plans, and nine directions to try
Total time of the test suite9,423 s7,312 stwo slow tests: 544 s → 67 s
Tests passing when run in parallel1,012 of 1,0191,019 of 1,019tests no longer leave settings changed

All measured on the one development PC (RTX 3090 graphics card, full HD unless stated) and recorded in the project's engineering log. Where two numbers are given, the test was run twice.

17Should you copy this?

Our honest advice for anyone starting a game like this: copy the techniques and the habits, and think carefully before copying the choice of language.

IdeaVerdictWhy
Drawing the ground from a height map on the graphics cardKEEPFor flying and fighting over big landscapes it's hard to beat. The cost depends on screen size, not world size, and a crater is just a change to one image.
Building the world in separate helper programsKEEPIn Python it's the only way that works: a background thread was 35 times slower. Never make two programs wait on each other.
A low-detail copy of the real land for the horizonKEEP7.3 MB gives a real horizon about 4.5 km away, accurate to a third of a pixel.
Vehicles made of boxes, drawn in one batch eachKEEPHundreds of vehicles for well under a millisecond. Damage and moving parts cost a few numbers each.
Everything generated from codeKEEPNo art pipeline, and any model or sound can be changed by editing code. Build them on the loading screen, not mid-game.
Proving tests fail, reading the player's own logsKEEPThe habit that paid off most. Passing tests that were never shown to fail missed real problems several times.
Python as the languageONLY IFIt works here because the heavy work is compiled or on the graphics card. The rest (enemy AI, rules, display) costs far more than it would in C#, Rust or C++. Choose it to learn, or because you love Python.
A single height for every spot of groundIMPROVENo overhangs, bridges or caves, and buildings are just tall spots. Real 3D buildings would improve cities most.
The "hi-Z" speed-upSKIPMeasured slower here; the big leaps through open air already do its job.
Checking what's hidden on the main processorSKIPWhen the graphics card already knows, it's both wasted work and wrong: vehicles blink.

18What's missing

  • The mini PC's benchmark, and the laptop plugged in. The desktop and the Arc laptop are measured (section 15); the Ryzen mini PC is next, and the laptop's run was on battery.
  • Cheaper ground drawing for graphics-limited PCs. On the laptop, drawing the ground is 80% of the graphics work on battery (36 of 44 ms in a big fight). Plugged in, no graphics setting stood out, so the next step is finding what else takes the chip's time there.
  • A faster core, one piece at a time. The plan is written down: first add timers to the parts of a big fight that aren't measured yet; then lay the busiest data (every unit's position, speed, team and so on) out as simple tables, and hand the heavy loops over those tables to compiled code, starting with how enemies spot, choose and avoid each other. Each piece keeps the same connections to the rest of the game, is checked to produce exactly the same results as the Python it replaces, and can be switched back. If something ever needs more than today's compiler can give, the next step is Rust, delivered ready-built so players never compile anything. A full rewrite isn't planned: it would throw away the game and more than a thousand tests to gain speed that careful, piece-by-piece work can get.
  • Two ideas for that are already measured. They haven't been built yet. One sets a step limit for each quality setting. The other starts each pixel's search from where it found the ground last frame; that could skip 41–66% of the work, but isn't yet safe when things move or appear suddenly.
  • Real 3D buildings. This is the honest limit of the 1992 trick. Cities would benefit most.
  • Erosion in the endless worlds. The fixed maps smooth their peaks; the endless ones don't, and valleys would look more natural carved by water.
  • Online play. Multiplayer sends each player's position about 20 times a second in small messages of about 48 bytes, and every computer builds the same world from the same recipe. That's fine on a home network, but not yet built for the open internet.

19Talking points

Questions this project raises and doesn't settle. They're here to argue about.

  1. Is a 1992 trick still the right tool? It makes huge landscapes cheap and craters easy, but it can't draw a bridge you fly under. For a flying-and-fighting game, is that a good trade or a ceiling hit early?
  2. Can you really build a game engine in Python? Here it works because the heavy lifting is handed off. Even so, the main processor, not the graphics card, set the pace. Where would you draw the line?
  3. Is all this measuring the product or the overhead? 250 of the 862 lessons are about testing and measuring, and the test file is the biggest file in the project. It also caught passing tests hiding missing mountains. How much of it would a team keep?
  4. Would you trust a test you've never seen fail? Checking every test this way found several that passed no matter what. Most projects never check even once.
  5. When an AI writes the code, where does the design live? Here, in the developer's exact words, kept until they've played the result, and in a 22,000-line log of what was measured and why. Is that enough, or does something get lost?
  6. How many "standard" speed-ups are actually measured? Here, a well-known technique made drawing slower, spreading work across all cores made the game slower, and a common upload trick made uploads slower.

20Lessons learned

The project keeps a list of 862 one-line lessons, each linked to the measurement that taught it. These are the ones that apply to almost any project:

  1. Passing tests mean nothing if none of them checks what the user actually sees. 348 of 348 passed while the jet flew toward a mountain that wasn't there.
  2. A test you've never seen fail may not be testing anything. Break the thing on purpose and watch the test catch it.
  3. When a number looks dramatic, suspect the measurement first. Reproduce it before acting on it.
  4. Don't make the same decision in two places. When the graphics card already knows what's hidden, a second check on the main processor is slower and wrong.
  5. Slowness is usually repeated work, not a bad method. Look for work done more often than anything changes.
  6. In Python, real parallel work needs separate programs, and speeding up one part can slow down the whole. Always time the whole thing.
  7. Python's quick ID numbers get reused. They merged missiles, raised a destroyed hangar and swapped tree pictures. Use permanent serial numbers.
  8. Say exactly what you mean. Store a height as a height, not as a speed that happens to produce it, and check that something has actually arrived safely, not just that it was placed.
  9. To measure something see-through, compare with and without it.
  10. A resting graphics card can look like a slow one.
  11. Keep requests in the asker's own words. A request repeated word for word is the clearest sign a fix didn't land.
  12. Delete abandoned code, don't just switch it off. Switched-off code with confident comments invites someone to make the same mistake again.