# Build a Simple Game Engine

### Build a Simple Game Engine

A game engine is a program structure for running interactive simulations.

A game usually repeats the same cycle:

```text
read input
update world
draw frame
repeat
```

That cycle is called the game loop.

In this project, we will build a very small terminal game engine. It will not use graphics, windows, audio, or GPU APIs. It will run in the terminal so the core ideas stay visible.

#### The Goal

We will build a small engine that moves a player on a 2D grid.

The world looks like this:

```text
..........
..........
....@.....
..........
..........
```

The `@` is the player.

The player can move:

```text
w -> up
s -> down
a -> left
d -> right
q -> quit
```

This project teaches:

```text
game loop
world state
input handling
frame rendering
bounds checking
separating engine code from game code
```

#### The World

Start with a simple position type:

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

Now define the game state:

```zig
const Game = struct {
    width: i32,
    height: i32,
    player: Vec2,
    running: bool,
};
```

The game stores the world size, the player position, and whether the game should continue running.

#### Initialize the Game

Add:

```zig
fn initGame() Game {
    return .{
        .width = 10,
        .height = 5,
        .player = .{
            .x = 4,
            .y = 2,
        },
        .running = true,
    };
}
```

This creates a 10 by 5 grid with the player near the center.

#### Updating the Game

An update changes the game state.

For this first engine, input is one byte:

```zig
fn update(game: *Game, input: u8) void {
    var next = game.player;

    switch (input) {
        'w' => next.y -= 1,
        's' => next.y += 1,
        'a' => next.x -= 1,
        'd' => next.x += 1,
        'q' => game.running = false,
        else => {},
    }

    if (next.x >= 0 and
        next.x < game.width and
        next.y >= 0 and
        next.y < game.height)
    {
        game.player = next;
    }
}
```

The player moves only if the new position stays inside the world.

This is bounds checking.

Without it, the player could move outside the grid.

#### Rendering the Game

Rendering means drawing the current state.

In a terminal game, rendering means printing text.

```zig
fn render(game: Game) void {
    std.debug.print("\x1B[2J\x1B[H", .{});

    var y: i32 = 0;
    while (y < game.height) : (y += 1) {
        var x: i32 = 0;
        while (x < game.width) : (x += 1) {
            if (game.player.x == x and game.player.y == y) {
                std.debug.print("@", .{});
            } else {
                std.debug.print(".", .{});
            }
        }

        std.debug.print("\n", .{});
    }

    std.debug.print("\nuse w/a/s/d to move, q to quit\n", .{});
}
```

This line clears the terminal and moves the cursor to the top-left:

```zig
std.debug.print("\x1B[2J\x1B[H", .{});
```

It uses ANSI escape codes. Most modern terminals support them.

#### Reading Input

For a simple terminal program, we can read a line and use the first character.

```zig
fn readInput() !u8 {
    var stdin_buffer: [128]u8 = undefined;
    var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer);
    const stdin = &stdin_reader.interface;

    var line_buffer: [128]u8 = undefined;

    const line = (try stdin.takeDelimiterExclusive(
        '\n',
        &line_buffer,
    )) orelse return 'q';

    if (line.len == 0) {
        return 0;
    }

    return line[0];
}
```

This means the user must press Enter after each command.

A real-time game would read keys immediately without waiting for Enter. That requires terminal raw mode, which is more platform-specific. For this beginner project, line input is enough.

#### The Game Loop

Now we can build the loop:

```zig
pub fn main() !void {
    var game = initGame();

    while (game.running) {
        render(game);

        const input = try readInput();

        update(&game, input);
    }

    std.debug.print("goodbye\n", .{});
}
```

The shape is the important part:

```text
while running:
    render
    read input
    update
```

Many real engines use a slightly different order:

```text
while running:
    read input
    update
    render
```

Both are fine for this simple terminal game.

#### Complete Program

Put this in `src/main.zig`:

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

const Vec2 = struct {
    x: i32,
    y: i32,
};

const Game = struct {
    width: i32,
    height: i32,
    player: Vec2,
    running: bool,
};

fn initGame() Game {
    return .{
        .width = 10,
        .height = 5,
        .player = .{
            .x = 4,
            .y = 2,
        },
        .running = true,
    };
}

fn update(game: *Game, input: u8) void {
    var next = game.player;

    switch (input) {
        'w' => next.y -= 1,
        's' => next.y += 1,
        'a' => next.x -= 1,
        'd' => next.x += 1,
        'q' => game.running = false,
        else => {},
    }

    if (next.x >= 0 and
        next.x < game.width and
        next.y >= 0 and
        next.y < game.height)
    {
        game.player = next;
    }
}

