Three Numbers to Live By
Every unit has three stats — Hunger, Energy, and Health — and nothing forces it to stay alive. A unit that keeps making bad choices can starve to death.
Hunger drops steadily while a unit is awake. Energy drops while it's active and recovers while it rests or sleeps. Health only starts falling once Hunger is already empty. Importantly, these stats don't override the unit's decisions — they're just numbers that feed into its scores. So a unit that keeps choosing to socialize instead of eat really can die. That's intentional: if units couldn't die from bad decisions, the God's interventions later in the series wouldn't matter. And when a unit dies, any resource locations it found but hadn't reported yet are lost with it.
The stat math
All the rates are set in the SimConfig asset: Hunger falls 0.5/sec, Health falls 0.8/sec while starving, Energy falls 0.5/sec when active and recovers 2.5/sec asleep. Health only drops once Hunger is at or below 15:
if (Hunger <= C->HealthDrainHungerThreshold)
Health = FMath::Clamp(Health - C->HealthDrainRate * DeltaSeconds, 0.f, 100.f);
Score Everything, Pick the Winner
There's no fixed priority order. Every tick, a unit scores all nine of its possible actions and does the highest-scoring one. A little randomness is added so two identical units don't always choose the same thing.
Each bar is one action's score for a single unit on a single tick. Foraging is highest, so the unit forages. Because Wandering is close behind and random noise is added to every score, the same unit could just as easily wander instead. Emergency-eat scores nothing here because it only turns on when hunger is low.
That randomness is also a tuning dial. More of it makes the tribe behave more unpredictably; less of it makes units act more like pure optimizers. A small amount keeps behavior varied without turning it into chaos.
The scoring loop
Each action's score gets random noise (up to ±12) added, and the highest total wins. No action is treated as special. Adding a new behavior means writing one score function and one line to include it — it then competes with everything else automatically:
auto Eval = [&](ESimUnitState Action, float Score) { Score += FMath::RandRange(-Noise, Noise); if (Score > Best) { Best = Score; Winner = Action; } }; Eval(ESimUnitState::Foraging, ScoreForage()); Eval(ESimUnitState::Wandering, ScoreWander()); // ...seven more... ExecuteAction(Winner);
Why Identical Units Diverge
Each unit is born with five personality traits that shift its scores, and those traits slowly change based on what the unit actually does.
The traits are Curiosity, Industriousness, Sociability, Prudence, and Piety. A curious unit scores Wander higher; a prudent one eats before it gets dangerously hungry. Every time a unit acts, the trait behind that action rises a little, and the traits it didn't use drift back toward the values it was born with. So a unit that forages a lot becomes more industrious, which makes it score foraging even higher, which makes it forage more often. Over a long run, that feedback is what turns a group of near-identical units into distinct individuals.
Two units from the same run. Both started with every trait at 0.5. The one on the left has done a lot of work and drifted toward high Industriousness; the one on the right has explored a lot and drifted toward high Curiosity.
How drift works
The trait behind the chosen action goes up; the unused traits move back toward the unit's original SeedTraits values. That means a trait only stays high while the behavior keeps happening:
*Expressed = FMath::Clamp(*Expressed + C->TraitDriftRate, 0.f, 1.f); auto Regress = [C](float& T, float Seed) { T += (Seed - T) * C->TraitRegression; };
Each action maps to one trait: wandering to Curiosity, foraging/gathering/depositing to Industriousness, socializing/reporting to Sociability, and sleeping/resting/emergency-eating to Prudence.
Gather, Carry, Stockpile
Units eat at a resource node, then carry the extra back to shared storage. Depositing raises a unit's reputation; hoarding a full load, or taking from storage without real need, lowers it.
Each storage depot holds only one resource type, so you can tell at a glance what the tribe is running low on. Depots also act as an emergency food supply: a starving unit with no food nearby will draw from storage. But taking from storage when it isn't truly desperate costs reputation, which keeps the shared supply available for genuine emergencies.
When is a storage run worth it?
ScoreDeposit() weights the trip by the square of how full the unit is, so a nearly empty pack barely counts and a full one strongly pulls the unit home — units finish gathering before making the trip. Deposit() returns how much the depot actually took in, so a unit doesn't lose surplus when the depot is full:
float Before = StoredAmount; StoredAmount = FMath::Min(StoredAmount + Amount, Capacity); return StoredAmount - Before; // the caller keeps whatever wasn't accepted
The Map Isn't Shared
Units don't know where anything is at the start. A resource only becomes known to the tribe after a unit finds it and reports it at the town center, and that shared knowledge fades over time.
A resource node is invisible to the tribe until a unit physically gets close to it, and even then only that one unit knows until it walks the information back to the town center. Each entry in the shared map loses confidence as time passes and is dropped once it hits zero, so the tribe has to keep re-confirming where things are. If a unit finds a good spot and then dies before reporting it, that location is simply lost until another unit happens to find it again.
This Scout found something while wandering and is now walking the discovery back to the town center to report it. Until that walk finishes, no other unit knows it exists.
This is what the shared map actually holds after a run: the Town Center's own inspect panel, with a debug overlay drawing a line from itself to every node that's been reported so far. Five discoveries in, five lines out — nothing else on the map exists to the tribe yet.
Confidence decay
A report sets an entry's confidence to full; every tick it erodes, and entries that reach zero are removed:
for (FResourceMemoryEntry& E : Entries) E.Confidence = FMath::Max(0.f, E.Confidence - ConfidenceDecayPerHour * GameHoursDelta); Entries.RemoveAll([](const FResourceMemoryEntry& E) { return E.Confidence <= 0.f; });
Roles No One Assigned
Units gain skills by doing tasks. When one skill clearly dominates, the unit is automatically given a matching job title: Forager, Gatherer, Scout, or Elder.
A unit that spends its early time near food builds up its foraging skill, which raises its forage scores, which leads it to forage more — until it clearly qualifies as a Forager. The title comes from what the unit has actually been doing, not from anything set in advance. A unit that never specializes in one thing stays a Generalist.
Three units from the same run, each inspected mid-task: a Forager hauling wood home, a Scout wandering to find new ground, and an Elder bonding with others at the town center. Each label tracks whichever skill that unit has actually maxed out.
The dominance test
A unit only earns a title if its best skill group is above a minimum (15) and at least 1.5× higher than its next-best group. Otherwise it stays a Generalist. Adding a new profession is one row in the table:
{ "Foraging", "Depositing", "Forager" },
{ "Exploring", "Reporting", "Scout" },
{ "Socializing", NAME_None, "Elder" },
// ...
if (BestLevel < Threshold || BestLevel < SecondBest * Dominance)
return FName("Generalist");
Why the Tribe Needs Elders
Elders spread information. An Elder shares what the tribe currently needs with nearby units, so units working far from the town center don't fall out of date.
The shared map only updates at the town center, so a unit working at the edge of the map slowly loses track of where the food is. An Elder — a unit that has specialized in socializing — periodically passes its up-to-date picture of the tribe's needs to any nearby unit whose information is older. The Elder becomes important because of the role it grew into, not because it was handed any authority.
The broadcast rule
An Elder only overwrites information that is older than its own, and it passes along the real age of that information rather than resetting it to brand-new:
if (Other->TribalPressureAge <= TribalPressureAge) continue; // they already know as much Other->TribalPressure = TribalPressure; Other->TribalPressureAge = TribalPressureAge; // keep the real age
Night Splits the Tribe
When night falls, sleeping becomes an option — but only an option. Tired units head home to rest, while others stay out and keep working.
During the day, sleep isn't on the table at all. At night it becomes another scored action competing with everything else — and only for units that have been given a house to sleep in. A tired, cautious unit scores sleep highly and heads home to recover its energy. A unit that's still hungry keeps foraging straight through the night. A unit with plenty of energy left, or little caution, might simply keep working or wandering in the dark. So night doesn't flip a switch that sends everyone to bed; it shifts the balance of scores, and the tribe naturally splits into the ones who rest and the ones who stay out. As new roles come online later in the series, that divide gets richer — a warrior, for example, might stay up to keep watch while the rest of the tribe sleeps.
The same clearing, five points across the day/night cycle, crossfading on a loop — from full daylight down to the dimmest, coolest night light and back again.
The twilight hand-off
A cosine curve gives the daylight amount. The moon's strength is a SmoothStep that stays at full until the sun is well up, so the two lights cross-fade with no dark gap between them:
const float SunHeight = FMath::Sin((TimeOfDay - 6.f) / 24.f * 2.f * PI); const float MoonAmt = 1.f - FMath::SmoothStep(-0.1f, 0.35f, SunHeight); MoonComp->SetIntensity(C->MoonlightIntensity * MoonAmt);
The moon is set up so it does not count as an atmosphere sun light, which is why it lights the ground without brightening the sky.
A God in the Machine
The tribe now runs entirely on its own — it eats, explores, remembers where resources are, specializes into roles, and dies. The next step is the God: a large language model that reads the simulation's state every few seconds. It will be able to answer questions about the tribe, point out problems like a coming famine before you notice them, and, when you ask, step in and change something in the world.