All nine parts
Every town needs a few trees, some flowers and a bit of water. In Naijapolis these are not models that an artist made once and the game copies. They are designs that a player can shape, grown from a seed, and they sit on the land you read about in Part 4. This part is about how a handful of numbers becomes a baobab, a bed of hibiscus and a river with a waterfall.
A plant is data
A tree design is a small record: how tall, how long the bare trunk is, how much it leans, how many forks, how widely they spread, how much they droop, the crown shape, and a few colours. A flower design has petals, rings, openness, a centre and a stem. There are twelve ready-made trees (oak, willow, pine, acacia, blossom, maple, jacaranda, palm, cypress, baobab, bamboo, a fruit tree) and a set of ready-made flowers (such as daisy, rose, bougainvillea, tulip, hibiscus, lavender and a grass tuft), each one just a design with different numbers. A "surprise" function writes a new design at random, picking a colour palette that goes together.
That small size matters. A design can be saved with a city, sent to a friend as a short code, and cleaned on load exactly like the city codes in Part 6: every number is clamped, every colour is checked, and there are caps on how many parts a design may ask for, so a hostile design cannot ask for a million leaves.
Growing a tree
The layout is a tiny simulation of growth, in a few steps:
- The trunk is a curve of seven points that leans away from vertical, wobbles a little, and thins toward the top.
- Forks leave the upper trunk in a ring. Each reaches out, and gradually bends downward by the design’s droop. A willow droops a lot; an acacia hardly at all.
- Levels: each fork sprouts smaller, thinner kids partway along, and those can sprout their own. More levels, more twigginess.
- Leaf clumps gather first at the branch tips, then fill the rest of the crown according to its shape.
The crown shape is a tiny function that says how wide the tree is at each height. That one idea gives most of the variety in a forest:
// how wide the crown is at a height: t = 0 at the bottom, 1 at the top
function crownProfile(shape, t) {
switch (shape) {
case 'cone': return clamp(1.05 - t, 0.08, 1); // wide at the bottom, a point at the top
case 'umbrella': return t > 0.55 ? 1 : 0.12 + t * 0.4; // bare, then a flat wide canopy
case 'baobab': return t > 0.5 ? 1 : 0.18; // a fat bare trunk, a bulb of leaves
case 'oval': return Math.sqrt(Math.max(0, 1 - (2*t - 1) ** 2)) * 0.85 + 0.1;
default: return Math.sqrt(Math.max(0, 1 - (2*t - 1) ** 2)) * 0.9 + 0.12; // round
}
}
The same tree, never the same twice
The seed is what makes a grove look alive. It feeds a small, fast random-number generator, so the same seed always gives the same tree, but a different seed gives a sibling: a little taller or wider, with a different lean and different leaf colours. A "variety" setting on each design says how far siblings may differ.
// a few lines make a repeatable generator; this is the classic mulberry32
function mulberry32(seed) {
let a = seed | 0;
return () => {
a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296; // 0 .. 1
};
}
const rand = mulberry32(seedOfThisTree);
const height = design.height * (1 + (rand() - 0.5) * design.variety * 0.3); // illustrative
Because the seed is stored with the placed tree, the same city looks the same every time it is opened, on any phone, for any visitor. Nothing is random at draw time.
Colour that does not look flat
A palette of three greens would make a flat green ball if each leaf took one of them. Two tricks fix that. First, mixing rules decide where each colour goes: scattered at random, as a gradient from the bottom to the top, in patches (using a cheap hash of the position, so neighbours share a colour), or only on the outer tips. Second, every colour is nudged a little in hue and lightness for each leaf mass, so no two are quite alike.
function leafColor(design, rand, pos, h, edge) { // h: 0 bottom .. 1 top; edge: 0 middle .. 1 outer
const pal = design.colors; let c;
if (design.mix === 'gradient') c = paletteAt(pal, 1 - h);
else if (design.mix === 'patches') c = pal[Math.floor(cellNoise(pos) * pal.length)];
else if (design.mix === 'tips') c = edge > 0.55 ? pal[1 + Math.floor(rand() * (pal.length - 1))] : pal[0];
else c = pal[Math.floor(rand() * pal.length)];
return nudge(c, rand, 0.025 * design.jitter, 0.07 * design.jitter); // a little hue, a little light
}
Painted, not modelled
The game’s look is "painterly", and trees show it most. A leaf mass is a faceted blob whose faces each take a slightly different tint (lit from above, cooler underneath). Around each blob stand little cards, each showing a brush-stroke leaf, a round dab or a ragged splash. They stick out past the blob’s edge, so the silhouette is ragged like paint on canvas, not a smooth ball. Those three strokes are drawn once on a small offscreen canvas when the game starts. Five styles are on offer (painterly, low-poly, soft clay, leafy and inked), and a plant’s whole geometry is merged into a few meshes so a garden of fifty flowers costs only a handful of draw calls.
A plant’s detail setting follows the same rule as everything else in Part 4: it is chosen from the device and can be changed, and when it changes, every plant is simply grown again from its design and seed.
Flowers: a ring of petals
A flower is a stem that bends, a few leaves, and a head made of rings of petals around a centre. Each ring has a petal count, length, width, how open it is and how far it curls. Change those, and a daisy becomes a rose.
Flowers come in beds and hedges too. A bed scatters several designs over an area using a seed, so a garden has a mix of colours that never repeats exactly. Mixing rules for flowers (alternate, random, by ring, tipped with a second colour) work like the tree ones. A whole bed sways gently in the wind as one object, which is cheap and looks lovely.
Rivers, streams and lakes
Water is harder than plants, because it must obey the ground. A player taps points; the game puts them in flow order (a river runs from high ground to low, so if you started at the bottom, the points are reversed), then builds the river in stages.
The curve. A Catmull-Rom spline passes smoothly through every point. It is resampled at even steps, and an optional "wander" setting adds two slow sine waves across the line, fading to nothing at each tap so the river still passes through them. The width tapers: a little narrower near the source, wider toward the mouth.
The bed. This is the rule that makes it look right. Water never flows uphill, so the bed is built downstream one step at a time, always at least a hair lower than the last, and deeper where the ground rises ahead:
function waterBed(curve, ground, depth) {
const g = smooth(curve.map(c => ground(c.x, c.z)), 3); // the ground under each sample
const bed = [];
for (let i = 0; i < curve.length; i++) {
bed.push(i === 0
? g[0] - depth
: Math.min(bed[i - 1] - MIN_SLOPE * STEP, // never climb
g[i] - depth)); // never sit above the ground
}
return smooth(bed, 3);
}
The channel. The bed is cut into the height map, and the banks blend into the surrounding ground. The game remembers exactly which grid cells it changed and what they held before. If the player moves the river, those cells are put back first, so a river can be dragged around without scarring the land. A lake is a bowl cut the same way, with a level.
The surface and the waterfalls. The water is not stored. It is read back from the ground as it is now: bed height plus water depth, never allowed to rise downstream. Wherever that surface drops steeply, the code marks a waterfall:
const slope = (y[i - 1] - y[i]) / STEP;
fall[i] = clamp((slope - FALL_MIN) / (FALL_MAX - FALL_MIN), 0, 1); // 0 = calm, 1 = a full waterfall
// then a run of samples where fall > 0.5 becomes one waterfall, with its height and width
Because the surface comes from the ground, anything that changes the ground (a player sculpting the hill upstream) changes the river automatically. Bridges are planned along the same curve. And a check stops anything being built in the water.
Making water look like water
Water has its own shader, written for the GPU, and it layers a handful of simple ideas:
| Layer | How it is made |
|---|---|
| Colour with depth | Pale at the banks, deeper in the middle: a blend from the shallow colour to the deep one by distance from the centre line. Eight colour looks, from teal to tea brown. |
| Ripples | Three layers of smooth random noise at different scales, scrolling downstream at different speeds. Faster over a waterfall. |
| Light under the surface | Slow bands of light built from the same noise, dimmed at night. |
| Sky at a low angle | At glancing angles the surface shows the sky colour. This one trick (the Fresnel effect) is what makes a flat plane read as water. |
| Foam | White along the banks, and strong at the foot of a waterfall, with drifting mist. |
| Weather and season | Rain rings in a rainstorm, wind ruffles, and in the rainy months the river turns muddy and rises a little. |
| A mirror | The town is drawn upside down into a small texture that the shader samples, so buildings reflect in a lake. Only when the device can afford it. |
The noise itself is classic and tiny: hash a grid point to a number, then blend between the four corners with a smooth curve.
float hash(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
float noise(vec2 p) {
vec2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f); // smoothstep
return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x),
mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);
}
// ripples = three octaves, scrolling at different speeds
float rip = noise(p * vec2(2.1, 0.55) - vec2(0, t)) * 0.5
+ noise(p * vec2(4.7, 1.3) - vec2(0, t * 1.6)) * 0.3
+ noise(p * vec2(9.1, 3.2) - vec2(0, t * 2.4)) * 0.2;
A little life at the water
Every river and lake gets its own small cast, chosen from its seed so it is the same each time: egrets, ducks, dragonflies, fish, people and canoes. The banks grow reeds, wildflowers and stones, and slow water gets lily pads. Everything is built from the same merged-primitive kit as the props, and the player can switch it all off in the Look panel.
Like the graphics tiers in Part 4, water has a rich and a light setting. Rich has rain rings, wind, the mirror, all the wildlife and full bank growth. Light has plain ripples, half the bank growth, no mirror and fewer animals. The game picks light by itself on a phone with little memory or few cores, and the player can override it.
What we learned
- Store the recipe, not the result. A tree is a dozen numbers and a seed; a river is a few points. Saves stay tiny, and everything can be regrown when the ground or the settings change.
- Let a seed be the only source of chance. It makes places that look the same every visit, and it makes tests possible.
- Derive what you can. The water surface is read from the ground, never stored, so it can never disagree with it.
- Make the cheap trick do the heavy lifting: a ring of petals, a width formula, three octaves of noise, and the sky colour at a low angle.
One more part
Gardens are the pleasant half of a town. The other half is the one nobody photographs: what happens to the rubbish, and how a few simple rules turn it into floods, outbreaks and votes.