#3 Build a prison with the mouse, and rooms that find themselves
· Nick
The second milestone is done, and for the first time there is something to look at. The Godot client now sits on top of the simulation core, and you can build a one-storey prison with the mouse: lay floors, draw walls, place doors, mark rooms, tear things down. Everything is plain cubes and flat colours, and there are still no people in it. But it is a place, and it is a place you can walk the camera around.
What you can do now
You start a new game, pick a map size (64, 128 or 256 tiles square) and an optional seed, and get an empty grid. The camera is orthographic with a fixed tilt. It turns in strict 90° steps, zooms toward the cursor, pans with the keys or the middle button, and has a top-down mode for building. Walls would hide rooms from a camera that turns freely, so it doesn’t.
Floors and demolition take a rectangle. Walls follow an outline (or a straight line if you hold Shift). A door is a click, and it picks its direction from the neighbouring walls, with a key to turn it. While you drag, the area under the cursor is shown as a ghost: green tiles will be built, red tiles will be skipped, and a label says how many and why. Nothing is sent to the world until you let go, and if the core says not a single tile would change, nothing is sent at all. Right click or Escape cancels.
There are several floors in the world model, so there is a floor picker, and floors above the one you are on are hidden. Rooms get their own tool, covered below. A debug overlay shows chunk borders, walkable tiles, the tile under the cursor and a timing panel. Every word in the interface is English.
The preview is the real thing
A ghost that disagrees with the result is worse than no ghost. So the client does not guess what a build command will do. The core can answer “what would happen if you ran this” for the same area, tile by tile, with a reason for every tile it would refuse. The check that fills the preview is the same rule that the command uses when it really runs, so the two cannot drift apart. The client only draws the answer. It asks the core again only when something that affects the answer has changed: the tool, the dragged area, the floor, or the map itself.
The same principle shapes the rest of the client: it reads the state of the world and sends commands, and has no simulation logic of its own. For now the core ticks on the main thread, and a command from a click is applied at the start of the next frame, before the world ticks. If tick times ever start to spike once the world is full of people, moving the core to its own thread is the plan, with a measured threshold for when to do it.
Rooms that find themselves
I looked at two ways to make rooms. Prison Architect lets you draw a zone, which then is its own object with its own tiles. That is easy to understand, but it goes wrong in a prison: a whole cell block would be a single zone, and after you rebuild a wall you have to repaint it. The other way is to find enclosed areas automatically, but then you cannot mark a yard that has no walls around it.
What I chose is a mix. The only thing stored is a room type on each tile: cell, canteen, bathhouse, infirmary, visiting room, exercise yard and so on. Everything else is worked out from the walls. A space is a group of open tiles joined by sides, on one level, and it is enclosed if it does not touch the edge of the map or an empty drop. A room is all tiles of one type within one space. Walls and doors are borders, and a type painted under a wall is kept but ignored, so it comes back if you take the wall down.
This gives behaviour that feels right without being told. Put a wall through a room and it becomes two rooms of the same type. Take the wall out and they merge. Put a door where a wall was and nothing changes. A room that is not valid is not deleted, since that would be the opposite of “without pain”; it is shown in red with the reasons: not enclosed, shared with another type, too small, or no door. You can paint a rectangle, fill a whole enclosed space with one click, or clear it.
The room list is derived data, so it is not saved and not part of the world hash; only the type on each tile is. It is rebuilt after commands and after systems, and checking an unchanged world costs nothing. In this milestone the changed level is recalculated whole, which takes about half a millisecond on a 256×256 map; doing it chunk by chunk can wait until path regions need the same machinery. The rule I hold it to is that an incremental rebuild must give the same result as a full one.
Rebuild only what changed, slice floors for free
The map is drawn as one mesh per chunk per level, not one object per wall. Every chunk has a version counter in the core, and the renderer remembers which version it last drew. When the world changes, only chunks with a newer version go into a queue, and the queue is worked on with a time budget per frame, so a huge change spreads over a few frames instead of freezing one. Painting a room type does not bump the map version, so it never wakes the mesh builder at all.
Floor slicing follows the same idea of doing less. Floors above the current one are simply hidden. The walls of the current floor are squashed down to one metre by a shader, one shared material and one height value, so switching floors or toggling full walls rebuilds nothing. Switching through four levels triggers zero mesh rebuilds.
A gap of ten centimetres
When I tried the first one-storey prison on the final check, there was a floor on the level above, and from the side there was a thin light line between the top of a wall and the slab over it. At a corner you could see the floor hanging in the air. I suspected the floor slicing, then z-fighting, then smoothing.
None of them. The floor was a single flat quad with no thickness, drawn at the height of its level plus ten centimetres. The walls of the level below ran up to exactly three metres. The floor of the next level started at three metres ten. The gap was ten centimetres wide, and through it you could see the lit top face of the wall. Horizontally everything lined up perfectly, which is why it took a while to see.
The fix is that a floor is now a slab with real thickness, reaching down to the start of its level. The top stays at the same height, so picking tiles with the mouse and all overlays are unchanged. Side faces are skipped where a floor or wall touches in the same chunk and drawn on chunk borders, so a chunk’s mesh never depends on its neighbours. That leaves a dark ten-centimetre edge on the slab’s end. I looked at screenshots from before and after, and decided to keep it. It reads as the edge of a floor, which it is.
Does it hold up
The last task of the milestone was to build a one-storey prison with the mouse and see whether it hurts. Mostly it didn’t. What the run found beyond the gap above was small and annoying: the tool buttons did not un-highlight when you picked another tool with the mouse, and there was no way back out of the New Game dialog with Escape. Both are fixed. A pause menu is on the list of ideas for later.
Next
Next is M2, movement and the daily routine. The world gets regions and reachability, pathfinding that works across floors, and flow fields for crowds. The core finally has real work to do on every tick, so the benchmark gate will stop sleeping. See the roadmap.