Tiny Colony (part 2)
Create Something Moving

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 = 0one 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



