Skip to main content

Command Palette

Search for a command to run...

Tiny Colony (part 2)

Create Something Moving

Updated
3 min read
Tiny Colony (part 2)

In the previous post, I defined the scope of Tiny Colony: a small, 2D, grid-based colony simulator inspired by games like RimWorld, built mainly as a way to learn Rust and explore Bevy.

This post covers the next step: rendering a grid, spawning pawns, and moving one of them using a fixed simulation tick.


The world is just a grid

At its core, the world is a fixed-size grid:

pub const MAP_W: i32 = 64;
pub const MAP_H: i32 = 64;

Tiles are stored as a flat vector:

#[derive(Clone, Copy)]
enum Tile {
    Ground,
    Tree,
    Stockpile,
}

There’s no map abstraction yet — just a Vec<Tile> and some helpers to translate between grid coordinates and world coordinates.

fn grid_to_world(x: i32, y: i32) -> Vec3 {
    let origin_x = -(MAP_W as f32) * TILE_SIZE * 0.5 + TILE_SIZE * 0.5;
    let origin_y = -(MAP_H as f32) * TILE_SIZE * 0.5 + TILE_SIZE * 0.5;

    Vec3::new(
        origin_x + x as f32 * TILE_SIZE,
        origin_y + y as f32 * TILE_SIZE,
        0.0,
    )
}

This keeps the simulation grid-centered and avoids camera math early on.


Rendering tiles (brute force, yay!)

Each tile is rendered as a simple sprite, tinted by type:

commands.spawn((
    Sprite {
        color,
        custom_size: Some(Vec2::splat(TILE_SIZE - TILE_GAP)),
        ..default()
    },
    Transform::from_translation(world_pos),
));

This means spawning 64 × 64 = 4096 sprites on startup.

Is that optimal? Probably not.
Is it good enough for now? Absolutely.

One of the nice things about Bevy is how easy it is to brute-force something visible without caring about performance too early.


Pawns are data first, visuals second

A pawn is deliberately boring:

#[derive(Component)]
struct Pawn {
    id: u32,
    x: i32,
    y: i32,
}

No animation, no state machine, no behavior tree.

Visually, pawns are just small circles rendered slightly above tiles:

commands.spawn((
    Pawn { id, x, y },
    Sprite {
        image: circle_image.clone(),
        color: Color::srgb(0.85, 0.85, 0.95),
        custom_size: Some(Vec2::splat(TILE_SIZE - 2.0)),
        ..default()
    },
    Transform::from_translation(world_pos + Vec3::Z),
));

The circle texture itself is generated in code, not loaded from disk. This keeps iteration fast and avoids asset management entirely at this stage.


A fixed simulation tick

The most important step so far was introducing a fixed simulation clock.

struct Sim {
    paused: bool,
    speed: f32,
    tick: Timer,
    target: IVec2,
}

This separates:

  • simulation time (deterministic)

  • rendering time (best effort)

The tick runs at 10 Hz:

Timer::from_seconds(0.10, TimerMode::Repeating)

And can be paused or sped up independently from framerate.


Moving one pawn (dumbest way possible)

With the simulation tick in place, I implemented the simplest possible behavior:

Move pawn id = 0 one grid tile per tick toward a fixed target.

if pawn.x < target.x {
    pawn.x += 1;
} else if pawn.x > target.x {
    pawn.x -= 1;
} else if pawn.y < target.y {
    pawn.y += 1;
} else if pawn.y > target.y {
    pawn.y -= 1;
}

After updating the grid position, the render transform is updated accordingly:

let pos = grid_to_world(pawn.x, pawn.y);
transform.translation = pos + Vec3::Z;

No smoothing, no interpolation, no pathfinding.

But once the pawn started moving, the project immediately felt more “alive”.


Early lessons

A few things became clear very quickly:

  • Bevy makes it easy to get something visible fast

  • Rust’s borrow checker shows up early, but usually points to real design improvements

  • Fixed-tick simulation feels right for this genre

Most importantly: keeping the scope small made it possible to actually finish these steps.


Current state

At this point, Tiny Colony has:

  • a grid-based world

  • basic terrain

  • multiple pawns as ECS entities

  • a fixed simulation loop

  • pause and speed controls

  • one pawn moving deterministically

That’s enough of a foundation to start building actual gameplay.

The code at this stage is available on GitHub under the post-1 tag: https://github.com/GuzzoLM/tiny-colony/tree/post-1/tiny-colony

A Tiny Colony Simulation in Rust

Part 3 of 4

This series documents building a tiny colony simulation in Rust with Bevy. It’s a learning journal, not a tutorial: a small, understandable sim with a clear core loop, built step by step, focused on real trade-offs, constraints, and finishing.

Up next

A Tiny Colony Simulation in Rust (Part 1)

Scope, Constraints, and Why Bevy