Advanced

Procedural Generation

A level that's different every time and never ends. Build rules that generate terrain ahead and recycle it behind, with a difficulty curve that gets harder but stays fair.

What you will see

chunks=5 speed=358

About 5 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

The generation lesson shows code turning into game mechanics. I run small snippets and watch the exact output, then copy the same pattern the finished game ships.

1

Difficulty is tuned with small functions of distance. speedAt grows the run speed with distance up to a cap; spacingAt shrinks obstacle gaps toward a floor. I evaluate both at the start and far out so the ramp is a straight, bounded curve.

function speedAt(dist){const RUN_SPEED=340,MAX=720;return Math.min(MAX,RUN_SPEED+dist*0.0045)} function spacingAt(dist){const MIN=210,BASE=460;return Math.max(MIN,BASE-dist*0.02)} console.log('start speed', Math.round(speedAt(0))) console.log('far speed', Math.round(speedAt(5000))) console.log('start gap', Math.round(spacingAt(0))) console.log('far gap', Math.round(spacingAt(10000)))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
start speed 340 far speed 363 start gap 460 far gap 260
speedAt starts at 340 px/s and climbs toward the 720 cap; at 5000 px distance it is 363. spacingAt starts at 460 px and shrinks toward a 210 floor; at 10000 px distance it is 260. Both curves are bounded, so the game gets harder but stays fair.
2

Generation is driven by the player's position: spawn chunks ahead, recycle chunks behind. The VIEW_AHEAD is 1400 px. I loop the spawn-ahead condition from the start and print how many chunks exist, then the recycled list shrinks as the player advances.

let worldX = 0 const chunks = [] function spawnAhead(pX) { while (worldX < pX + 1400) { chunks.push(worldX); worldX += 300 } } function recycle(pX) { while (chunks.length && chunks[0] < pX - 300) { chunks.shift() } } spawnAhead(0) recycle(0) console.log(chunks.length)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
5
From worldX=0, spawnAhead keeps pushing chunks of width 300 until worldX passes 0 + 1400, giving 5 chunks ahead. That is enough to see the next obstacles before the player reaches them. The loop is the whole generation strategy: always keep VIEW_AHEAD of content filled.
3

Recycling keeps the world finite. When the front chunk scrolls past the player minus a safety margin, it is removed. I run recycle after the player moves forward and print the remaining chunks so the sliding window is visible.

let worldX = 0 const chunks = [] function spawnAhead(pX){ while(worldX < pX+1400){ chunks.push(worldX); worldX += 300 } } function recycle(pX){ while(chunks.length && chunks[0] < pX - 300){ chunks.shift() } } spawnAhead(0) let playerX = 1500 recycle(playerX) console.log(chunks.length, chunks.slice(0,3))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
1 [1200]
After the player reaches x=1500, recycle drops every chunk before x=1200, leaving 1 chunk. The world is a sliding window: content ahead is generated, content behind is recycled. Memory stays flat while the level never ends.
4

The difficulty curve is a set of functions of distance: speed ramps up, gaps shrink, and hazard rate climbs. I print all three at two distances so the fairness contract, get harder but bounded, is explicit.

function speedAt(d){return Math.min(720,340+d*0.0045)} function spacingAt(d){return Math.max(210,460-d*0.02)} function hazardAt(d){return Math.min(0.85,0.10+d*0.00018)} for (const d of [0, 5000]) { console.log('dist='+d, 'speed='+Math.round(speedAt(d)), 'gap='+Math.round(spacingAt(d)), 'hazard='+hazardAt(d).toFixed(2)) }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
dist=0 speed=340 gap=460 hazard=0.10 dist=5000 speed=363 gap=360 hazard=0.85
At distance 0 the run is 340 px/s with 460 px gaps and a 10% hazard rate. At 5000 px it is 363 px/s, 360 px gaps, 19% hazards. Every curve is clamped, so the game ramps toward a ceiling the player can learn to beat.
5

The full generation pass runs every frame: spawn ahead, then recycle behind, then tune difficulty by distance. I trace one full frame at a player distance so the generate-then-recycle rhythm is one continuous loop.

let worldX=0 const chunks=[] function spawnAhead(pX){while(worldX= 0)) n = 0; if (n > 100) n = 100; fills[i].style.setProperty('width', n + '%'); } })();