LOG_001 · DEVLOG · GodSim · July 2026

Building a World
That Runs Itself

On emergence, world-building tools, and the first milestone of an AI god game

GodSim is a simulation where nothing is scripted. A tribe of people — identical at the start — are placed into a world with food, shelter, a gathering place. Then time passes.

No one is assigned a role. A unit becomes a forager because it spent its early hours near resource nodes, which nudged its behavior weights, which made it more likely to forage again. An elder emerges not because the game decided there should be one, but because a specific unit spent time at the town center, built social bonds, and now carries information others seek out. Factions form around nothing more than proximity and shared history.

Alongside all of this is an AI — not controlling the units, but watching. An LLM (starting with Claude, designed to be swappable) reads the state of the simulation every few seconds. It can answer questions, notice patterns before you do, or intervene if you ask it to. Think less "AI plays the game" and more "an entity exists in the same world you're observing."

No objectives. No win state. The goal is to watch emergence happen.

Before any of that can exist, there has to be a world. This post is about building the stage.

01 · World Authoring

Painting a World in seconds

The biome map is a small PNG image — a few hundred pixels wide. Yellow pixels become Plains. Green becomes Forest. Red becomes Mountain. Blue becomes Water. I painted it in a pixel editor in about a minute. The engine reads it directly, and one pixel becomes one tile in the world.

The appeal of this over procedural generation: I can hand-sculpt the world's shape. I know where I want the water, where I want mountains to create natural borders, where I want open plains for foragers to roam. The simulation handles everything else — but the world's geography is intentional. Changing the world is as simple as repainting a few pixels and pressing a button in the editor.

The Source Image
The hand-painted biome map: yellow plains, a green forest border, a red mountain corner, and a blue lake with a river trailing off it

This is the actual file — a few hundred pixels, four flat colors, painted free-hand in a couple minutes. The lake and its river are just a blob and a squiggle; forest hugs two edges for cover, and a corner of mountain sits top-right as a natural border. Nothing here was generated.

Color → Biome
The BiomeColorMap details panel on the DA_BiomeConfig asset, mapping yellow to Plains, green to Forest, red to Mountain, and blue to Water

This map lives on the DA_BiomeConfig data asset and is the exact lookup table the texture parser checks pixel-by-pixel. Yellow resolves to Plains, green to Forest, red to Mountain, blue to Water. Add an entry, and a fifth color is a new biome — no code change.

Biome → Material
The BiomeMaterials details panel on the grid actor, assigning M_Plains, M_Forest, M_Mountain, and MI_WaterRiver1 to each biome type

A second, independent map on the grid actor — one material per biome, assigned by name. Swapping MI_WaterRiver1 for a different water material changes every lake and river in the world at once, since they're all reading from the same table.

The Result
The generated floor mesh, showing the same lake, river, forest border, and mountain corner as the source pixel drawing, now rendered with real materials

Same lake, same river, same corner of mountain — now a textured floor mesh, built from the same PNG in the same button press. Line this up against the source image above: the geometry and the color choices both trace back to that one small file.

How the texture parsing works

The ASimWorldGrid actor holds a reference to a UTexture2D. The texture must use CompressionSettings = TC_UserInterface2D and MipGenSettings = NoMipmaps — this preserves exact pixel values without compression or mip blending.

At generation time, the actor locks the texture's bulk data and reads each pixel as an FColor (BGRA). A configurable TMap<FColor, EBiomeType> maps pixel colors to biome types — this lives in the Details panel so any designer can remap colors without touching code. Alpha is normalized to 255 before the lookup so the source image's alpha channel doesn't matter.

// Lock read-only, iterate pixels
const FColor* Pixels = static_cast<const FColor*>(Mip.BulkData.LockReadOnly());
for (int32 y = 0; y < GridHeight; ++y) {
    for (int32 x = 0; x < GridWidth; ++x) {
        FColor Pixel = Pixels[y * GridWidth + x];
        Pixel.A = 255; // normalize alpha
        const EBiomeType* Found = BiomeColorMap.Find(Pixel);
        Tiles[y * GridWidth + x].Biome = Found ? *Found : EBiomeType::Plains;
    }
}
Mip.BulkData.Unlock();

Grid width and height come directly from the texture's mip dimensions — so the image size defines the world size, with no separate configuration needed.

02 · World Structure

Connected Regions: Each Lake Is Its Own Actor

After the image is parsed into a tile array, a flood fill algorithm runs across it. It finds every connected group of same-biome tiles — "connected" meaning touching on four sides, not diagonally — and treats each group as a single region.

A lake isn't just a collection of blue pixels. It becomes a Region actor placed at the centroid of its tiles. So does each forest, each mountain range. Each region knows its tile count, derives a resource cap proportional to its size, spawns resource nodes (berries in plains, timber in forests, ore in mountains), and manages a regeneration timer. When a resource depletes, the region notices and regrows one after a configurable interval. The game mode doesn't micromanage any of this — the regions run themselves.

