AppleyAppley Docs
Docs/Terrain

Procedural Cities

Generate realistic cities using road networks, zoning, and building placement.

16 min read Advanced Updated 2026-07-01 v2.0 ~55 min to complete

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

This is part of the Advanced Documentation collection. Category: Terrain. Estimated completion: 55 minutes. Difficulty: Advanced/Professional.

Screenshot

Procedural Cities
Procedural Cities

How Professional Games Do It

1

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.

2

Create city blocks

After generating roads, find enclosed areas (blocks) by detecting road-bounded polygons. These blocks become zones for buildings.

3

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.

4

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.

5

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.

6

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 Cities — FluxScript
// 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:

Procedural Cities — Node Graph
[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.