fn render(game: Game) void {
    std.debug.print("\x1B[2J\x1B[H", .{});

    var y: i32 = 0;
    while (y < game.height) : (y += 1) {
        var x: i32 = 0;
        while (x < game.width) : (x += 1) {
            if (game.player.x == x and game.player.y == y) {
                std.debug.print("@", .{});
            } else {
                std.debug.print(".", .{});
            }
        }

        std.debug.print("\n", .{});
    }

    std.debug.print("\nuse w/a/s/d to move, q to quit\n", .{});
}

fn readInput() !u8 {
    var stdin_buffer: [128]u8 = undefined;
    var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer);
    const stdin = &stdin_reader.interface;

    var line_buffer: [128]u8 = undefined;

    const line = (try stdin.takeDelimiterExclusive(
        '\n',
        &line_buffer,
    )) orelse return 'q';

    if (line.len == 0) {
        return 0;
    }

    return line[0];
}

pub fn main() !void {
    var game = initGame();

    while (game.running) {
        render(game);

        const input = try readInput();

        update(&game, input);
    }

    std.debug.print("goodbye\n", .{});
}
```

Run:

```bash
zig build run
```

You should see:

```text
..........
..........
....@.....
..........
..........

use w/a/s/d to move, q to quit
```

Type:

```text
d
```

Then press Enter.

The player moves right.

#### Separating Engine and Game Code

Right now, the code is small. But even here, there are two kinds of code.

Engine-like code:

```text
main loop
input reading
rendering frame
```

Game-specific code:

```text
player position
movement rules
world size
```

A larger engine tries to keep these separate.

For example:

```zig
fn engineLoop(game: *Game) !void {
    while (game.running) {
        render(game.*);
        const input = try readInput();
        update(game, input);
    }
}
```

Then `main` becomes:

```zig
pub fn main() !void {
    var game = initGame();
    try engineLoop(&game);
}
```

This seems like a small change, but it teaches an important design habit: isolate the loop from the rules of the game.

#### Adding Walls

A world becomes more interesting when some cells are blocked.

Add a function:

```zig
fn isWall(x: i32, y: i32) bool {
    return (x == 3 and y >= 1 and y <= 3) or
        (x == 7 and y == 2);
}
```

Update movement:

```zig
if (next.x >= 0 and
    next.x < game.width and
    next.y >= 0 and
    next.y < game.height and
    !isWall(next.x, next.y))
{
    game.player = next;
}
```

Update rendering:

```zig
if (game.player.x == x and game.player.y == y) {
    std.debug.print("@", .{});
} else if (isWall(x, y)) {
    std.debug.print("#", .{});
} else {
    std.debug.print(".", .{});
}
```

Now the map may look like this:

```text
..........
...#......
...#@..#..
...#......
..........
```

The player cannot move through `#`.

#### Adding a Goal

Add a goal position:

```zig
goal: Vec2,
won: bool,
```

Update `initGame`:

```zig
.goal = .{
    .x = 9,
    .y = 4,
},
.won = false,
```

Then in `update`:

```zig
if (game.player.x == game.goal.x and game.player.y == game.goal.y) {
    game.won = true;
    game.running = false;
}
```

Render the goal:

```zig
if (game.player.x == x and game.player.y == y) {
    std.debug.print("@", .{});
} else if (game.goal.x == x and game.goal.y == y) {
    std.debug.print("G", .{});
} else if (isWall(x, y)) {
    std.debug.print("#", .{});
} else {
    std.debug.print(".", .{});
}
```

After the loop:

```zig
if (game.won) {
    std.debug.print("you win\n", .{});
} else {
    std.debug.print("goodbye\n", .{});
}
```

Now the project is a tiny game.

#### What a Real Game Engine Adds

A real engine usually adds:

```text
window creation
graphics API
audio
asset loading
timing
input devices
physics
animation
entity systems
collision detection
scripting
scene management
```

But the center remains the game loop.

Even a large game engine still repeats:

```text
collect input
advance simulation
draw result
```

The project here keeps everything small so you can see that shape clearly.

#### What You Learned

You built a small terminal game engine.

You represented world state with structs.

You wrote a game loop.

You handled input.

You updated player state.

You rendered a frame.

You checked world bounds.

You separated loop structure from game rules.

This is the base pattern behind interactive programs. A game engine is not magic. It is a disciplined loop around state, input, update, and output.

