unity-game-agent / unity_agent /knowledge /entries /open_world_design.md
ryzerrr's picture
Upload folder using huggingface_hub
40c0886 verified
|
Raw
History Blame Contribute Delete
6.53 kB
# 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.