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
- The game loop is input → update → render, running continuously.
- Delta time makes movement frame-rate independent: multiply every rate by elapsed seconds.
- A fixed timestep for simulation plus interpolation for rendering is the standard correct pattern.
- Cap the accumulator or a slow frame triggers the spiral of death.
- ECS separates identity (entity ID), data (components in packed arrays), and behavior (systems iterating those arrays).
- ECS wins through cache locality, not through cleverness — it's a memory-layout optimization with an architectural side effect.
- Deep inheritance hierarchies (
Enemy → FlyingEnemy → FlyingShootingEnemy) collapse under combinatorics; composition doesn't. - You don't need ECS for most games. You need composition; ECS is one implementation of it.
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_FRAMEabove). 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
- Unity DOTS — Entities, the Job System, and the Burst compiler
- Bevy — Rust engine built around ECS from the ground up
- EnTT — the widely used header-only C++ library
- flecs — C/C++ ECS with relationships and hierarchies
- Godot — node-based rather than ECS; composition without the data layout
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
- Game Physics — Why fixed timesteps matter for integration and collision
- Unity —
Update/FixedUpdateand the DOTS ECS stack - Godot —
_process/_physics_processand node composition - Unreal Engine — Tick groups and Mass Entity
- Multiplayer Netcode — Determinism as a prerequisite for rollback
- Memory Management — Cache behavior and allocation strategy
- Performance Optimization — Where the frame budget actually goes
- Design Patterns — Composition over inheritance in general
- Rust — Bevy and the ECS-heavy Rust game ecosystem