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