Procedural Cities
Generate realistic cities using road networks, zoning, and building placement.
Introduction
Games like Spider-Man, Cyberpunk 2077, and City Skylines use procedural city generation to create vast urban environments. This guide explains how studios generate road networks via L-systems, zone city blocks, and place buildings by type — then builds equivalent systems in Appley.
Advanced Guide 2 of 25
Screenshot
How Professional Games Do It
Generate a road network
Use L-systems (Lindenmayer systems) to grow road networks from a seed. Start with a highway, branch into major roads, then minor streets. Each road segment has rules for when to branch, turn, or terminate.
Create city blocks
After generating roads, find enclosed areas (blocks) by detecting road-bounded polygons. These blocks become zones for buildings.
Assign zones
Classify blocks by distance from city center: downtown (commercial), mid-town (mixed), suburbs (residential), industrial (outskirts). Distance-based zoning creates realistic city layouts.
Place buildings by zone
Each zone has building presets with size ranges, heights, and styles. Downtown gets tall commercial towers; suburbs get small residential houses. Place buildings within block boundaries.
Add variation
Randomize building height, width, facade color, and rooftop details within each zone. Use a seed per block so the same city regenerates identically.
Generate interiors
For enterable buildings, generate floor plans procedurally. Use room templates and corridor connectors to create varied but navigable interiors.
FluxScript Implementation
Here is how to build this system in Appley using FluxScript (Appley Scripting):
// Procedural city generation using L-system road growth
on start {
citySeed = 42
math.seed(citySeed)
// Step 1: Generate road network via L-system
roads = []
// Seed: main highway through city center
roads.push({ start: vec3(-500, 0, 0), end: vec3(500, 0, 0), width: 20, type: "highway" })
roads.push({ start: vec3(0, 0, -500), end: vec3(0, 0, 500), width: 20, type: "highway" })
// Grow road network with L-system rules
for generation = 0 to 5 {
newRoads = []
for i = 0 to roads.length {
road = roads[i]
// Branch probability depends on road type
if road.type == "highway" {
// Highways branch into major roads
if math.random() < 0.4 {
branch = createBranch(road, "major", 90)
newRoads.push(branch)
}
} else if road.type == "major" {
// Major roads branch into minor streets
if math.random() < 0.3 {
branch = createBranch(road, "minor", 90)
newRoads.push(branch)
}
}
// Extend road with slight curve
if math.random() < 0.6 {
extension = extendRoad(road)
if extension != null newRoads.push(extension)
}
}
roads = roads.concat(newRoads)
}
// Step 2: Find city blocks (enclosed areas)
blocks = findCityBlocks(roads)
// Step 3: Zone and populate blocks
for i = 0 to blocks.length {
block = blocks[i]
distFromCenter = math.distance(block.center, vec3(0, 0, 0))
zone = "suburb"
if distFromCenter < 100 zone = "downtown"
else if distFromCenter < 250 zone = "midtown"
else if distFromCenter < 400 zone = "suburb"
else zone = "industrial"
placeBuildingsInBlock(block, zone)
}
// Step 4: Create road meshes
for i = 0 to roads.length {
createRoadMesh(roads[i])
}
print("City generated: " + roads.length + " roads, " + blocks.length + " blocks")
}
func placeBuildingsInBlock(block, zone) {
presets = {
downtown: { minHeight: 40, maxHeight: 120, style: "glass_tower" },
midtown: { minHeight: 15, maxHeight: 50, style: "mixed_commercial" },
suburb: { minHeight: 4, maxHeight: 8, style: "residential" },
industrial: { minHeight: 6, maxHeight: 15, style: "warehouse" }
}
p = presets[zone]
// Subdivide block into building lots
lots = subdivideBlock(block, 15, 15)
for j = 0 to lots.length {
lot = lots[j]
height = math.random(p.minHeight, p.maxHeight)
building = world.spawn(p.style, lot.center)
building.setScale(lot.width, height, lot.depth)
building.setMaterial(zone + "_facade")
// Seed-based variation for facades
building.setSeed(citySeed + i * 1000 + j)
}
}Node Graph Equivalent
The same system implemented as a Node Graph in Appley Node Scripting:
[On Start] --> [Seed: 42 (deterministic)]
--> [Create: 2 seed highways]
--> [L-System Growth: 5 generations]
--> [For Each Road]
--> [Branch: highway->major, major->minor]
--> [Extend: with slight curve]
--> [Probability: 0.4/0.3/0.6 per type]
--> [Find City Blocks: enclosed polygons]
--> [For Each Block]
--> [Calculate: distance from center]
--> [Assign Zone: downtown/midtown/suburb/industrial]
--> [Subdivide: into building lots]
--> [For Each Lot]
--> [Spawn: building by zone preset]
--> [Set: random height, facade material]
--> [Create Road Meshes]
--> [Print: summary]Professional Optimization Tips
How AAA studios optimize these systems — applicable to your Appley project:
- Use deterministic seeds. Seed the random number generator with a per-city seed. This ensures the same city regenerates identically when players revisit, which is essential for persistence.
- Use L-systems for road networks, not pure random walks. L-systems create hierarchical road structures (highways branch into major roads, then minor streets) that mimic real city growth patterns.
- Cache generated city data. City generation is expensive (road finding, block subdivision, building placement). Store the result and only regenerate if the city is explicitly rebuilt.
- Use instancing for repeated buildings. Cities have hundreds of similar buildings. Use GPU instancing to render all buildings of the same type in a single draw call.
Common Mistakes
Roads overlapping or crossing incorrectly
Use L-system rules with angle constraints. Pure random road placement creates chaotic intersections. L-systems with 90-degree branching produce grid-like or organic patterns.
All buildings look identical
Use per-building seeds for facade variation. Even with the same building model, randomizing height, color, and rooftop details creates visual variety.
