# Writing a Game Engine Core

### Writing a Game Engine Core

A game engine core is the small central layer that runs the game.

It does not need to contain everything. A serious engine may have rendering, physics, audio, networking, animation, scripting, asset loading, editor tools, and platform layers. The core is smaller than that.

The core usually answers these questions:

```text
What objects exist?
How is game state stored?
What code runs each frame?
How does time move forward?
How do systems communicate?
Who owns memory?
```

A good engine core is boring. It gives the game a stable loop and clear data ownership.

#### The Game Loop

Most games run inside a loop.

Conceptually:

```text
while the game is running:
    read input
    update simulation
    render frame
```

In Zig-like pseudocode:

```zig
while (running) {
    try pollInput();
    try update(dt);
    try render();
}
```

The variable `dt` usually means delta time. It is the amount of time that passed since the previous frame.

If the previous frame happened 16 milliseconds ago, then:

```text
dt = 0.016 seconds
```

The update step uses `dt` so movement can be based on time instead of frame count.

#### A Minimal Engine Type

Start with one central type.

```zig
const std = @import("std");

const Engine = struct {
    allocator: std.mem.Allocator,
    running: bool,

    pub fn init(allocator: std.mem.Allocator) Engine {
        return Engine{
            .allocator = allocator,
            .running = true,
        };
    }

    pub fn deinit(self: *Engine) void {
        _ = self;
    }

    pub fn run(self: *Engine) !void {
        while (self.running) {
            try self.update(1.0 / 60.0);
            try self.render();
        }
    }

    fn update(self: *Engine, dt: f32) !void {
        _ = self;
        _ = dt;
    }

    fn render(self: *Engine) !void {
        _ = self;
    }
};
```

This engine does almost nothing, but the shape is useful:

```text
init creates the engine
deinit releases resources
run owns the main loop
update changes game state
render draws the current state
```

#### Fixed Time Step

A simple loop uses the real time between frames. That is called a variable time step.

A fixed time step updates the simulation using a constant amount of time, such as `1 / 60` seconds.

```zig
const fixed_dt: f32 = 1.0 / 60.0;
```

Fixed time steps are useful for physics and deterministic simulation.

Conceptually:

```text
collect elapsed time
while elapsed time >= fixed_dt:
    update simulation by fixed_dt
    subtract fixed_dt
render
```

This keeps simulation updates stable even when rendering speed changes.

A simplified version:

```zig
var accumulator: f32 = 0.0;
const fixed_dt: f32 = 1.0 / 60.0;

while (running) {
    const frame_time = measureFrameTime();
    accumulator += frame_time;

    while (accumulator >= fixed_dt) {
        try update(fixed_dt);
        accumulator -= fixed_dt;
    }

    try render();
}
```

This pattern separates simulation rate from render rate.

#### Game State

Game state is all the data that describes the current world.

Examples:

```text
player position
enemy health
current map
camera state
inventory
projectiles
timers
animation state
```

A simple game object might look like this:

```zig
const Vec2 = struct {
    x: f32,
    y: f32,
};

const Entity = struct {
    position: Vec2,
    velocity: Vec2,
    alive: bool,
};
```

Then the world can store many entities:

```zig
const World = struct {
    entities: std.ArrayList(Entity),

    pub fn init(allocator: std.mem.Allocator) World {
        return World{
            .entities = std.ArrayList(Entity).init(allocator),
        };
    }

    pub fn deinit(self: *World) void {
        self.entities.deinit();
    }
};
```

The engine owns the world:

```zig
const Engine = struct {
    allocator: std.mem.Allocator,
    world: World,
    running: bool,
};
```

This makes ownership clear.

#### Updating Entities

A basic update moves entities according to velocity.

```zig
fn updateWorld(world: *World, dt: f32) void {
    for (world.entities.items) |*entity| {
        if (!entity.alive) continue;

        entity.position.x += entity.velocity.x * dt;
        entity.position.y += entity.velocity.y * dt;
    }
}
```

The `|*entity|` syntax gives a pointer to each entity, so the loop can modify it.

Without the pointer, the loop receives a copy.

```zig
for (world.entities.items) |entity| {
    // entity is a copy
}
```

With a pointer:

