| # 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. |
|
|