This commit is contained in:
2026-07-04 02:06:24 -07:00
parent 6e8907ebf6
commit 68dfdfa33b
20 changed files with 564 additions and 19 deletions
+117
View File
@@ -0,0 +1,117 @@
---
title: "Starting a colony sim, lessons I'm carrying forward"
description: "Kicking off a new Odin prototype and the grid-game patterns I already trust: cameras, tilemaps, and how to lay out entity data."
date: 2026-07-03
type: lesson
series: colony-sim-prototype
series_order: 1
languages: ["odin"]
tags: ["odin", "colony-sim", "game-dev"]
---
I'm starting a second prototype, a colony simulation, in [Odin](https://odin-lang.org/) and Raylib. It's early days and there's not much game there yet. This post isn't a feature tour. It's the stuff I already know works because I learned it building the [tower defense prototype](https://github.com/Codegirl-Games/tower-defense-prototype) first.
If you're starting a top-down grid game, these are the foundations I keep reaching for.
## The 2D camera is just coordinate math
A 2D camera doesn't need a library. It's two numbers and two functions:
- **Offset**: which world point sits at the center of the screen
- **Zoom**: how many world pixels map to one screen pixel
Convert world → screen:
```
screen = (world - offset) * zoom + screen_center
```
Convert screen → world (for mouse input):
```
world = (screen - screen_center) / zoom + offset
```
That's it. Panning moves the offset. The scroll wheel clamps zoom between sensible min/max values. Pan speed gets divided by zoom so movement feels consistent when you're zoomed in.
The lesson I keep re-learning: **every mouse click must go through `screen_to_world` before you do anything useful.** Selection, movement commands, building placement: all of it. Forget this once and your clicks drift when the camera moves.
## Tilemaps are flat arrays with helpers
A tilemap is a width, a height, and a flat buffer. Tile `(x, y)` lives at index `y * width + x`. Wrap access in two helpers and never think about the math again:
- `tile_index(map, x, y)`: buffer lookup
- `tile_in_bounds(map, x, y)`: guard every read and write
World position to tile coordinate is just `floor(world / tile_size)` on each axis. I used the same pattern in both prototypes. The colony sim added a comment in `tile_in_bounds`, *"learning from another game, this will become handy"*, because I skipped it early in the tower defense project and paid for it later.
Terrain type can start as a `u8` per tile. A switch or lookup table maps type → color. Don't over-engineer biomes on day one; get the grid drawing and the coordinate conversions right first.
When drawing, multiply tile size by zoom and cull tiles that fall off-screen. An 80×60 map is 4,800 rectangles, fine for a prototype, but the cull pass is free and keeps the pattern honest for bigger maps.
## Logical tiles vs visual position
Grid games have two positions whether you plan for it or not:
- **Logical position**: which tile the entity occupies (`Tile_Coord{3, 7}`)
- **Visual position**: where the sprite actually renders (smoothly interpolated between tile centers)
The colonist's grid cell updates one step at a time. The circle on screen lerps toward the next tile center each frame. Gameplay stays discrete; motion looks continuous. Mix these up and pathfinding, collision, and selection all get harder.
I didn't need this in the tower defense game: enemies moved in continuous world space along a path. Colony sims live on tiles. Separate the two early.
## Struct of arrays vs array of structs
Both layouts show up in my code. Neither is always wrong.
**Array of structs (AoS)**, what the tower defense prototype uses:
```odin
enemies: [MAX_ENEMIES]Enemy,
```
Each slot is a full `Enemy` struct: position, health, speed, active flag, all together. Natural to read: `enemy.health -= damage`. Good when you often touch most fields on one entity at once.
**Struct of arrays (SoA)**, what the colony sim uses:
```odin
Entity_World :: struct {
active: [MAX_ENTITIES]bool,
position: [MAX_ENTITIES]Tile_Coord,
move_state: [MAX_ENTITIES]Move_State,
visual_position: [MAX_ENTITIES]rl.Vector2,
// ...
}
```
Each field is a parallel array across all entities. Good when you update one system at a time (move every entity, then draw every entity) and when entities are sparse (lots of inactive slots in a fixed pool).
My rule of thumb so far:
| Reach for AoS when… | Reach for SoA when… |
|---|---|
| Entities are few and always accessed whole | You iterate one component across many entities |
| Struct fits in cache and you're touching most of it | Many slots are inactive (object pool) |
| Code clarity matters more than layout | Systems are split (movement, render, AI) |
Both prototypes use **fixed pools** with an `active` flag, no allocate/free per spawn. That pattern transferred directly from tower defense to colony sim regardless of AoS vs SoA.
## Entity handles, not raw indices
The colony sim returns `Entity`, a `distinct u32`, instead of passing array indices around. Internally it's `index + 1`, with `0` meaning invalid. Small thing, but it stops you from accidentally passing a tile coordinate or a mouse value where an entity ID goes.
## Update and draw stay separate
Both games follow the same loop shape:
1. Read input
2. Update simulation (`world_update`, `entity_update_movement`)
3. Draw (`tilemap_draw`, `entity_draw`)
Simulation code never calls draw functions. Draw code never changes game state. Obvious, but worth stating because it's the seam that keeps things readable as files multiply.
## What I'm not writing about yet
Pathfinding, job queues, resources, building: none of that exists in the repo yet. When there's a full week of work to show, I'll write a proper devlog. For now, the [repo](https://github.com/Codegirl-Games/colony-sim-prototype) is a camera, a tilemap, one colonist, and a right-click move command.
The game will come. These patterns are the part I'm confident in.