Game Loops & ECS

Every game is one loop. Read input, advance the simulation, draw the result, repeat — sixty times a second, indefinitely. Almost every architectural question in game development is downstream of two decisions about that loop: how you advance time, and how you lay out the entities you advance.

The first decision is the fixed timestep. Advancing the simulation by however long the last frame happened to take is the obvious approach and it's wrong — physics becomes non-deterministic, collisions get missed on slow frames, and the same replay produces different results on different hardware. The second decision is the transition from object-oriented entity hierarchies to entity-component-system design, where entities are just IDs, components are plain data in contiguous arrays, and systems are functions over those arrays. The first is about correctness; the second is about the CPU cache.

TL;DR

Quick Example

The canonical fixed-timestep loop with interpolated rendering:

Three separate ideas live in that loop: the simulation is deterministic because integrate always receives the same dt; rendering is smooth at any refresh rate because it interpolates between the two most recent states; and a hitch can't cascade because frameTime is clamped.

Core Concepts

Delta time

Every value that expresses a rate — velocity, angular speed, damage over time, a lerp factor — must be scaled by delta time. This is the first thing every engine teaches and still the most common bug in prototype code.

One subtlety: naive exponential smoothing is not frame-rate independent even with delta time.

Why a variable timestep breaks physics

Determinism isn't an academic property. It's what makes replays work, what makes rollback netcode possible, and what makes a physics bug reproducible instead of a ghost.

The spiral of death

If a frame takes longer than the fixed step, the accumulator asks for extra simulation steps. Those steps cost time. If they cost more than they consume, the accumulator grows every frame and the game freezes solid.

⚠️ Warning: Always clamp the maximum frame time fed into the accumulator (MAX_FRAME above). Without it, one long stall — a garbage collection pause, an asset load, dragging the window — can lock the game permanently.

The tradeoff of clamping is that the simulation runs slower than wall-clock time during a stall. That's the correct choice: slow motion is recoverable, a freeze is not.

Interpolation vs. extrapolation

With a 60 Hz simulation on a 144 Hz display, most rendered frames fall between simulation steps. Interpolating between the previous and current state renders one step in the past but always smooth and always correct. Extrapolating forward from velocity has no latency but overshoots on collisions, producing visible snap-back. Interpolation is the default for good reason.

Entity-Component-System

The problem ECS solves

Composition alone fixes the combinatorics. ECS goes further and fixes the memory layout.

Cache locality — the actual reason

A main-memory access is on the order of a hundred times slower than an L1 hit. Iterating ten thousand entities over packed arrays instead of scattered objects can be an order of magnitude faster with identical logic. That is the whole argument for ECS, and it's why the technique is usually described as data-oriented design rather than an architectural pattern.

The three parts

Adding flight to an enemy means adding a Flight component. Removing it at runtime means removing the component. No class changes, no combinatorial explosion, and the systems that care are exactly the ones whose queries now match.

Where ECS is worth it

ECS also imposes real costs: debugging is harder (an entity's state is scattered across arrays), one-off special-case behavior fits badly, and most engines' UI, animation, and audio subsystems still expect objects. Hybrid approaches — ECS for the simulation-heavy parts, objects for everything else — are common and sensible.

Implementations

Best Practices

Separate simulation from presentation

Simulation runs at a fixed rate and owns the truth. Rendering runs at display rate and reads an interpolated view of it. Mixing the two — updating a transform inside a draw call, reading input during physics — produces bugs that only appear at particular frame rates.

Cap the accumulator, and log when you do

Clamping prevents the spiral of death, but a clamp that fires regularly means the simulation is over budget. Count clamped frames and surface it in a debug overlay; it's an early warning that would otherwise show up as "the game feels weird sometimes."

Keep systems small and single-purpose

movement_system, collision_system, damage_system — each one doing one thing over one query. Small systems are trivially parallelizable (they declare exactly what they read and write) and much easier to reason about than an omnibus update_everything.

Make components data, not objects

A component with methods that mutate other components has reintroduced the coupling ECS removes. Behavior belongs in systems. The discipline pays off precisely when you want to parallelize.

Order systems explicitly

Input → AI → movement → collision → damage → cleanup. Implicit ordering derived from registration order is a source of one-frame-lag bugs that are extremely hard to trace. Most ECS frameworks let you declare dependencies; use them.

Prefer removing components to boolean flags

if (!entity.isAlive) continue; inside every system means every system pays for dead entities. Removing the component (or the entity) means the query simply doesn't match — the check disappears.

Don't adopt ECS to be modern

The gain is real and specific: thousands of entities, cache pressure, parallelism. Below that threshold you pay the complexity and get nothing back. Composition over inheritance gets you most of the architectural benefit at none of the cost.

Common Mistakes

Forgetting delta time

Using variable timestep for physics

Omitting the frame-time clamp

Rendering the raw simulation state

Putting logic in components

Allocating inside the loop

Per-frame allocation is the fastest way to introduce frame-time spikes, whether from a garbage collector or from allocator contention. Pool objects, reuse buffers, and preallocate anything sized by entity count.

FAQ

What frame rate should the simulation run at?

60 Hz is the common default and pairs well with 60 FPS displays. Physics-heavy or fast-moving games often use 120 Hz for accuracy; turn-based or slow simulations can run much lower. What matters is that it's fixed and that the simulation reliably fits in its budget — the rendering frame rate is independent of it either way.

Can I just use my engine's loop?

Yes, and you usually should. Unity's FixedUpdate, Godot's _physics_process, and Unreal's tick groups all implement fixed-timestep simulation with interpolation. The value in understanding the underlying loop is knowing why those callbacks exist and which of your code belongs in which one.

Is ECS always faster?

No. It's faster when you iterate many entities with the same component set — that's when cache locality dominates. With few entities, or with access patterns that jump randomly between components, the overhead of the ECS machinery can exceed the gain. Measure with profiling on real data rather than assuming.

How does ECS interact with hierarchies?

Awkwardly, which is its main weak point. Parent-child transforms (a gun attached to a hand attached to an arm) are natural in a scene graph and require an explicit Parent component plus a transform-propagation system in ECS. Most mature ECS libraries provide this, but it's added machinery rather than something you get for free.

Should I write my own ECS?

Writing a simple one is an excellent way to understand the idea — a sparse set, a few component arrays, and a query iterator is a weekend. Shipping on your own is a different proposition: archetype storage, change detection, system scheduling, and parallelism are where the real work lives. Use EnTT, flecs, Bevy, or your engine's implementation for production.

What's the relationship between ECS and data-oriented design?

Data-oriented design is the broader principle: structure code around how data is laid out and accessed, because memory access dominates performance on modern hardware. ECS is the most common application of it in games. You can apply DOD without ECS — sorting entities to improve locality, converting arrays of structs to structs of arrays in a hot loop — and get much of the benefit incrementally.

Related Topics

References