```zig
for (world.entities.items) |*entity| {
    // entity points to the real item
}
```

This is an important Zig detail for game code.

#### Data-Oriented Design

Game engines often process many objects every frame.

When many objects share the same fields, memory layout matters.

An array of structs looks like this:

```zig
const Entity = struct {
    position: Vec2,
    velocity: Vec2,
    health: i32,
};

entities: []Entity
```

Memory layout:

```text
position, velocity, health
position, velocity, health
position, velocity, health
```

A struct of arrays looks like this:

```zig
const EntityStore = struct {
    positions: []Vec2,
    velocities: []Vec2,
    healths: []i32,
};
```

Memory layout:

```text
positions, positions, positions
velocities, velocities, velocities
healths, healths, healths
```

If the update only needs positions and velocities, the second layout can be more cache-friendly.

Zig gives you enough control to choose either layout.

Start simple. Change layout when profiling shows a real problem.

#### Handles Instead of Raw Pointers

Game objects often come and go. If you store raw pointers to entities, those pointers can become invalid when the entity list reallocates or an entity is removed.

A safer design is to use handles.

```zig
const EntityId = struct {
    index: u32,
    generation: u32,
};
```

The index points into an array. The generation helps detect stale handles.

Conceptually:

```text
create entity at slot 5, generation 1
destroy entity at slot 5
create new entity at slot 5, generation 2
old handle has generation 1, so it is rejected
```

This avoids many use-after-free style bugs.

#### Systems

A system is code that updates one part of the game.

Examples:

```text
movement system
physics system
animation system
AI system
collision system
render system
audio system
```

A system usually receives the world and modifies some part of it.

```zig
fn movementSystem(world: *World, dt: f32) void {
    for (world.entities.items) |*entity| {
        if (!entity.alive) continue;

        entity.position.x += entity.velocity.x * dt;
        entity.position.y += entity.velocity.y * dt;
    }
}
```

The engine update can call systems in order:

```zig
fn update(self: *Engine, dt: f32) !void {
    movementSystem(&self.world, dt);
    collisionSystem(&self.world);
    animationSystem(&self.world, dt);
}
```

Order matters. Physics before animation may produce different results than animation before physics.

Make system order explicit.

#### Events

Systems sometimes need to communicate.

Example:

```text
collision system reports that player touched coin
audio system plays sound
score system increments score
render system shows effect
```

One approach is an event queue.

```zig
const Event = union(enum) {
    coin_collected: struct {
        player: EntityId,
        coin: EntityId,
    },
    entity_died: struct {
        entity: EntityId,
    },
};
```

The world or engine can store events:

```zig
events: std.ArrayList(Event)
```

Systems push events. Later systems read them.

Keep events small and clear. Avoid using events as a hidden global control system.

#### Memory Strategy

Game engines allocate many resources:

```text
textures
meshes
sounds
levels
entities
temporary frame data
strings
commands
```

A good engine core separates memory lifetimes.

Common lifetimes:

```text
whole program lifetime
level lifetime
frame lifetime
temporary scratch lifetime
asset lifetime
```

Zig allocators make this explicit.

Example:

```zig
const Engine = struct {
    gpa: std.mem.Allocator,
    frame_arena: std.heap.ArenaAllocator,
    level_arena: std.heap.ArenaAllocator,
};
```

Frame memory can be cleared every frame.

Level memory can be cleared when changing maps.

This reduces leaks and simplifies cleanup.

#### Frame Arena

A frame arena is useful for temporary data.

```zig
var frame_arena = std.heap.ArenaAllocator.init(parent_allocator);
defer frame_arena.deinit();

const frame_allocator = frame_arena.allocator();
```

At the end of each frame, reset the arena.

Conceptually:

```zig
_ = frame_arena.reset(.retain_capacity);
```

Then all temporary frame allocations disappear at once.

This is often better than freeing many small allocations individually.

#### Resource Ownership

An engine core should make ownership boring.

If the engine loads a texture, who frees it?

If a level owns entities, when are they destroyed?

If audio owns sound buffers, can gameplay code keep pointers to them?

Define these rules early.

A simple rule:

```text
The engine owns global systems.
The world owns entities.
The asset manager owns loaded assets.
Gameplay code uses handles, not raw asset pointers.
```

