All posts

Keeping game logic apart from presentation

EraClash is a strategy game where two players spawn soldiers from their castles and watch the battle live. In an online match both phones have to compute the same battle exactly alike. So the first rule of the game became: logic and presentation are two separate layers.

The simulation is a pure layer

The game logic lives under src/sim/ and may not use Node, scenes, Engine, Time, OS, randf(), randi() or the physics engine. It is made of plain RefCounted classes only.

  • No floats. Every value is an integer. Positions are in “milli-units”, with a lane length of 140,000.
  • A fixed time step. The simulation runs at 20 ticks per second. There is no delta time, everything is counted in ticks.
  • Randomness from one place. When it is needed, only SimRng (xorshift32) is used, seeded per match.
  • A fixed order. Entities always update in entity_id order; the iteration order of unordered collections is never relied on.

Presentation only reads the simulation

Animation, effects, sound and skins look at the state the simulation produces after every tick. They interpolate between two ticks to smooth things out, but they never change the state. Effects like hit-stop and screen shake live in the presentation only and never slow the simulation down.

Everything is a command

The player, bots and the online opponent affect the simulation the same way: through Command objects. That is why the same command sequence gives the same result, and why recording a match and replaying it to verify the outcome is easy.

What it bought

In an online match both clients run the same commands on the same tick and compare a summary of the simulation state to catch any divergence. Because the logic is independent of the engine, unit tests run without opening a scene.

There is a cost too: working in integers and turning everything into commands is slower at first. But in an online game, the guarantee that two devices find the same result matters more than that cost.