on_camera_z_changed: only dirty z-levels whose z_diff changed (both old and new camera positions, within 0..=8 window). Previously dirtied all 20k entities on every z-change. populate_tilemap_chunk_data: process up to MAX_POPULATE_PER_FRAME (512) keys per frame, sorted by z-distance from current camera. Spreads z-scroll cost across multiple frames instead of one spike. TilemapBenchmark: added z-change spike instrumentation (last_z_change_dirty_ms, last_z_change_dirty_count, last_z_change_populate_ms, z_change_history_ms). F9 report now shows z-scroll metrics. PreviousZIndex resource tracks prior camera z for targeted dirty calc.
Dorf
A Dwarf Fortress-inspired simulation game built in Rust with Bevy. The focus is on a fully data-oriented chunk engine: procedural terrain, hierarchical pathfinding, GPU-baked sprite rendering, and dynamic chunk loading/unloading — all designed to scale to a full DF-style world with z-levels, entities, and simulation.
This is an engine foundation, not a complete game. Many features (crafting, needs, combat, world history) exist only as stubs or are entirely absent. The chunk rendering and pathfinding systems are production-quality; the simulation layer above them is early.
Architecture
src/
├── camera.rs # Panning camera, z-level control (Q/E, scroll)
├── config.rs # GameConfig resource
├── constants.rs # Tile size, pathfinding constants
├── entities/ # Dorfs, pigs, rabbits — ambulatory entities
│ ├── sentient/
│ ├── livestock/
│ └── shared_*/ # Ambulatory, pathfinding systems
├── game.rs # ZIndex resource, frame timing
├── main.rs # Bevy app bootstrap
└── world/
├── chunks/ # ChunkMap, chunk loading/unloading, connectivity
├── generation/ # Terrain blobs, forestry, foliage (stub), fauna (stub)
└── tiles/ # TileMap, TileRegistry, rendering, visibility
Chunk Architecture
- World chunks: 8×8 tiles (
CHUNK_SIZE = 8,CHUNK_SIZE_TILE = 128px) - Render chunks: 32×32 tiles (
CHUNK_TILES = 32,512px). One render chunk = 4×4 world chunks. This mismatch is intentional — a single GPU texture covers multiple world chunks, which means chunk unloading must mark the render chunk dirty (not the world chunk). - Z-levels: 5 below, 15 above, 21 total (
Z_BELOW = 5,Z_ABOVE = 15). Terrain is rendered as quilted sprites — one texture per z-level, all stacked in the shader. - Loading:
setup_initial_chunksruns atStartup, emitsGenerateChunkEventfor all chunks ininitial_chunk_radius.handle_chunk_eventsprocesses events and fires terrain/forestry/foliage/fauna generation events. - Unloading:
unload_chunk(chunk_pos)is the canonical function. Currently disabled —dynamic_chunk_unloading_systemis a no-op stub. To enable: implement a system that diffs wanted chunks (player/NPC interest radius) againstChunkMap.loaded_chunksand callsunload_chunkfor each evictable chunk.
Rendering Pipeline
handle_tile_occlusion_updates— raycasts from each tile upward to computevisible_rangebitmask (which z-levels can see it)build_quilted_terrain_sprites— GPU bake: tiles bucketed by(render_chunk, z_level), blitted into pooled pixel buffers, spawned asTerrainSpriteentitiesupdate_tile_visibility— togglesVisibilityon sprites based on currentZIndex(camera z-level)
Pathfinding
Three tiers, gated by distance:
- Tier 1 (<4 chunks): synchronous A* on
TileMap - Tier 2 (4–8 chunks): provisional path + full path via async queue
- Tier 3 (>8 chunks): hierarchical chunk graph → async segmented A*
PathfindingBenchmark resource tracks sync vs async performance per frame.
Entity Indexing
Static terrain entities (floor tiles, fixtures, trees) are indexed in chunk_entity_index: HashMap<IVec2, Vec<Entity>> at spawn time. When a chunk unloads, the index is retrieved and entities are despawned. This is O(entities in chunk), not O(total entities).
Mobile entities (dorfs, pigs, rabbits) are not in chunk_entity_index. They are spawned once at game start and persist. They will need per-chunk lifecycle management once fauna generation is implemented.
Controls
| Key | Action |
|---|---|
| Click + drag | Pan camera |
| Scroll | Pan camera |
| Q / E | Move camera z-level down / up |
| Mouse wheel | Pan camera |
Configuration
config.toml:
initial_chunk_radius: chunks loaded around origin at startup (default: 7 → 15×15 = 225 chunks)spawn_counts: dorfs, pigs, rabbits spawned at game start
tiles.toml: tile registry — floor tiles, fixture tiles. Each tile has id, astar_weight, can_stand_in, can_stand_on, transparent flags.
Status
Implemented
- Procedural terrain (blob-based, simplex noise heightmap)
- Tile registry (
tiles.toml) with A* weights - Floor tiles, fixture tiles, item tiles
- Z-levels with quilted GPU sprite rendering
- Tile occlusion / visibility raycasting
- Forestry: tree trunks + leaves spawned per chunk
- Dorfs, pigs, rabbits spawning at startup
- Hierarchical async pathfinding (3 tiers)
- Chunk loading infrastructure
- Chunk unloading infrastructure (
unload_chunk,dynamic_chunk_unloading_systemstub) - Camera panning + z-level control
- GPU texture pooling (pixel buffer reuse across bakes)
- Render chunk dirty-tracking for incremental rebakes
Stubs (exist as empty functions, wired into schedules)
- Foliage generation per chunk (
generate_chunk_foliage) - Fauna spawning per chunk (
generate_chunk_fauna) - Weathering and precipitation (
generate_chunk_weathering_and_precipitation)
Not Started
- Crafting, stockpiles, resource gathering
- Needs, mood, stress systems
- Job assignment and task automation
- Building and construction (digging/mining)
- Combat and squad management
- Item persistence and inventory
- Save/load world state
- World history and procedural storytelling
- UI (menus, z-level overlay, workshop assignment)
- Dynamic chunk loading (player/NPC-centered, not just radius-at-startup)
Building
cargo build # dev profile, ~1.2K warnings (pre-existing)
cargo run # dev mode
[profile.dev.package."*"] opt-level = 3 gives near-release perf in dev builds.