Handles keep ownership centralized.

#### Asset Handles

A texture handle might be:

```zig
const TextureHandle = struct {
    index: u32,
    generation: u32,
};
```

Gameplay code stores the handle:

```zig
sprite_texture: TextureHandle
```

The renderer resolves the handle when needed.

This avoids spreading raw graphics API objects throughout game logic.

#### Platform Layer

The engine core should not know too much about the operating system.

Create a platform layer for:

```text
window creation
input
timing
file access
graphics surface
audio device
```

Then the core can depend on an interface rather than platform-specific code.

Example shape:

```zig
const Platform = struct {
    pollEvents: *const fn (*Platform) anyerror!void,
    shouldClose: *const fn (*Platform) bool,
    nowSeconds: *const fn (*Platform) f64,
};
```

The exact design depends on how low-level you want the engine to be.

#### Rendering Boundary

Do not mix gameplay logic with rendering API details.

Bad:

```text
player update directly calls Vulkan or OpenGL functions
```

Better:

```text
gameplay updates state
render system reads state
renderer submits draw commands
```

A draw command might be:

```zig
const DrawSprite = struct {
    texture: TextureHandle,
    position: Vec2,
    size: Vec2,
};
```

The render system builds draw commands. The renderer backend turns them into graphics API calls.

This keeps gameplay code portable.

#### Determinism

Some games need deterministic simulation.

That means the same inputs produce the same results.

Determinism matters for:

```text
replays
lockstep multiplayer
tests
debugging
simulation tools
```

To improve determinism:

```text
use fixed time steps
avoid unordered iteration when order matters
control random seeds
be careful with floating point
avoid reading wall-clock time inside simulation logic
```

The engine core should decide where nondeterminism is allowed.

#### Hot Reloading

Advanced engines often support hot reloading.

Examples:

```text
reload textures when files change
reload shaders
reload scripts
reload level data
```

Hot reloading is useful, but it complicates ownership.

A safe first version is asset hot reload:

```text
asset manager watches file timestamp
asset manager reloads texture
old handle now points to new data
```

Avoid hot reloading core Zig code until the engine architecture is already stable.

#### Error Handling in a Game Engine

A game engine has different kinds of errors.

Some are fatal:

```text
graphics device cannot initialize
required asset directory missing
out of memory during startup
```

Some are recoverable:

```text
optional sound missing
bad config value
failed hot reload
network packet malformed
```

Use Zig errors for recoverable failure.

For fatal startup failure, return an error from initialization.

```zig
pub fn init(allocator: std.mem.Allocator) !Engine {
    // fail if required systems cannot start
}
```

Avoid panicking for normal failures. Panic is for bugs and impossible states.

#### Testing the Core

A good engine core can be tested without opening a window.

Test:

```text
entity creation and deletion
handle generation
movement system
event queue
asset handle lookup
fixed time step accumulator
serialization
```

Do not make every test depend on graphics or audio.

Example:

```zig
test "movement updates position" {
    var world = World.init(std.testing.allocator);
    defer world.deinit();

    try world.entities.append(.{
        .position = .{ .x = 0, .y = 0 },
        .velocity = .{ .x = 10, .y = 0 },
        .alive = true,
    });

    updateWorld(&world, 0.5);

    try std.testing.expectEqual(@as(f32, 5.0), world.entities.items[0].position.x);
}
```

This test checks gameplay logic without rendering anything.

#### Keep the Core Small

A common mistake is making the engine core too large.

Do not put everything in `Engine`.

Bad shape:

```text
Engine owns every object, every renderer detail, every input detail, every asset detail, every gameplay rule
```

Better shape:

```text
Engine owns the loop
World owns game state
Systems update game state
AssetManager owns assets
Renderer draws
Platform talks to the OS
```

The core coordinates. It should not become a dumping ground.

#### The Main Idea

A game engine core is the stable center of the game.

It owns the loop, time step, world update order, memory strategy, and boundaries between systems.

In Zig, this fits naturally because Zig makes ownership and allocation explicit. You can build the core from simple structs, arrays, handles, function calls, and allocators.

Start with a small loop and a clear world model. Add systems only when the game needs them.

