Home / 05 — The Loop

The Simulation

Everything above exists to let one function run identically everywhere: the per-frame update. Here's what happens inside a single tick — 24 times a second — and how units, animation, and memory are organized to make it fast and reproducible.

The heartbeat

24 frames a second, every one identical.

The simulation advances in fixed logical frames — 24 per second. Each frame is a deterministic function of the last frame plus that frame's commands. Game speed changes only the wall-clock time per frame, never the logic — which is why a replay plays identically at any speed.

// bwgame.h:13083 — the whole frame, in order
void process_frame() {
  recede_creep();                          // Zerg creep slowly retreats
  if (st.update_tiles_countdown == 0) st.update_tiles_countdown = 100;
  --st.update_tiles_countdown;
  update_tiles = st.update_tiles_countdown == 0;  // fog-of-war every 100 frames
  update_units();                          // orders, movement, combat
  update_bullets();                        // projectiles in flight
  update_thingies();                       // effects, sprites, splatter
}

The order is fixed and so is the iteration order of every list inside it. That determinism-of-ordering matters as much as the fixed-point math: if two machines updated units in a different sequence, an RNG call could land on a different unit and desync.

one frame, stage by stage

step through process_frame() · watch the frame counter and per-stage work

Units

Each unit is a small state machine.

A unit is a small bundle of state:

Updating one unit runs, in order: main order → secondary order (gather/repair/addon) → subunit (e.g. a tank turret) → animation:

// bwgame.h:8445
void update_unit(unit_t* u) {
  update_unit_values(u);        // hp/shield/energy regen, timers
  execute_main_order(u);        // the big order switch
  execute_secondary_order(u);   // gather, repair, build addon...
  if (u->subunit && !ut_turret(u)) update_unit(u->subunit);
  if (u->sprite && !iscript_execute_sprite(u->sprite)) u->sprite = nullptr;
}
The order switch — one giant dispatch deeper

execute_main_order (bwgame.h:7620) is a large switch over the Orders enum routing to per-order handlers: order_Die, movement+pathfinding for Move, target tracking and firing for AttackUnit/AttackMove, ConstructingBuilding, Train, plus race-specifics like ZergBirth, ZergUnitMorph, and Protoss warp-in. Each handler is a step of a state machine — it reads order_state, does a little work, maybe advances the state or pops the order queue.

The build queue and a faithfully-reproduced quirk deeper

A production building holds up to 5 queued units (static_vector<const unit_type_t*, 5> build_queue). OpenBW even reproduces a strange original behavior via a build_queue_limbo field — recent commits (a59617d, 0fa172f) exist specifically to replicate BW's quirky queue handling rather than "fix" it. In a determinism-faithful rewrite, matching the original's bugs is correctness: a desync against real BW would otherwise occur.

Animation as code

iscript: a tiny virtual machine per image.

Brood War's animations aren't just frame sequences — they're programs. Each graphical image runs a bytecode script (iscript) on a little stack-less VM with a program counter. Opcodes play frames, wait, turn, fire weapons, play sounds, spawn sprites, and crucially signal the order system — so the visual animation and the game logic stay welded together and deterministic.

// data_types.h:373 — a sample of the ~44 iscript opcodes
opc_playfram, opc_wait, opc_waitrand, opc_goto, opc_call, opc_return,
opc_attack, opc_attackwith, opc_move, opc_turn1cwise, opc_turnccwise,
opc_playsnd, opc_sigorder, opc_orderdone, opc_creategasoverlays, ...

// game_types.h:392 — the VM's state per image
struct iscript_state_t {
  const iscript_t::script* current_script;
  size_t program_counter;   // byte offset into the bytecode
  size_t return_address;    // for call/return
  int animation;            // Init, Walking, GndAttkInit, Death...
  int wait;                 // frames to idle before next opcode
};
iscript VM — step a Marine's attack animation

a stylized GndAttkInit script · watch the program counter, wait timer, and when it signals the order system

How wait ties the VM to the 24fps clock deeper

When the VM hits opc_wait N, it stores N in wait and stops. On each game frame, wait is decremented; only when it reaches zero does the VM resume executing opcodes. That's how an animation paces itself to real frames without any timers — the frame loop is the clock. Because every machine runs the same frames, every machine's animations advance in lockstep too. opc_sigorder/opc_orderdone flip flags the order state machine reads, so "the recoil animation finished" can drive "the shot fires."

Memory model

Fixed pools, intrusive lists, no surprises.

Determinism extends to memory layout. The engine pre-allocates fixed-capacity object pools and threads objects onto intrusive linked lists — the list pointers live inside the objects themselves. There's no general-purpose allocator churning during a frame, no pointer addresses leaking into game logic, and allocation/free is O(1).

// game_types.h:47 — fixed-capacity pools (capacity, chunk size)
object_container<unit_t,   1700, 17> units_container;
object_container<bullet_t,  100, 10> bullets_container;
object_container<sprite_t, 2500, 25> sprites_container;
object_container<image_t,  5000, 50> images_container;
1700
max units
100
max bullets
2500
max sprites
5000
max images

Units live on several lists at once — visible_units, hidden_units, per-player lists, cloaked_units — each via a different intrusive link, so a unit can be on the "all visible" list and its owner's list simultaneously without any extra allocation.

Save / restore: the state_copier deeper

State is split into state_base_copyable (everything that can be snapshotted: frame number, economy, RNG state, tiles) and state_base_non_copyable (runtime lists and pools). The state_copier (bwgame.h:19551) deep-copies the world by walking the object containers by index and remapping every pointer — unit→unit, sprite→sprite — so a restored snapshot has internally consistent references. This is what lets tools and AI rewind and branch the simulation. Recent commits (a59617d) fixed the copier's handling of the build_queue_limbo quirk — even the save system must preserve quirks to stay deterministic.

Pathfinding: regions, then waypoints deeper

Maps are decomposed into walkable regions. Long-distance pathing runs A* over the region adjacency graph (cheap — far fewer regions than tiles); short-distance pathing then produces concrete waypoints within and between adjacent regions (capped around 24 waypoints). Walkability is resolved per-tile, with partially-walkable tiles falling back to finer vf4 sub-tile flags (bwgame.h:767). A spatial index — units kept sorted on X and Y — answers "what's near this box?" for collision and targeting without scanning all 1700 units. Tiles are 32×32 px; walkability granularity is 8×8 px "walk tiles."