File size: 6,533 Bytes
40c0886 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | # Open-World Game Design
Designing an open world is fundamentally a trade-off between **density of
content** and **memory/CPU budget**. This document collects the patterns the
agent uses when generating the open-world city example.
## Procedural generation
### Grid + jitter
The simplest viable city layout is a grid of blocks where each block holds
one or more buildings whose position is jittered slightly to break the
symmetry:
```csharp
for (int x = 0; x < gridX; x++)
for (int z = 0; z < gridZ; z++)
{
Vector3 block = origin + new Vector3(x * blockSpacing, 0f, z * blockSpacing);
SpawnBuilding(block + Random.insideUnitSphere * 4f);
}
```
The generated `CityGenerator.cs` follows this pattern. For more variety,
swap the grid for:
* **Voronoi street networks** (Delaunay triangulation, then prune).
* **L-system road growth** (recursive rewrite rules -- great for organic
cities).
* **Wave function collapse** -- tile-based, but expensive to author.
### Building composition
A building is rarely a single mesh. The generated `BuildingGenerator.cs`
composes three primitives:
1. A wall cube with a Standard material (concrete/glass color).
2. A slightly smaller cube with an emissive **window material** so windows
glow at night.
3. A `BoxCollider` for collision.
For more variety:
* Vary the footprint (square vs rectangular).
* Add a flat roof cap with a different material.
* Add a parapet along the top.
* Randomly choose between glass-tower, brick-mid-rise, and concrete-low-rise
palettes.
### Runtime vs baked
The agent generates everything **at runtime in `Start()`** because:
* The Unity project opens with an empty scene and the user immediately
sees content after pressing Play.
* No need to ship `.unity` scenes with hundreds of GameObjects.
* Easy to randomize: just change the seed.
The downside is the first-frame hitch on large worlds. Mitigations:
* Spread generation across frames using a coroutine that yields after
every N buildings.
* Use `Object.Instantiate` from a small set of prefabs instead of building
primitives from scratch each time.
* Use `Graphics.DrawMeshInstanced` for buildings once you do not need
per-building colliders.
## Level of Detail (LOD)
For an open world you must use LOD. Without it, the GPU processes every
window pane on every building no matter how far away it is.
| LOD | Screen size | Poly count |
|-----|-------------|-----------------------------|
| 0 | 60%+ | Full mesh with windows |
| 1 | 25-60% | Simplified mesh, no windows |
| 2 | 5-25% | Single box per building |
| Culled | <5% | Not rendered at all |
Add a `LODGroup` to each building and assign LOD meshes. For procedural
buildings you can build the LOD meshes in code by reducing the window
count and merging meshes.
## Streaming and chunking
For worlds larger than ~1km x 1km you must chunk:
* Divide the world into a grid of chunks (e.g. 200m x 200m).
* Load chunks near the player; unload chunks far away.
* Use `Addressables` or `SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive)`
to load chunk scenes.
The generated example does NOT chunk -- it builds everything in one shot,
which is fine for an 8x8 block city. Chunking becomes mandatory around
16x16 blocks.
## Fog and far plane
Always set `RenderSettings.fog` for open worlds. It hides:
* Distant popping when LOD changes happen.
* The far clip plane edge.
A linear fog from 100m to 800m is a good default for a city.
## Day/Night cycle
The generated `DayNightCycle.cs` rotates a directional light around the X
axis to simulate the sun:
```csharp
float sunAngle = (_timeOfDay - 0.25f) * 360f; // 0.25 = noon
sun.transform.rotation = Quaternion.Euler(sunAngle, -30f, 0f);
```
* Keep the day length short (60-180 s) for demo purposes; 24 min real time
per day for actual gameplay.
* Lerp ambient light and skybox color between day/night presets so the
world does not suddenly flip black.
* Turn street lights on/off based on a sun-intensity threshold.
## Player traversal
Open worlds need fast traversal. Options:
| Method | Speed (m/s) | Use case |
|-----------------------|-------------|---------------------------------------|
| Walking | 3-5 | On-foot exploration |
| Sprinting | 7-10 | Short bursts |
| Car (arcade) | 15-30 | The generated `VehicleController.cs` |
| Horse / mount | 8-12 | Fantasy open world |
| Fast travel | instant | Pre-unlocked locations |
The generated project ships both a `PlayerController` (on-foot) and a
`VehicleController` (car) so the user can switch between them.
## NPC traffic
For an open-world city, AI traffic should be **lightweight**:
* Use `NavMeshAgent` with a low `acceleration` for cars so they look
smooth, not jerky.
* Pool the agents -- never `Instantiate` a new car every time one drives
off-screen.
* Despawn agents when they are > 100m from the player.
The generated `NPCController.cs` covers pedestrians; for cars you would
extend it with lane-following logic.
## Save systems
Open worlds need persistent saves. The generated `SaveSystem.cs` writes a
single JSON file to `Application.persistentDataPath`:
```csharp
[Serializable]
public class GameSave
{
public Vector3 playerPosition;
public int score;
public float timeOfDay;
public List<string> completedQuests;
}
```
For larger worlds, split saves into:
* Player profile (small, frequent).
* World state (large, infrequent -- chunk ownership, building destruction,
NPC schedules).
## Common pitfalls
* **Floating point precision:** Unity uses single-precision floats. Beyond
~5km from the origin, physics and rendering jitter. Use a **floating
origin** that re-centres the world on the player every few hundred metres.
* **Shadow distance:** the default 150m shadow distance is too short for an
open world. Raise to 300-500m for desktop; lower for mobile.
* **Lighting rebuild:** make sure all your static city geometry is marked
**Navigation Static** AND **Lightmap Static** before baking. For runtime-
generated cities you cannot bake -- rely on realtime lighting and the
directional sun.
* **Collider spam:** 50 buildings x 6 faces each = 300 colliders. Mark
small decoration meshes as `isTrigger = false` and use a single
`BoxCollider` per building.
|