Water regions do something else: at runtime, they register a navigation obstacle covering their tile footprint, so unit pathfinding treats water as impassable. No movement code changes — the world wires itself up.

Per-Biome Properties
The Biomes map on the config data asset, showing Resources Per Tile, Regrowth Interval Seconds, Spawn Table, and Decorations set independently for Plains, Forest, Mountain, and Water

Each biome gets its own entry here: how densely resources spawn per tile, how long regrowth takes, and its own spawn and decoration tables. Water's regrowth interval is set to 300 seconds against 1.0 for the rest — its "resource" is really the occasional fish node, not something that needs to come back quickly.

One Actor Per Region
The World Outliner filtered to BiomeRegion actors, showing 4 of 29 total, with the Details panel for BiomeRegion_Plains showing Biome Type Plains, Tile Count 12463, Resource Cap 25

This is what the flood fill actually produces — real actors in the World Outliner, one per connected region, created the moment the world generates. BiomeRegion_Plains's own details panel shows exactly what it knows about itself: its biome type, and the stats it derives from its own tile footprint.

Flood fill + region lifecycle

The flood fill is a standard iterative DFS using an explicit stack — no recursion to avoid stack overflows on large grids. Each tile is visited once. Connected same-biome tiles accumulate into a list, then get handed to SpawnRegion().

// Iterative DFS flood fill
TArray<bool> Visited; Visited.SetNumZeroed(Tiles.Num());
for (int32 StartIdx = 0; StartIdx < Tiles.Num(); ++StartIdx) {
    if (Visited[StartIdx]) continue;
    EBiomeType BiomeType = Tiles[StartIdx].Biome;
    TArray<FIntPoint> RegionTiles;
    TArray<int32> Stack; Stack.Add(StartIdx);
    Visited[StartIdx] = true;
    while (Stack.Num() > 0) {
        const FIntPoint Tile = IndexToTile(Stack.Pop());
        RegionTiles.Add(Tile);
        // check 4 neighbors, push unvisited same-biome tiles
    }
    SpawnRegion(BiomeType, RegionTiles);
}

Each ASimBiomeRegion receives its tile list and resource cap (= TileCount × ResourcesPerTile, configurable per biome in a data asset). Water regions set up a hidden UBoxComponent sized to their tile footprint with a UNavModifierComponent set to NavArea_Null. This is why water blocking works without any special code in the movement system: the NavMesh simply has no traversable surface over water tiles.

03 · Rendering

Materials That Don't Show the Grid

The first version of the floor used vertex colors — each tile colored by its biome type. It was fast to implement and immediately obvious it was wrong. Water looked like hundreds of individual puddles.

The fix was two things working together. First: instead of one mesh section with all tiles sharing a material, the floor is split into one section per biome type. Each section gets its own material — a water material, a grass material, a rock material. You can author those however you like in the editor.

Second: world-space UV coordinates. With per-tile UVs (0→1 per tile), the material texture restarts at every tile boundary, which is what makes the grid visible. With world-space UVs, coordinates are derived from the vertex's actual position divided by a configurable scale. The UVs flow continuously across the entire region. A water material now samples coordinates as if the ocean is one surface. No tile edges appear in the output. A single FloorUVScale parameter on the grid actor controls how large the texture appears relative to the tiles.

Before: Per-Tile UVs
The lake and river with per-tile UVs, showing the water texture repeating identically on every tile in a visible grid pattern

Every tile samples the same 0→1 texture range, so the water material repeats identically on every single tile. The pattern lines up perfectly at every seam — which is exactly the problem. That perfect repetition is what reads as a grid.

After: World-Space UVs
The same lake and river with world-space UVs, showing the water texture flowing continuously across tiles with no repeating pattern or visible seams

Same lake, same material, coordinates derived from world position instead of per-tile. The ripples and foam patches now span multiple tiles at irregular sizes, because the texture is being sampled as one continuous surface rather than restarted at every boundary.

UV math and mesh section per biome

During GenerateFloor(), tiles are grouped by biome type into a TMap<EBiomeType, TArray<int32>>. Each group becomes one CreateMeshSection call on the UProceduralMeshComponent, and each section gets its material assigned via SetMaterial(SectionIndex, BiomeMaterials[Biome]).

The UV for each vertex corner is just the vertex's local-space X/Y position divided by a scale divisor:

const float UVDivisor = TileSize * FloorUVScale; // e.g. 1500 × 4.0 = 6000

// For tile at grid position (x, y):
const float L = x * TileSize;
const float R = (x + 1) * TileSize;
const float B = y * TileSize;
const float T = (y + 1) * TileSize;

UVs.Add(FVector2D(L / UVDivisor, B / UVDivisor));
UVs.Add(FVector2D(R / UVDivisor, B / UVDivisor));
UVs.Add(FVector2D(R / UVDivisor, T / UVDivisor));
UVs.Add(FVector2D(L / UVDivisor, T / UVDivisor));

Because adjacent tiles share the same formula, the UV at the right edge of tile N equals the UV at the left edge of tile N+1. No discontinuity, no seam.

One note on NavMesh: UProceduralMeshComponent only creates complex (per-triangle) collision, which NavMesh recast ignores — it only queries simple collision shapes. The floor includes a hidden UBoxComponent spanning the full grid to give the NavMesh something to project onto.

04 · Visual Dressing

Filling the World

Materials make the biomes readable. Decorations make them feel real. Each biome type has a configurable decoration table — a list of meshes or Blueprint actors, each with a density (probability per tile), scale variance, random yaw, and position jitter. The world grid iterates every tile at generation time and rolls against each entry.

Static mesh decorations — trees, rocks, grass clumps — use GPU instancing: one draw call per unique mesh regardless of how many instances exist. A dense forest with thousands of trees costs the same as a handful. Blueprint actor decorations are spawned individually for cases where something needs logic or animation.

One checkbox per decoration entry controls whether instances generate collision. Trees and boulders block movement; grass and flowers don't. This keeps physics overhead minimal without needing separate mesh variants.

The Decoration Table
The Forest biome's decoration table, listing mesh entries like SM_BirchTreeAdultA and SM_GenericTreeB, each with its own Density, Scale Range, Random Yaw, Position Jitter, and Enable Collision settings

This is the table that drives everything below — one entry per mesh or Blueprint actor, each with its own density, scale range, random yaw, position jitter, and collision toggle. Forest alone has four of these; a birch and a generic tree at low density, plus entries for whatever else fills out the canopy.

Placed Per Region
Top-down view of the generated world with trees lining the forest edges, rocks scattered across the mountain corner, and the lake and river left bare

Top-down, the same map from the World Authoring section — trees along the forest border, rocks across the mountain corner, the lake and river left alone since water's table is mostly empty. Each biome only ever spawns from its own table.

Angled view of the plains showing scattered grass clumps at low density, with a tree line along one edge and boulders along another

Closer in, the plains aren't empty either — a low-density grass entry gives the open ground some texture without competing visually with the forest and mountain edges framing it.

Ground-level view inside the forest, showing dense tree cover along the border with the lake and grass visible through the gaps

And from ground level inside the tree line: the density and scale range settings from the table above are what turn a flat border into something that reads as an actual forest edge rather than a row of identical props.

GPU instancing and the ISM cache

Each unique static mesh asset gets exactly one UInstancedStaticMeshComponent (ISM), created on demand and stored as a component on the grid actor — not as a separate world actor. This keeps the World Outliner clean.

The cache is keyed on (UStaticMesh*, bool bCollision) — two separate maps, one for collision-enabled instances and one without. The same mesh can appear in multiple biome tables with different collision settings and still get separate ISM components:

// Two caches: one per collision setting
TMap<UStaticMesh*, UInstancedStaticMeshComponent*> ISMCacheNoCollision;
TMap<UStaticMesh*, UInstancedStaticMeshComponent*> ISMCacheCollision;

auto GetOrCreateISM = [&](UStaticMesh* Mesh, bool bCollision) {
    auto& Cache = bCollision ? ISMCacheCollision : ISMCacheNoCollision;
    auto& ISM = Cache.FindOrAdd(Mesh);
    if (!ISM) {
        ISM = NewObject<UInstancedStaticMeshComponent>(this);
        ISM->SetStaticMesh(Mesh);
        ISM->SetCollisionEnabled(bCollision
            ? ECollisionEnabled::QueryAndPhysics
            : ECollisionEnabled::NoCollision);
        ISM->RegisterComponent();
        ISM->AttachToComponent(RootComponent, ...);
        DecorationISMs.Add(ISM);
    }
    return ISM;
};

Blueprint actor decorations are spawned via SpawnActor and tracked in a separate DecorationActors array. Both are destroyed by ClearDecorations(), which is always called before ClearGenerated().

What's Next

Putting People In It

The world exists. The next milestone is the survival loop: hunger, energy, health. Units that forage, sleep, rest, and die if left long enough without food. The decision system is a utility AI — every possible action gets a float score based on current stats and personality traits, and the highest scorer wins. Two units with identical hunger but different personalities will make different choices. That difference, repeated across 30 units over hours of simulation time, is where emergence starts to happen.

Series roadmap
World
Layer
Survival
Loop
LLM
Oracle
Social
Fabric
Deep
Emergence