Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +254 -0
- examples/generate_example.py +78 -0
- examples/open_world_city/Assets/Scenes/MainScene.unity +101 -0
- examples/open_world_city/Assets/Scenes/MainScene.unity.meta +11 -0
- examples/open_world_city/Assets/Scripts/AudioManager.cs +67 -0
- examples/open_world_city/Assets/Scripts/AudioManager.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/BuildingGenerator.cs +68 -0
- examples/open_world_city/Assets/Scripts/BuildingGenerator.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/CameraController.cs +13 -0
- examples/open_world_city/Assets/Scripts/CameraController.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/CityGenerator.cs +110 -0
- examples/open_world_city/Assets/Scripts/CityGenerator.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/DayNightCycle.cs +69 -0
- examples/open_world_city/Assets/Scripts/DayNightCycle.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/GameManager.cs +116 -0
- examples/open_world_city/Assets/Scripts/GameManager.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/HealthSystem.cs +82 -0
- examples/open_world_city/Assets/Scripts/HealthSystem.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/NPCController.cs +124 -0
- examples/open_world_city/Assets/Scripts/NPCController.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/PickupSystem.cs +63 -0
- examples/open_world_city/Assets/Scripts/PickupSystem.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/PlayerController.cs +109 -0
- examples/open_world_city/Assets/Scripts/PlayerController.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/RoadNetwork.cs +75 -0
- examples/open_world_city/Assets/Scripts/RoadNetwork.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/SaveSystem.cs +66 -0
- examples/open_world_city/Assets/Scripts/SaveSystem.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/ThirdPersonCamera.cs +85 -0
- examples/open_world_city/Assets/Scripts/ThirdPersonCamera.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/UIManager.cs +59 -0
- examples/open_world_city/Assets/Scripts/UIManager.cs.meta +11 -0
- examples/open_world_city/Assets/Scripts/UnityAgent.Scripts.asmdef +11 -0
- examples/open_world_city/Assets/Scripts/UnityAgent.Scripts.asmdef.meta +11 -0
- examples/open_world_city/Assets/Scripts/VehicleController.cs +136 -0
- examples/open_world_city/Assets/Scripts/VehicleController.cs.meta +11 -0
- examples/open_world_city/Packages/manifest.json +23 -0
- examples/open_world_city/ProjectSettings/ProjectSettings.asset +10 -0
- examples/open_world_city/ProjectSettings/ProjectVersion.txt +1 -0
- examples/open_world_city/ProjectSettings/TagManager.asset +15 -0
- pyproject.toml +23 -0
- unity_agent/__init__.py +32 -0
- unity_agent/__main__.py +103 -0
- unity_agent/config.py +90 -0
- unity_agent/knowledge/README.md +33 -0
- unity_agent/knowledge/__init__.py +41 -0
- unity_agent/knowledge/entries/open_world_design.md +185 -0
- unity_agent/knowledge/entries/unity_audio.md +120 -0
- unity_agent/knowledge/entries/unity_fundamentals.md +99 -0
- unity_agent/knowledge/entries/unity_input_system.md +154 -0
README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Unity Agent
|
| 2 |
+
|
| 3 |
+
A Python package that generates complete, playable **Unity 2022.3 LTS**
|
| 4 |
+
projects on disk. The agent exposes a registry of code-generation tools
|
| 5 |
+
that emit C# scripts (MonoBehaviours), `.unity` scene files, project
|
| 6 |
+
configuration (manifest, ProjectSettings, .meta) and a full open-world
|
| 7 |
+
city example game. The output is a real Unity project you can open in
|
| 8 |
+
Unity Hub and press Play.
|
| 9 |
+
|
| 10 |
+
## What it does
|
| 11 |
+
|
| 12 |
+
* Writes complete, compilable **C#** MonoBehaviours (player, vehicle,
|
| 13 |
+
camera, AI, weapons, inventory, quests, save system, ...).
|
| 14 |
+
* Scaffolds a Unity project structure (`Assets/`, `Packages/manifest.json`,
|
| 15 |
+
`ProjectSettings/`, `.meta` files) exactly the way Unity expects.
|
| 16 |
+
* Provides a **system prompt** that teaches an LLM to act like a senior
|
| 17 |
+
Unity engineer and to call the right tool in the right order.
|
| 18 |
+
* Ships a **knowledge base** of focused markdown references
|
| 19 |
+
(physics, navigation, input, UI, audio, rendering, open-world design).
|
| 20 |
+
* Runs **static QA** against generated projects to catch unbalanced
|
| 21 |
+
braces, missing meta files, missing manifest, etc.
|
| 22 |
+
* Comes with a fully working **open-world city example** generated
|
| 23 |
+
end-to-end from the tool layer.
|
| 24 |
+
|
| 25 |
+
## Installation
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
git clone <this-repo> unity-agent
|
| 29 |
+
cd unity-agent
|
| 30 |
+
pip install -e . # optional; the package also works with PYTHONPATH=.
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
The package has **no third-party Python dependencies** -- it only uses the
|
| 34 |
+
standard library. Python 3.9+ is required.
|
| 35 |
+
|
| 36 |
+
## Quick start
|
| 37 |
+
|
| 38 |
+
### Generate the bundled example
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
# Generate the full open-world city example into examples/open_world_city/
|
| 42 |
+
python -m unity_agent generate --game open_world_city --preset open_world_city --output-dir examples
|
| 43 |
+
|
| 44 |
+
# Run static QA on it
|
| 45 |
+
python -m unity_agent qa examples/open_world_city
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
Open `examples/open_world_city/` in Unity Hub (2022.3 LTS or newer) and
|
| 49 |
+
press Play.
|
| 50 |
+
|
| 51 |
+
### Use the package from Python
|
| 52 |
+
|
| 53 |
+
```python
|
| 54 |
+
from unity_agent.config import Settings
|
| 55 |
+
from unity_agent.transport.unity_transport import UnityTransport
|
| 56 |
+
from unity_agent.tools.unity_tools import (
|
| 57 |
+
SetupUnityProjectTool,
|
| 58 |
+
CreatePlayerControllerTool,
|
| 59 |
+
CreateProceduralCityTool,
|
| 60 |
+
WriteSceneFileTool,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
settings = Settings(output_dir="my_games")
|
| 64 |
+
transport = UnityTransport(settings)
|
| 65 |
+
|
| 66 |
+
SetupUnityProjectTool(settings, transport).run(project_name="MyGame")
|
| 67 |
+
CreatePlayerControllerTool(settings, transport).run(project_name="MyGame")
|
| 68 |
+
CreateProceduralCityTool(settings, transport).run(project_name="MyGame")
|
| 69 |
+
WriteSceneFileTool(settings, transport).run(project_name="MyGame", scene_name="MainScene")
|
| 70 |
+
|
| 71 |
+
print(transport.summary())
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
### CLI
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
python -m unity_agent --help
|
| 78 |
+
|
| 79 |
+
# Commands
|
| 80 |
+
python -m unity_agent list-tools
|
| 81 |
+
python -m unity_agent setup --project-name MyGame --output-dir my_games
|
| 82 |
+
python -m unity_agent generate --game MyGame --preset open_world_city
|
| 83 |
+
python -m unity_agent generate --game MyFPS --preset fps_arena
|
| 84 |
+
python -m unity_agent generate --game Minimal --preset minimal
|
| 85 |
+
python -m unity_agent qa path/to/MyGame
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
## Project layout
|
| 89 |
+
|
| 90 |
+
```
|
| 91 |
+
unity-agent/
|
| 92 |
+
unity_agent/
|
| 93 |
+
__init__.py # Package entry point
|
| 94 |
+
__main__.py # CLI: `python -m unity_agent ...`
|
| 95 |
+
config.py # Settings (Unity path, LLM keys, output dirs)
|
| 96 |
+
tests.py # Self-test suite (run with `python -m unity_agent.tests`)
|
| 97 |
+
transport/
|
| 98 |
+
__init__.py
|
| 99 |
+
unity_transport.py # Writes .cs / .unity / .meta / config files
|
| 100 |
+
tools/
|
| 101 |
+
__init__.py
|
| 102 |
+
base.py # ToolBase + ToolRegistry + @register_tool
|
| 103 |
+
unity_tools.py # 24 tools (see below)
|
| 104 |
+
orchestrator/
|
| 105 |
+
__init__.py
|
| 106 |
+
prompts.py # SYSTEM_PROMPT + build_user_prompt + tool_catalog
|
| 107 |
+
knowledge/
|
| 108 |
+
README.md
|
| 109 |
+
__init__.py # load_entry / list_entries / load_all
|
| 110 |
+
entries/
|
| 111 |
+
unity_fundamentals.md
|
| 112 |
+
unity_physics.md
|
| 113 |
+
unity_rendering.md
|
| 114 |
+
unity_navigation.md
|
| 115 |
+
unity_input_system.md
|
| 116 |
+
unity_ui.md
|
| 117 |
+
unity_audio.md
|
| 118 |
+
open_world_design.md
|
| 119 |
+
qa/
|
| 120 |
+
__init__.py # ProjectQA + Issue + QAReport + CLI
|
| 121 |
+
examples/
|
| 122 |
+
generate_example.py # Regenerates examples/open_world_city/
|
| 123 |
+
open_world_city/ # A complete generated Unity project
|
| 124 |
+
Assets/Scripts/*.cs
|
| 125 |
+
Assets/Scenes/MainScene.unity
|
| 126 |
+
Packages/manifest.json
|
| 127 |
+
ProjectSettings/*
|
| 128 |
+
README.md
|
| 129 |
+
pyproject.toml
|
| 130 |
+
README.md
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
## Tools (24 total)
|
| 134 |
+
|
| 135 |
+
| Tool | Output |
|
| 136 |
+
|-------------------------------|--------------------------------------------------------------|
|
| 137 |
+
| `setup_unity_project` | Project scaffold (manifest, ProjectSettings, asmdef) |
|
| 138 |
+
| `create_player_controller` | PlayerController.cs (WASD + jump + physics) |
|
| 139 |
+
| `create_vehicle_controller` | VehicleController.cs (arcade car physics) |
|
| 140 |
+
| `create_procedural_city` | CityGenerator.cs + BuildingGenerator.cs (50+ buildings) |
|
| 141 |
+
| `create_third_person_camera` | ThirdPersonCamera.cs (smooth follow + collision) |
|
| 142 |
+
| `create_fps_controller` | FPSController.cs (mouse-look + WASD + jump) |
|
| 143 |
+
| `create_day_night_cycle` | DayNightCycle.cs (sun rotation + sky/ambient lerp) |
|
| 144 |
+
| `create_ai_npc` | NPCController.cs (NavMesh wander/chase FSM) |
|
| 145 |
+
| `create_pickup_system` | PickupSystem.cs + Pickup.cs (score + collectibles) |
|
| 146 |
+
| `create_health_system` | HealthSystem.cs + HealthBar.cs |
|
| 147 |
+
| `create_weapon_system` | WeaponSystem.cs (raycast hitscan + ammo + reload) |
|
| 148 |
+
| `create_audio_manager` | AudioManager.cs (SFX + music singleton) |
|
| 149 |
+
| `create_ui_manager` | UIManager.cs (TMP HUD: score / health / messages) |
|
| 150 |
+
| `create_terrain_generator` | TerrainGenerator.cs (Perlin heightmap + splat) |
|
| 151 |
+
| `create_water_shader` | WaterMaterial.cs (animated standard-material water) |
|
| 152 |
+
| `create_particle_effects` | ParticleSpawner.cs (bursts + explosion helper) |
|
| 153 |
+
| `create_save_system` | SaveSystem.cs (JSON save/load to persistentDataPath) |
|
| 154 |
+
| `create_inventory_system` | InventorySystem.cs (stacks + capacity + events) |
|
| 155 |
+
| `create_quest_system` | QuestSystem.cs (state machine + progress + completion) |
|
| 156 |
+
| `create_building_generator` | BuildingGenerator.cs (walls + emissive windows + collider) |
|
| 157 |
+
| `create_road_network` | RoadNetwork.cs (grid layout + intersections) |
|
| 158 |
+
| `write_csharp_script` | Any C# script to Assets/Scripts/ |
|
| 159 |
+
| `write_scene_file` | .unity scene file to Assets/Scenes/ |
|
| 160 |
+
| `generate_complete_game` | Full game in one call (presets: open_world_city, fps_arena, minimal) |
|
| 161 |
+
|
| 162 |
+
## The system prompt
|
| 163 |
+
|
| 164 |
+
`unity_agent.orchestrator.prompts.SYSTEM_PROMPT` is a long, prescriptive
|
| 165 |
+
prompt that teaches the LLM to:
|
| 166 |
+
|
| 167 |
+
* Think like a senior Unity engineer (lifecycle, physics step, idiomatic
|
| 168 |
+
C#).
|
| 169 |
+
* Know the tool catalog and the recommended workflow (scaffold ->
|
| 170 |
+
components -> bootstrap script -> scene).
|
| 171 |
+
* Always emit code that compiles on the first try in Unity 2022.3 LTS.
|
| 172 |
+
* Prefer procedural content (runtime `Start()` generation) over
|
| 173 |
+
inspector-wired prefabs so the example "just plays" on open.
|
| 174 |
+
* Use the `UnityAgent` namespace and the `UnityAgent.Scripts` asmdef.
|
| 175 |
+
|
| 176 |
+
Use it like this from your own LLM driver:
|
| 177 |
+
|
| 178 |
+
```python
|
| 179 |
+
from unity_agent.orchestrator import SYSTEM_PROMPT, build_user_prompt
|
| 180 |
+
|
| 181 |
+
prompt = build_user_prompt("Build me an open-world city game I can drive around in")
|
| 182 |
+
# Send SYSTEM_PROMPT + prompt to your LLM, then dispatch tool calls.
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
## Knowledge base
|
| 186 |
+
|
| 187 |
+
Eight focused markdown references in `unity_agent/knowledge/entries/`:
|
| 188 |
+
|
| 189 |
+
1. `unity_fundamentals.md` -- MonoBehaviour lifecycle, GameObjects,
|
| 190 |
+
components, coroutines, inspector best practices.
|
| 191 |
+
2. `unity_physics.md` -- Rigidbody, colliders, raycasting, ForceModes,
|
| 192 |
+
physics vs frame step.
|
| 193 |
+
3. `unity_rendering.md` -- Materials, Standard shader, lighting, post-
|
| 194 |
+
processing, LOD, fog.
|
| 195 |
+
4. `unity_navigation.md` -- NavMesh, NavMeshAgent, AI state machines,
|
| 196 |
+
NavMeshObstacle, off-mesh links.
|
| 197 |
+
5. `unity_input_system.md` -- Legacy Input Manager, mouse-look recipes,
|
| 198 |
+
new Input System, touch.
|
| 199 |
+
6. `unity_ui.md` -- Canvas, anchoring, HUD pattern, TMP, performance.
|
| 200 |
+
7. `unity_audio.md` -- AudioSource, 3D audio, AudioMixer, AudioManager
|
| 201 |
+
singleton.
|
| 202 |
+
8. `open_world_design.md` -- Procedural generation, LOD, streaming,
|
| 203 |
+
day/night cycle, save systems.
|
| 204 |
+
|
| 205 |
+
Load them programmatically:
|
| 206 |
+
|
| 207 |
+
```python
|
| 208 |
+
from unity_agent.knowledge import list_entries, load_entry, load_all
|
| 209 |
+
|
| 210 |
+
for slug in list_entries():
|
| 211 |
+
print(slug, len(load_entry(slug)))
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
## QA
|
| 215 |
+
|
| 216 |
+
`unity_agent.qa.ProjectQA` runs static checks against a generated project:
|
| 217 |
+
|
| 218 |
+
* Required directories exist (`Assets/`, `Assets/Scripts/`, `Packages/`,
|
| 219 |
+
`ProjectSettings/`).
|
| 220 |
+
* Required files exist (`Packages/manifest.json`,
|
| 221 |
+
`ProjectSettings/ProjectVersion.txt`,
|
| 222 |
+
`ProjectSettings/ProjectSettings.asset`).
|
| 223 |
+
* Every `.cs` file has balanced braces and at least one type declaration.
|
| 224 |
+
* Every `.cs` and `.unity` file has a sibling `.meta`.
|
| 225 |
+
* Scene files start with the `%YAML` header.
|
| 226 |
+
* `manifest.json` parses as JSON and has a `dependencies` object.
|
| 227 |
+
|
| 228 |
+
```python
|
| 229 |
+
from unity_agent.qa import ProjectQA
|
| 230 |
+
report = ProjectQA("examples/open_world_city").run()
|
| 231 |
+
print(report.summary())
|
| 232 |
+
print("PASS" if report.passed else "FAIL")
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
## Tests
|
| 236 |
+
|
| 237 |
+
```bash
|
| 238 |
+
python -m unity_agent.tests
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
Verifies the tool registry has 24 entries, the project scaffolder writes
|
| 242 |
+
the expected files, generated C# has balanced braces, the full game
|
| 243 |
+
generator produces 12+ scripts, and QA passes on a freshly generated
|
| 244 |
+
project.
|
| 245 |
+
|
| 246 |
+
## Requirements
|
| 247 |
+
|
| 248 |
+
* Python 3.9+ (no third-party dependencies).
|
| 249 |
+
* Unity Hub + Unity 2022.3 LTS (or newer) to actually open and play the
|
| 250 |
+
generated projects. The Python layer works without Unity installed.
|
| 251 |
+
|
| 252 |
+
## License
|
| 253 |
+
|
| 254 |
+
MIT.
|
examples/generate_example.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Regenerate the open_world_city example using the unity_agent package.
|
| 2 |
+
|
| 3 |
+
Usage::
|
| 4 |
+
|
| 5 |
+
python examples/generate_example.py
|
| 6 |
+
|
| 7 |
+
This re-creates ``examples/open_world_city/`` from scratch using the same
|
| 8 |
+
tool chain the orchestrator would use.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import sys
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
# Make the package importable when run directly.
|
| 17 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 18 |
+
sys.path.insert(0, str(ROOT))
|
| 19 |
+
|
| 20 |
+
from unity_agent.config import Settings
|
| 21 |
+
from unity_agent.transport.unity_transport import UnityTransport
|
| 22 |
+
from unity_agent.tools.unity_tools import (
|
| 23 |
+
GenerateCompleteGameTool,
|
| 24 |
+
WriteCSharpScriptTool,
|
| 25 |
+
)
|
| 26 |
+
from unity_agent.qa import ProjectQA
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
CAMERA_CONTROLLER_CS = """\
|
| 30 |
+
using UnityEngine;
|
| 31 |
+
|
| 32 |
+
namespace UnityAgent
|
| 33 |
+
{
|
| 34 |
+
/// <summary>
|
| 35 |
+
/// Convenience alias for <see cref="ThirdPersonCamera"/>. Lets designers
|
| 36 |
+
/// attach a component named CameraController if they prefer that name.
|
| 37 |
+
/// </summary>
|
| 38 |
+
public class CameraController : ThirdPersonCamera
|
| 39 |
+
{
|
| 40 |
+
// All behaviour is inherited from ThirdPersonCamera.
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def main() -> int:
|
| 47 |
+
examples_dir = ROOT / "examples"
|
| 48 |
+
settings = Settings(output_dir=str(examples_dir), product_name="open_world_city")
|
| 49 |
+
transport = UnityTransport(settings)
|
| 50 |
+
transport.open_project("open_world_city")
|
| 51 |
+
|
| 52 |
+
# Clear the destination so we always start fresh.
|
| 53 |
+
import shutil
|
| 54 |
+
if (examples_dir / "open_world_city").exists():
|
| 55 |
+
shutil.rmtree(examples_dir / "open_world_city")
|
| 56 |
+
transport.open_project("open_world_city")
|
| 57 |
+
|
| 58 |
+
result = GenerateCompleteGameTool(settings, transport).run(
|
| 59 |
+
game_name="open_world_city", preset="open_world_city",
|
| 60 |
+
project_name="open_world_city",
|
| 61 |
+
)
|
| 62 |
+
print(result.summary)
|
| 63 |
+
|
| 64 |
+
# Add the CameraController alias the user spec asks for.
|
| 65 |
+
cam_result = WriteCSharpScriptTool(settings, transport).run(
|
| 66 |
+
filename="CameraController.cs", code=CAMERA_CONTROLLER_CS,
|
| 67 |
+
project_name="open_world_city",
|
| 68 |
+
)
|
| 69 |
+
print(cam_result.summary)
|
| 70 |
+
|
| 71 |
+
# QA the result.
|
| 72 |
+
report = ProjectQA(examples_dir / "open_world_city").run()
|
| 73 |
+
print(report.summary())
|
| 74 |
+
return 0 if report.passed else 1
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
sys.exit(main())
|
examples/open_world_city/Assets/Scenes/MainScene.unity
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%YAML 1.1
|
| 2 |
+
%TAG !u! tag:unity3d.com,2011:
|
| 3 |
+
--- !u!29 &1
|
| 4 |
+
OcclusionCullingSettings:
|
| 5 |
+
m_ObjectHideFlags: 0
|
| 6 |
+
serializedVersion: 2
|
| 7 |
+
m_OcclusionBakeSettings:
|
| 8 |
+
smallestOccluder: 5
|
| 9 |
+
smallestHole: 0.25
|
| 10 |
+
backfaceThreshold: 100
|
| 11 |
+
m_SceneGUID: 00000000000000000000000000000000
|
| 12 |
+
m_OcclusionCullingData: {fileID: 0}
|
| 13 |
+
--- !u!104 &2
|
| 14 |
+
RenderSettings:
|
| 15 |
+
m_ObjectHideFlags: 0
|
| 16 |
+
serializedVersion: 9
|
| 17 |
+
m_Fog: 0
|
| 18 |
+
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
| 19 |
+
m_FogMode: 3
|
| 20 |
+
m_FogDensity: 0.01
|
| 21 |
+
m_LinearFogStart: 0
|
| 22 |
+
m_LinearFogEnd: 300
|
| 23 |
+
m_AmbientSkyColor: {r: 0.4, g: 0.4, b: 0.4, a: 1}
|
| 24 |
+
m_AmbientEquatorColor: {r: 0.2, g: 0.2, b: 0.2, a: 1}
|
| 25 |
+
m_AmbientGroundColor: {r: 0.1, g: 0.1, b: 0.1, a: 1}
|
| 26 |
+
m_AmbientIntensity: 1
|
| 27 |
+
m_AmbientMode: 0
|
| 28 |
+
m_SubtractiveShadowColor: {r: 0.2, g: 0.2, b: 0.2, a: 1}
|
| 29 |
+
m_SkyboxMaterial: {fileID: 0}
|
| 30 |
+
m_HaloStrength: 0.5
|
| 31 |
+
m_FlareStrength: 1
|
| 32 |
+
m_FlareFadeSpeed: 3
|
| 33 |
+
m_HaloTexture: {fileID: 0}
|
| 34 |
+
m_SpotCookie: {fileID: 0}
|
| 35 |
+
m_DefaultReflectionMode: 0
|
| 36 |
+
m_DefaultReflectionResolution: 128
|
| 37 |
+
m_ReflectionBounces: 1
|
| 38 |
+
m_ReflectionIntensity: 1
|
| 39 |
+
m_CustomReflection: {fileID: 0}
|
| 40 |
+
m_Sun: {fileID: 0}
|
| 41 |
+
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
|
| 42 |
+
m_UseRadianceAmbientProbe: 0
|
| 43 |
+
--- !u!157 &3
|
| 44 |
+
LightmapSettings:
|
| 45 |
+
m_ObjectHideFlags: 0
|
| 46 |
+
serializedVersion: 12
|
| 47 |
+
m_GIWorkflowMode: 0
|
| 48 |
+
m_EnableBakedLightmaps: 1
|
| 49 |
+
m_EnableRealtimeLightmaps: 0
|
| 50 |
+
--- !u!196 &4
|
| 51 |
+
NavMeshSettings:
|
| 52 |
+
m_ObjectHideFlags: 0
|
| 53 |
+
serializedVersion: 2
|
| 54 |
+
m_AutoGenerate: 0
|
| 55 |
+
--- !u!1 &100
|
| 56 |
+
GameObject:
|
| 57 |
+
m_ObjectHideFlags: 0
|
| 58 |
+
m_Name: Main Camera
|
| 59 |
+
m_IsActive: 1
|
| 60 |
+
m_Component:
|
| 61 |
+
- component: {fileID: 101}
|
| 62 |
+
- component: {fileID: 102}
|
| 63 |
+
m_Layer: 0
|
| 64 |
+
--- !u!4 &101
|
| 65 |
+
Transform:
|
| 66 |
+
m_GameObject: {fileID: 100}
|
| 67 |
+
m_LocalPosition: {x: 0, y: 5, z: -10}
|
| 68 |
+
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
| 69 |
+
m_LocalScale: {x: 1, y: 1, z: 1}
|
| 70 |
+
--- !u!20 &102
|
| 71 |
+
Camera:
|
| 72 |
+
m_GameObject: {fileID: 100}
|
| 73 |
+
m_ClearFlags: 1
|
| 74 |
+
m_BackGroundColor: {r: 0.4, g: 0.6, b: 0.9, a: 1}
|
| 75 |
+
m_Orthographic: 0
|
| 76 |
+
m_FieldOfView: 60
|
| 77 |
+
m_NearClipPlane: 0.3
|
| 78 |
+
m_FarClipPlane: 1000
|
| 79 |
+
--- !u!1 &200
|
| 80 |
+
GameObject:
|
| 81 |
+
m_ObjectHideFlags: 0
|
| 82 |
+
m_Name: Directional Light
|
| 83 |
+
m_IsActive: 1
|
| 84 |
+
m_Component:
|
| 85 |
+
- component: {fileID: 201}
|
| 86 |
+
- component: {fileID: 202}
|
| 87 |
+
m_Layer: 0
|
| 88 |
+
--- !u!4 &201
|
| 89 |
+
Transform:
|
| 90 |
+
m_GameObject: {fileID: 200}
|
| 91 |
+
m_LocalRotation: {x: 0.4, y: -0.3, z: 0.2, w: 0.8}
|
| 92 |
+
m_LocalPosition: {x: 0, y: 50, z: 0}
|
| 93 |
+
m_LocalScale: {x: 1, y: 1, z: 1}
|
| 94 |
+
--- !u!108 &202
|
| 95 |
+
Light:
|
| 96 |
+
m_GameObject: {fileID: 200}
|
| 97 |
+
m_Type: 1
|
| 98 |
+
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
| 99 |
+
m_Intensity: 1
|
| 100 |
+
m_Shadows:
|
| 101 |
+
m_Type: 2
|
examples/open_world_city/Assets/Scenes/MainScene.unity.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 8a32e127d322fab3b2c0affc00e372cd
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/AudioManager.cs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
|
| 3 |
+
namespace UnityAgent
|
| 4 |
+
{
|
| 5 |
+
/// <summary>
|
| 6 |
+
/// Singleton audio manager that exposes named one-shot SFX and music
|
| 7 |
+
/// channels. Used by other systems to play clips without having to manage
|
| 8 |
+
/// their own AudioSource components.
|
| 9 |
+
/// </summary>
|
| 10 |
+
public class AudioManager : MonoBehaviour
|
| 11 |
+
{
|
| 12 |
+
public static AudioManager Instance { get; private set; }
|
| 13 |
+
|
| 14 |
+
[Header("Mixers")]
|
| 15 |
+
public AudioSource sfxSource;
|
| 16 |
+
public AudioSource musicSource;
|
| 17 |
+
|
| 18 |
+
[Range(0f, 1f)] public float masterVolume = 1f;
|
| 19 |
+
[Range(0f, 1f)] public float sfxVolume = 0.8f;
|
| 20 |
+
[Range(0f, 1f)] public float musicVolume = 0.5f;
|
| 21 |
+
|
| 22 |
+
private void Awake()
|
| 23 |
+
{
|
| 24 |
+
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
| 25 |
+
Instance = this;
|
| 26 |
+
DontDestroyOnLoad(gameObject);
|
| 27 |
+
|
| 28 |
+
if (sfxSource == null)
|
| 29 |
+
{
|
| 30 |
+
sfxSource = gameObject.AddComponent<AudioSource>();
|
| 31 |
+
sfxSource.playOnAwake = false;
|
| 32 |
+
}
|
| 33 |
+
if (musicSource == null)
|
| 34 |
+
{
|
| 35 |
+
musicSource = gameObject.AddComponent<AudioSource>();
|
| 36 |
+
musicSource.loop = true;
|
| 37 |
+
musicSource.playOnAwake = false;
|
| 38 |
+
}
|
| 39 |
+
ApplyVolumes();
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
public void PlaySfx(AudioClip clip, float volumeScale = 1f)
|
| 43 |
+
{
|
| 44 |
+
if (clip != null && sfxSource != null)
|
| 45 |
+
sfxSource.PlayOneShot(clip, volumeScale * sfxVolume * masterVolume);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
public void PlayMusic(AudioClip clip)
|
| 49 |
+
{
|
| 50 |
+
if (clip == null || musicSource == null) return;
|
| 51 |
+
musicSource.clip = clip;
|
| 52 |
+
musicSource.volume = musicVolume * masterVolume;
|
| 53 |
+
musicSource.Play();
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
public void StopMusic()
|
| 57 |
+
{
|
| 58 |
+
if (musicSource != null) musicSource.Stop();
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
public void ApplyVolumes()
|
| 62 |
+
{
|
| 63 |
+
if (musicSource != null) musicSource.volume = musicVolume * masterVolume;
|
| 64 |
+
AudioListener.volume = masterVolume;
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
}
|
examples/open_world_city/Assets/Scripts/AudioManager.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 803745e8c679b595410a0a143b1abc48
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/BuildingGenerator.cs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
|
| 3 |
+
namespace UnityAgent
|
| 4 |
+
{
|
| 5 |
+
/// <summary>
|
| 6 |
+
/// Procedural building generator: builds a box building mesh with
|
| 7 |
+
/// emissive windows and adds a BoxCollider for collision.
|
| 8 |
+
/// </summary>
|
| 9 |
+
public static class BuildingGenerator
|
| 10 |
+
{
|
| 11 |
+
public static GameObject Generate(string name, Vector3 position, Vector3 size,
|
| 12 |
+
Material wallMaterial, Material windowMaterial)
|
| 13 |
+
{
|
| 14 |
+
GameObject go = new GameObject(name);
|
| 15 |
+
go.transform.position = position;
|
| 16 |
+
|
| 17 |
+
// Walls
|
| 18 |
+
GameObject walls = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
| 19 |
+
walls.name = "Walls";
|
| 20 |
+
walls.transform.SetParent(go.transform, false);
|
| 21 |
+
walls.transform.localScale = size;
|
| 22 |
+
walls.transform.localPosition = Vector3.up * size.y * 0.5f;
|
| 23 |
+
if (wallMaterial != null) walls.GetComponent<MeshRenderer>().sharedMaterial = wallMaterial;
|
| 24 |
+
|
| 25 |
+
// Windows: a smaller cube with the window material, slightly offset on each side.
|
| 26 |
+
if (windowMaterial != null)
|
| 27 |
+
{
|
| 28 |
+
GameObject windows = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
| 29 |
+
windows.name = "Windows";
|
| 30 |
+
windows.transform.SetParent(go.transform, false);
|
| 31 |
+
windows.transform.localScale = size * 0.99f;
|
| 32 |
+
windows.transform.localPosition = Vector3.up * size.y * 0.5f;
|
| 33 |
+
var mr = windows.GetComponent<MeshRenderer>();
|
| 34 |
+
mr.sharedMaterial = windowMaterial;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Collider (the cube already has one, but ensure it's a BoxCollider).
|
| 38 |
+
var box = go.GetComponent<BoxCollider>();
|
| 39 |
+
if (box == null) box = go.AddComponent<BoxCollider>();
|
| 40 |
+
box.size = size;
|
| 41 |
+
box.center = Vector3.up * size.y * 0.5f;
|
| 42 |
+
|
| 43 |
+
return go;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/// <summary>
|
| 47 |
+
/// Build an emissive window material procedurally so the agent does
|
| 48 |
+
/// not depend on any asset on disk.
|
| 49 |
+
/// </summary>
|
| 50 |
+
public static Material MakeWindowMaterial(Color glow)
|
| 51 |
+
{
|
| 52 |
+
var m = new Material(Shader.Find("Standard"));
|
| 53 |
+
m.name = "WindowMat";
|
| 54 |
+
m.color = Color.black;
|
| 55 |
+
m.SetColor("_EmissionColor", glow);
|
| 56 |
+
m.EnableKeyword("_EMISSION");
|
| 57 |
+
return m;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
public static Material MakeWallMaterial(Color col)
|
| 61 |
+
{
|
| 62 |
+
var m = new Material(Shader.Find("Standard"));
|
| 63 |
+
m.name = "WallMat";
|
| 64 |
+
m.color = col;
|
| 65 |
+
return m;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
examples/open_world_city/Assets/Scripts/BuildingGenerator.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 720373d252ebafade9462c2ee8876f1c
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/CameraController.cs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
|
| 3 |
+
namespace UnityAgent
|
| 4 |
+
{
|
| 5 |
+
/// <summary>
|
| 6 |
+
/// Convenience alias for <see cref="ThirdPersonCamera"/>. Lets designers
|
| 7 |
+
/// attach a component named CameraController if they prefer that name.
|
| 8 |
+
/// </summary>
|
| 9 |
+
public class CameraController : ThirdPersonCamera
|
| 10 |
+
{
|
| 11 |
+
// All behaviour is inherited from ThirdPersonCamera.
|
| 12 |
+
}
|
| 13 |
+
}
|
examples/open_world_city/Assets/Scripts/CameraController.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 34509a07673ea2c5921f70e933984b60
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/CityGenerator.cs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
|
| 3 |
+
namespace UnityAgent
|
| 4 |
+
{
|
| 5 |
+
/// <summary>
|
| 6 |
+
/// Procedural city generator: spawns a grid of buildings of varying
|
| 7 |
+
/// heights with emissive windows, street lights and parked vehicles.
|
| 8 |
+
/// Designed to be paired with <see cref="RoadNetwork"/>.
|
| 9 |
+
/// </summary>
|
| 10 |
+
public class CityGenerator : MonoBehaviour
|
| 11 |
+
{
|
| 12 |
+
[Header("Grid")]
|
| 13 |
+
public int gridX = 8;
|
| 14 |
+
public int gridZ = 8;
|
| 15 |
+
public float blockSpacing = 30f;
|
| 16 |
+
|
| 17 |
+
[Header("Buildings")]
|
| 18 |
+
public int buildingsPerBlock = 1;
|
| 19 |
+
public float minBuildingHeight = 8f;
|
| 20 |
+
public float maxBuildingHeight = 40f;
|
| 21 |
+
public float buildingFootprint = 8f;
|
| 22 |
+
public Color wallColor = new Color(0.4f, 0.4f, 0.42f);
|
| 23 |
+
public Color windowColor = new Color(1.0f, 0.85f, 0.4f);
|
| 24 |
+
|
| 25 |
+
[Header("Street Lights")]
|
| 26 |
+
public bool spawnStreetLights = true;
|
| 27 |
+
public float lightSpacing = 15f;
|
| 28 |
+
public Color lightColor = new Color(1.0f, 0.85f, 0.5f);
|
| 29 |
+
public float lightIntensity = 1.5f;
|
| 30 |
+
public float lightRange = 15f;
|
| 31 |
+
|
| 32 |
+
private readonly System.Collections.Generic.List<Light> _lights = new System.Collections.Generic.List<Light>();
|
| 33 |
+
|
| 34 |
+
private void Start()
|
| 35 |
+
{
|
| 36 |
+
Generate();
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
public void Generate()
|
| 40 |
+
{
|
| 41 |
+
Material wallMat = BuildingGenerator.MakeWallMaterial(wallColor);
|
| 42 |
+
Material winMat = BuildingGenerator.MakeWindowMaterial(windowColor);
|
| 43 |
+
|
| 44 |
+
float totalX = (gridX - 1) * blockSpacing;
|
| 45 |
+
float totalZ = (gridZ - 1) * blockSpacing;
|
| 46 |
+
Vector3 origin = transform.position - new Vector3(totalX * 0.5f, 0f, totalZ * 0.5f);
|
| 47 |
+
|
| 48 |
+
for (int x = 0; x < gridX; x++)
|
| 49 |
+
{
|
| 50 |
+
for (int z = 0; z < gridZ; z++)
|
| 51 |
+
{
|
| 52 |
+
Vector3 block = new Vector3(origin.x + x * blockSpacing, 0f, origin.z + z * blockSpacing);
|
| 53 |
+
SpawnBlock(block, wallMat, winMat);
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
if (spawnStreetLights) SpawnStreetLights(origin, totalX, totalZ);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
private void SpawnBlock(Vector3 center, Material wallMat, Material winMat)
|
| 61 |
+
{
|
| 62 |
+
for (int i = 0; i < buildingsPerBlock; i++)
|
| 63 |
+
{
|
| 64 |
+
float h = Random.Range(minBuildingHeight, maxBuildingHeight);
|
| 65 |
+
Vector3 size = new Vector3(buildingFootprint, h, buildingFootprint);
|
| 66 |
+
Vector3 pos = center + new Vector3(
|
| 67 |
+
Random.Range(-4f, 4f), 0f, Random.Range(-4f, 4f));
|
| 68 |
+
BuildingGenerator.Generate($"Building_{center.x}_{center.z}_{i}", pos, size, wallMat, winMat);
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
private void SpawnStreetLights(Vector3 origin, float totalX, float totalZ)
|
| 73 |
+
{
|
| 74 |
+
Material poleMat = BuildingGenerator.MakeWallMaterial(new Color(0.1f, 0.1f, 0.1f));
|
| 75 |
+
|
| 76 |
+
for (float x = origin.x; x <= origin.x + totalX + 0.1f; x += lightSpacing)
|
| 77 |
+
{
|
| 78 |
+
for (float z = origin.z; z <= origin.z + totalZ + 0.1f; z += lightSpacing)
|
| 79 |
+
{
|
| 80 |
+
SpawnStreetLight(new Vector3(x, 0f, z), poleMat);
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
private void SpawnStreetLight(Vector3 pos, Material poleMat)
|
| 86 |
+
{
|
| 87 |
+
GameObject pole = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
| 88 |
+
pole.name = "StreetLight";
|
| 89 |
+
pole.transform.SetParent(transform, false);
|
| 90 |
+
pole.transform.position = pos + Vector3.up * 4f;
|
| 91 |
+
pole.transform.localScale = new Vector3(0.2f, 4f, 0.2f);
|
| 92 |
+
pole.GetComponent<MeshRenderer>().sharedMaterial = poleMat;
|
| 93 |
+
|
| 94 |
+
GameObject bulb = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
| 95 |
+
bulb.transform.SetParent(pole.transform, false);
|
| 96 |
+
bulb.transform.localPosition = Vector3.up * 0.9f;
|
| 97 |
+
bulb.transform.localScale = Vector3.one * 0.4f;
|
| 98 |
+
bulb.GetComponent<MeshRenderer>().sharedMaterial = BuildingGenerator.MakeWindowMaterial(lightColor);
|
| 99 |
+
|
| 100 |
+
Light l = bulb.AddComponent<Light>();
|
| 101 |
+
l.type = LightType.Point;
|
| 102 |
+
l.color = lightColor;
|
| 103 |
+
l.intensity = lightIntensity;
|
| 104 |
+
l.range = lightRange;
|
| 105 |
+
_lights.Add(l);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
public Light[] StreetLights => _lights.ToArray();
|
| 109 |
+
}
|
| 110 |
+
}
|
examples/open_world_city/Assets/Scripts/CityGenerator.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: a1590f0f60a74829eaa91c784094575b
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/DayNightCycle.cs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
|
| 3 |
+
namespace UnityAgent
|
| 4 |
+
{
|
| 5 |
+
/// <summary>
|
| 6 |
+
/// Rotates a directional light around the world to simulate a day/night
|
| 7 |
+
/// cycle. Also lerps between day and night sky/ambient colors and turns
|
| 8 |
+
/// street lights on after sunset.
|
| 9 |
+
/// </summary>
|
| 10 |
+
public class DayNightCycle : MonoBehaviour
|
| 11 |
+
{
|
| 12 |
+
[Header("Sun")]
|
| 13 |
+
public Light sun;
|
| 14 |
+
public float dayLengthSeconds = 120f;
|
| 15 |
+
|
| 16 |
+
[Header("Colors")]
|
| 17 |
+
public Color daySky = new Color(0.5f, 0.7f, 1.0f);
|
| 18 |
+
public Color nightSky = new Color(0.03f, 0.04f, 0.08f);
|
| 19 |
+
public Color dayAmbient = new Color(0.6f, 0.6f, 0.6f);
|
| 20 |
+
public Color nightAmbient = new Color(0.12f, 0.13f, 0.18f);
|
| 21 |
+
|
| 22 |
+
[Header("Lights")]
|
| 23 |
+
public Light[] streetLights;
|
| 24 |
+
public float streetLightThreshold = 0.2f;
|
| 25 |
+
|
| 26 |
+
private float _timeOfDay; // 0..1
|
| 27 |
+
|
| 28 |
+
private void Start()
|
| 29 |
+
{
|
| 30 |
+
if (sun == null) sun = GetComponent<Light>();
|
| 31 |
+
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
private void Update()
|
| 35 |
+
{
|
| 36 |
+
_timeOfDay = (_timeOfDay + Time.deltaTime / dayLengthSeconds) % 1f;
|
| 37 |
+
|
| 38 |
+
// Sun angle: at timeOfDay=0.25 sun is at noon.
|
| 39 |
+
float sunAngle = (_timeOfDay - 0.25f) * 360f;
|
| 40 |
+
if (sun != null)
|
| 41 |
+
{
|
| 42 |
+
sun.transform.rotation = Quaternion.Euler(sunAngle, -30f, 0f);
|
| 43 |
+
float dayFactor = Mathf.Clamp01(Mathf.Cos((_timeOfDay - 0.25f) * Mathf.PI * 2f) * 0.5f + 0.5f);
|
| 44 |
+
sun.intensity = dayFactor * 1.2f;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
float t = Mathf.Clamp01(Mathf.Cos((_timeOfDay - 0.25f) * Mathf.PI * 2f) * 0.5f + 0.5f);
|
| 48 |
+
RenderSettings.ambientLight = Color.Lerp(nightAmbient, dayAmbient, t);
|
| 49 |
+
Camera mainCam = Camera.main;
|
| 50 |
+
if (mainCam != null)
|
| 51 |
+
{
|
| 52 |
+
mainCam.backgroundColor = Color.Lerp(nightSky, daySky, t);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
UpdateStreetLights(t);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
private void UpdateStreetLights(float dayFactor)
|
| 59 |
+
{
|
| 60 |
+
if (streetLights == null) return;
|
| 61 |
+
bool on = dayFactor < streetLightThreshold;
|
| 62 |
+
foreach (var l in streetLights)
|
| 63 |
+
if (l != null) l.enabled = on;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/// <summary>Current time of day in 0..1 range (0=midnight).</summary>
|
| 67 |
+
public float TimeOfDay => _timeOfDay;
|
| 68 |
+
}
|
| 69 |
+
}
|
examples/open_world_city/Assets/Scripts/DayNightCycle.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 06ebd9e278d5fc98279370f9128b86f9
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/GameManager.cs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
|
| 3 |
+
namespace UnityAgent
|
| 4 |
+
{
|
| 5 |
+
/// <summary>
|
| 6 |
+
/// Bootstrap that wires up the generated systems on scene load. The
|
| 7 |
+
/// scene only needs an empty GameObject with this component attached.
|
| 8 |
+
/// </summary>
|
| 9 |
+
public class GameManager : MonoBehaviour
|
| 10 |
+
{
|
| 11 |
+
[Header("Player")]
|
| 12 |
+
public GameObject playerPrefab;
|
| 13 |
+
public Vector3 spawnPosition = new Vector3(0f, 2f, 0f);
|
| 14 |
+
|
| 15 |
+
[Header("City")]
|
| 16 |
+
public int cityGridX = 8;
|
| 17 |
+
public int cityGridZ = 8;
|
| 18 |
+
|
| 19 |
+
private void Start()
|
| 20 |
+
{
|
| 21 |
+
SpawnPlayer();
|
| 22 |
+
SpawnCity();
|
| 23 |
+
SpawnLights();
|
| 24 |
+
SetupCamera();
|
| 25 |
+
SetupAudio();
|
| 26 |
+
SetupUI();
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
private void SpawnPlayer()
|
| 30 |
+
{
|
| 31 |
+
GameObject player = playerPrefab != null
|
| 32 |
+
? Instantiate(playerPrefab, spawnPosition, Quaternion.identity)
|
| 33 |
+
: CreatePrimitivePlayer();
|
| 34 |
+
player.tag = "Player";
|
| 35 |
+
player.name = "Player";
|
| 36 |
+
|
| 37 |
+
var pc = player.GetComponent<PlayerController>();
|
| 38 |
+
if (pc == null) pc = player.AddComponent<PlayerController>();
|
| 39 |
+
var rb = player.GetComponent<Rigidbody>();
|
| 40 |
+
if (rb == null) rb = player.AddComponent<Rigidbody>();
|
| 41 |
+
var col = player.GetComponent<CapsuleCollider>();
|
| 42 |
+
if (col == null) col = player.AddComponent<CapsuleCollider>();
|
| 43 |
+
col.height = 2f; col.radius = 0.4f; col.center = new Vector3(0f, 1f, 0f);
|
| 44 |
+
rb.constraints = RigidbodyConstraints.FreezeRotation;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
private GameObject CreatePrimitivePlayer()
|
| 48 |
+
{
|
| 49 |
+
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
| 50 |
+
go.transform.position = spawnPosition;
|
| 51 |
+
return go;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
private void SpawnCity()
|
| 55 |
+
{
|
| 56 |
+
GameObject city = new GameObject("City");
|
| 57 |
+
var gen = city.AddComponent<CityGenerator>();
|
| 58 |
+
gen.gridX = cityGridX;
|
| 59 |
+
gen.gridZ = cityGridZ;
|
| 60 |
+
|
| 61 |
+
GameObject roads = new GameObject("Roads");
|
| 62 |
+
roads.transform.SetParent(city.transform, false);
|
| 63 |
+
var net = roads.AddComponent<RoadNetwork>();
|
| 64 |
+
net.gridSize = Mathf.Max(cityGridX, cityGridZ);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
private void SpawnLights()
|
| 68 |
+
{
|
| 69 |
+
GameObject sunGo = new GameObject("Sun");
|
| 70 |
+
var sun = sunGo.AddComponent<Light>();
|
| 71 |
+
sun.type = LightType.Directional;
|
| 72 |
+
sun.intensity = 1.2f;
|
| 73 |
+
sun.shadows = LightShadows.Soft;
|
| 74 |
+
|
| 75 |
+
var dnc = sunGo.AddComponent<DayNightCycle>();
|
| 76 |
+
dnc.sun = sun;
|
| 77 |
+
// Bind street lights from the city generator after one frame.
|
| 78 |
+
StartCoroutine(BindStreetLights(dnc));
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
private System.Collections.IEnumerator BindStreetLights(DayNightCycle dnc)
|
| 82 |
+
{
|
| 83 |
+
yield return null; // allow CityGenerator.Start() to run
|
| 84 |
+
var cg = FindObjectOfType<CityGenerator>();
|
| 85 |
+
if (cg != null) dnc.streetLights = cg.StreetLights;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
private void SetupCamera()
|
| 89 |
+
{
|
| 90 |
+
Camera cam = Camera.main;
|
| 91 |
+
if (cam == null)
|
| 92 |
+
{
|
| 93 |
+
var go = new GameObject("Main Camera");
|
| 94 |
+
cam = go.AddComponent<Camera>();
|
| 95 |
+
go.tag = "MainCamera";
|
| 96 |
+
go.AddComponent<AudioListener>();
|
| 97 |
+
}
|
| 98 |
+
var tpc = cam.GetComponent<ThirdPersonCamera>();
|
| 99 |
+
if (tpc == null) tpc = cam.gameObject.AddComponent<ThirdPersonCamera>();
|
| 100 |
+
var player = GameObject.FindGameObjectWithTag("Player");
|
| 101 |
+
tpc.target = player != null ? player.transform : null;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
private void SetupAudio()
|
| 105 |
+
{
|
| 106 |
+
var go = new GameObject("AudioManager");
|
| 107 |
+
go.AddComponent<AudioManager>();
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
private void SetupUI()
|
| 111 |
+
{
|
| 112 |
+
var go = new GameObject("UIManager");
|
| 113 |
+
go.AddComponent<UIManager>();
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
examples/open_world_city/Assets/Scripts/GameManager.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 146f64a8241e4314114edb414ce6d721
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/HealthSystem.cs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
using UnityEngine.Events;
|
| 3 |
+
|
| 4 |
+
namespace UnityAgent
|
| 5 |
+
{
|
| 6 |
+
/// <summary>
|
| 7 |
+
/// Damageable entity with health regen and death event. Pairs with the
|
| 8 |
+
/// optional <see cref="HealthBar"/> UI component.
|
| 9 |
+
/// </summary>
|
| 10 |
+
public class HealthSystem : MonoBehaviour
|
| 11 |
+
{
|
| 12 |
+
[Header("Health")]
|
| 13 |
+
public float maxHealth = 100f;
|
| 14 |
+
public float currentHealth { get; private set; }
|
| 15 |
+
public float regenPerSecond = 1f;
|
| 16 |
+
public float regenDelay = 5f;
|
| 17 |
+
|
| 18 |
+
[Header("Events")]
|
| 19 |
+
public UnityEvent onDeath;
|
| 20 |
+
public UnityEvent<float> onDamaged;
|
| 21 |
+
|
| 22 |
+
private float _lastDamageTime;
|
| 23 |
+
private bool _dead;
|
| 24 |
+
|
| 25 |
+
public float Normalised => currentHealth / maxHealth;
|
| 26 |
+
|
| 27 |
+
private void Awake()
|
| 28 |
+
{
|
| 29 |
+
currentHealth = maxHealth;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
private void Update()
|
| 33 |
+
{
|
| 34 |
+
if (_dead) return;
|
| 35 |
+
if (Time.time - _lastDamageTime > regenDelay && currentHealth < maxHealth)
|
| 36 |
+
{
|
| 37 |
+
currentHealth = Mathf.Min(maxHealth, currentHealth + regenPerSecond * Time.deltaTime);
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
public void TakeDamage(float amount)
|
| 42 |
+
{
|
| 43 |
+
if (_dead) return;
|
| 44 |
+
currentHealth = Mathf.Max(0f, currentHealth - amount);
|
| 45 |
+
_lastDamageTime = Time.time;
|
| 46 |
+
onDamaged?.Invoke(amount);
|
| 47 |
+
if (currentHealth <= 0f) Die();
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
public void Heal(float amount)
|
| 51 |
+
{
|
| 52 |
+
if (_dead) return;
|
| 53 |
+
currentHealth = Mathf.Min(maxHealth, currentHealth + amount);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
private void Die()
|
| 57 |
+
{
|
| 58 |
+
_dead = true;
|
| 59 |
+
onDeath?.Invoke();
|
| 60 |
+
Destroy(gameObject, 0.5f);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/// <summary>
|
| 65 |
+
/// Simple health bar that reads a <see cref="HealthSystem"/> and updates
|
| 66 |
+
/// a child <c>Image.fillAmount</c>.
|
| 67 |
+
/// </summary>
|
| 68 |
+
public class HealthBar : MonoBehaviour
|
| 69 |
+
{
|
| 70 |
+
public HealthSystem source;
|
| 71 |
+
public UnityEngine.UI.Image fillImage;
|
| 72 |
+
public bool hideWhenFull = true;
|
| 73 |
+
|
| 74 |
+
private void Update()
|
| 75 |
+
{
|
| 76 |
+
if (source == null || fillImage == null) return;
|
| 77 |
+
float n = source.Normalised;
|
| 78 |
+
fillImage.fillAmount = n;
|
| 79 |
+
if (hideWhenFull) fillImage.transform.parent.gameObject.SetActive(n < 0.999f);
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
examples/open_world_city/Assets/Scripts/HealthSystem.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 3e2b705888cccf6861f62175541c5a1d
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/NPCController.cs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
using UnityEngine.AI;
|
| 3 |
+
|
| 4 |
+
namespace UnityAgent
|
| 5 |
+
{
|
| 6 |
+
/// <summary>
|
| 7 |
+
/// AI NPC that wanders between random points and chases the player when
|
| 8 |
+
/// they enter the aggro radius. Uses Unity's NavMeshAgent.
|
| 9 |
+
/// </summary>
|
| 10 |
+
[RequireComponent(typeof(NavMeshAgent))]
|
| 11 |
+
public class NPCController : MonoBehaviour
|
| 12 |
+
{
|
| 13 |
+
public enum State { Idle, Wander, Chase }
|
| 14 |
+
|
| 15 |
+
[Header("References")]
|
| 16 |
+
public Transform player;
|
| 17 |
+
|
| 18 |
+
[Header("Behaviour")]
|
| 19 |
+
public State initialState = State.Wander;
|
| 20 |
+
public float wanderRadius = 20f;
|
| 21 |
+
public float aggroRange = 12f;
|
| 22 |
+
public float loseSightRange = 20f;
|
| 23 |
+
public float idleDuration = 2f;
|
| 24 |
+
public float wanderSpeed = 2.5f;
|
| 25 |
+
public float chaseSpeed = 5f;
|
| 26 |
+
|
| 27 |
+
[Header("Debug")]
|
| 28 |
+
public bool drawGizmos = true;
|
| 29 |
+
|
| 30 |
+
private NavMeshAgent _agent;
|
| 31 |
+
private State _state;
|
| 32 |
+
private float _stateTimer;
|
| 33 |
+
private Vector3 _startPos;
|
| 34 |
+
|
| 35 |
+
private void Awake()
|
| 36 |
+
{
|
| 37 |
+
_agent = GetComponent<NavMeshAgent>();
|
| 38 |
+
_agent.speed = wanderSpeed;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
private void Start()
|
| 42 |
+
{
|
| 43 |
+
_state = initialState;
|
| 44 |
+
_startPos = transform.position;
|
| 45 |
+
if (player == null && GameObject.FindGameObjectWithTag("Player") != null)
|
| 46 |
+
player = GameObject.FindGameObjectWithTag("Player").transform;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
private void Update()
|
| 50 |
+
{
|
| 51 |
+
switch (_state)
|
| 52 |
+
{
|
| 53 |
+
case State.Idle: TickIdle(); break;
|
| 54 |
+
case State.Wander: TickWander(); break;
|
| 55 |
+
case State.Chase: TickChase(); break;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
private void TickIdle()
|
| 60 |
+
{
|
| 61 |
+
_stateTimer -= Time.deltaTime;
|
| 62 |
+
if (PlayerInRange(aggroRange)) { EnterChase(); return; }
|
| 63 |
+
if (_stateTimer <= 0f) EnterWander();
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
private void TickWander()
|
| 67 |
+
{
|
| 68 |
+
if (PlayerInRange(aggroRange)) { EnterChase(); return; }
|
| 69 |
+
if (_agent.remainingDistance <= _agent.stoppingDistance && !_agent.pathPending)
|
| 70 |
+
EnterIdle();
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
private void TickChase()
|
| 74 |
+
{
|
| 75 |
+
if (player == null) { EnterWander(); return; }
|
| 76 |
+
_agent.SetDestination(player.position);
|
| 77 |
+
|
| 78 |
+
if (!PlayerInRange(loseSightRange)) EnterWander();
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
private void EnterIdle()
|
| 82 |
+
{
|
| 83 |
+
_state = State.Idle;
|
| 84 |
+
_stateTimer = idleDuration;
|
| 85 |
+
_agent.isStopped = true;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
private void EnterWander()
|
| 89 |
+
{
|
| 90 |
+
_state = State.Wander;
|
| 91 |
+
_agent.speed = wanderSpeed;
|
| 92 |
+
_agent.isStopped = false;
|
| 93 |
+
Vector3 dir = Random.insideUnitSphere * wanderRadius;
|
| 94 |
+
dir.y = 0f;
|
| 95 |
+
Vector3 target = _startPos + dir;
|
| 96 |
+
if (NavMesh.SamplePosition(target, out NavMeshHit hit, wanderRadius, NavMesh.AllAreas))
|
| 97 |
+
_agent.SetDestination(hit.position);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
private void EnterChase()
|
| 101 |
+
{
|
| 102 |
+
_state = State.Chase;
|
| 103 |
+
_agent.speed = chaseSpeed;
|
| 104 |
+
_agent.isStopped = false;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
private bool PlayerInRange(float range)
|
| 108 |
+
{
|
| 109 |
+
if (player == null) return false;
|
| 110 |
+
return Vector3.Distance(transform.position, player.position) <= range;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
private void OnDrawGizmosSelected()
|
| 114 |
+
{
|
| 115 |
+
if (!drawGizmos) return;
|
| 116 |
+
Gizmos.color = Color.yellow;
|
| 117 |
+
Gizmos.DrawWireSphere(transform.position, aggroRange);
|
| 118 |
+
Gizmos.color = Color.red;
|
| 119 |
+
Gizmos.DrawWireSphere(transform.position, loseSightRange);
|
| 120 |
+
Gizmos.color = Color.green;
|
| 121 |
+
Gizmos.DrawWireSphere(_startPos == Vector3.zero ? transform.position : _startPos, wanderRadius);
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
}
|
examples/open_world_city/Assets/Scripts/NPCController.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: bdb23444532d812096a8a0bf7c0f341f
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/PickupSystem.cs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
using UnityEngine.Events;
|
| 3 |
+
|
| 4 |
+
namespace UnityAgent
|
| 5 |
+
{
|
| 6 |
+
/// <summary>
|
| 7 |
+
/// Collectible pickup. Triggers an event when collected and adds to the
|
| 8 |
+
/// global PickupSystem score.
|
| 9 |
+
/// </summary>
|
| 10 |
+
[RequireComponent(typeof(Collider))]
|
| 11 |
+
public class Pickup : MonoBehaviour
|
| 12 |
+
{
|
| 13 |
+
public int value = 10;
|
| 14 |
+
public AudioClip collectClip;
|
| 15 |
+
public UnityEvent onCollected;
|
| 16 |
+
|
| 17 |
+
private void Reset()
|
| 18 |
+
{
|
| 19 |
+
GetComponent<Collider>().isTrigger = true;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
private void OnTriggerEnter(Collider other)
|
| 23 |
+
{
|
| 24 |
+
if (!other.CompareTag("Player")) return;
|
| 25 |
+
PickupSystem.Instance?.Collect(value);
|
| 26 |
+
onCollected?.Invoke();
|
| 27 |
+
if (collectClip != null && PickupSystem.Instance != null)
|
| 28 |
+
PickupSystem.Instance.PlayPickupSound(collectClip);
|
| 29 |
+
Destroy(gameObject);
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/// <summary>
|
| 34 |
+
/// Singleton that tracks the player's score and plays pickup sounds.
|
| 35 |
+
/// </summary>
|
| 36 |
+
public class PickupSystem : MonoBehaviour
|
| 37 |
+
{
|
| 38 |
+
public static PickupSystem Instance { get; private set; }
|
| 39 |
+
|
| 40 |
+
public int score { get; private set; }
|
| 41 |
+
public AudioSource audioSource;
|
| 42 |
+
|
| 43 |
+
public System.Action<int> onScoreChanged;
|
| 44 |
+
|
| 45 |
+
private void Awake()
|
| 46 |
+
{
|
| 47 |
+
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
| 48 |
+
Instance = this;
|
| 49 |
+
if (audioSource == null) audioSource = gameObject.AddComponent<AudioSource>();
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
public void Collect(int amount)
|
| 53 |
+
{
|
| 54 |
+
score += amount;
|
| 55 |
+
onScoreChanged?.Invoke(score);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
public void PlayPickupSound(AudioClip clip)
|
| 59 |
+
{
|
| 60 |
+
if (audioSource != null && clip != null) audioSource.PlayOneShot(clip);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
examples/open_world_city/Assets/Scripts/PickupSystem.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 08af630d5809345e6854281a491a6740
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/PlayerController.cs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
|
| 3 |
+
namespace UnityAgent
|
| 4 |
+
{
|
| 5 |
+
/// <summary>
|
| 6 |
+
/// Third-person character controller with WASD movement, jump and a
|
| 7 |
+
/// rigidbody-based physics response. Designed to be paired with the
|
| 8 |
+
/// ThirdPersonCamera component.
|
| 9 |
+
/// </summary>
|
| 10 |
+
[RequireComponent(typeof(Rigidbody))]
|
| 11 |
+
[RequireComponent(typeof(CapsuleCollider))]
|
| 12 |
+
public class PlayerController : MonoBehaviour
|
| 13 |
+
{
|
| 14 |
+
[Header("Movement")]
|
| 15 |
+
public float moveSpeed = 6f;
|
| 16 |
+
public float sprintMultiplier = 1.6f;
|
| 17 |
+
public float rotationSmooth = 12f;
|
| 18 |
+
public float jumpForce = 7.5f;
|
| 19 |
+
|
| 20 |
+
[Header("Ground Check")]
|
| 21 |
+
public float groundCheckDistance = 0.2f;
|
| 22 |
+
public LayerMask groundMask = ~0;
|
| 23 |
+
|
| 24 |
+
[Header("Camera")]
|
| 25 |
+
public Transform cameraTransform;
|
| 26 |
+
|
| 27 |
+
private Rigidbody _rb;
|
| 28 |
+
private CapsuleCollider _col;
|
| 29 |
+
private Vector2 _moveInput;
|
| 30 |
+
private bool _jumpRequested;
|
| 31 |
+
private bool _isGrounded;
|
| 32 |
+
private float _targetRotation;
|
| 33 |
+
|
| 34 |
+
private void Awake()
|
| 35 |
+
{
|
| 36 |
+
_rb = GetComponent<Rigidbody>();
|
| 37 |
+
_col = GetComponent<CapsuleCollider>();
|
| 38 |
+
_rb.freezeRotation = true;
|
| 39 |
+
_rb.interpolation = RigidbodyInterpolation.Interpolate;
|
| 40 |
+
if (cameraTransform == null && Camera.main != null)
|
| 41 |
+
cameraTransform = Camera.main.transform;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
private void Update()
|
| 45 |
+
{
|
| 46 |
+
_moveInput.x = Input.GetAxisRaw("Horizontal");
|
| 47 |
+
_moveInput.y = Input.GetAxisRaw("Vertical");
|
| 48 |
+
_moveInput = Vector2.ClampMagnitude(_moveInput, 1f);
|
| 49 |
+
|
| 50 |
+
if (Input.GetButtonDown("Jump") && _isGrounded)
|
| 51 |
+
_jumpRequested = true;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
private void FixedUpdate()
|
| 55 |
+
{
|
| 56 |
+
GroundCheck();
|
| 57 |
+
Move();
|
| 58 |
+
if (_jumpRequested)
|
| 59 |
+
{
|
| 60 |
+
_rb.AddForce(Vector3.up * jumpForce, ForceMode.VelocityChange);
|
| 61 |
+
_jumpRequested = false;
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
private void GroundCheck()
|
| 66 |
+
{
|
| 67 |
+
Vector3 origin = transform.position + Vector3.up * (_col.height * 0.5f - _col.radius);
|
| 68 |
+
_isGrounded = Physics.SphereCast(
|
| 69 |
+
origin, _col.radius * 0.9f, Vector3.down,
|
| 70 |
+
out _, groundCheckDistance, groundMask, QueryTriggerInteraction.Ignore);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
private void Move()
|
| 74 |
+
{
|
| 75 |
+
float speed = moveSpeed;
|
| 76 |
+
if (Input.GetKey(KeyCode.LeftShift)) speed *= sprintMultiplier;
|
| 77 |
+
|
| 78 |
+
if (_moveInput.sqrMagnitude < 0.01f)
|
| 79 |
+
{
|
| 80 |
+
// Apply horizontal damping so the character stops cleanly.
|
| 81 |
+
Vector3 v = _rb.velocity;
|
| 82 |
+
v.x *= 0.85f;
|
| 83 |
+
v.z *= 0.85f;
|
| 84 |
+
_rb.velocity = v;
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
Vector3 camForward = cameraTransform ? cameraTransform.forward : transform.forward;
|
| 89 |
+
Vector3 camRight = cameraTransform ? cameraTransform.right : transform.right;
|
| 90 |
+
camForward.y = 0f; camForward.Normalize();
|
| 91 |
+
camRight.y = 0f; camRight.Normalize();
|
| 92 |
+
|
| 93 |
+
Vector3 desired = (camForward * _moveInput.y + camRight * _moveInput.x) * speed;
|
| 94 |
+
Vector3 velocity = _rb.velocity;
|
| 95 |
+
velocity.x = desired.x;
|
| 96 |
+
velocity.z = desired.z;
|
| 97 |
+
_rb.velocity = velocity;
|
| 98 |
+
|
| 99 |
+
if (desired.sqrMagnitude > 0.01f)
|
| 100 |
+
{
|
| 101 |
+
_targetRotation = Mathf.Atan2(desired.x, desired.z) * Mathf.Rad2Deg;
|
| 102 |
+
float smoothed = Mathf.LerpAngle(
|
| 103 |
+
transform.rotation.eulerAngles.y, _targetRotation,
|
| 104 |
+
Time.fixedDeltaTime * rotationSmooth);
|
| 105 |
+
_rb.MoveRotation(Quaternion.Euler(0f, smoothed, 0f));
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
}
|
examples/open_world_city/Assets/Scripts/PlayerController.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: c6566cf7b83ff91f376a7d09d43b5fd3
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/RoadNetwork.cs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System.Collections.Generic;
|
| 2 |
+
using UnityEngine;
|
| 3 |
+
|
| 4 |
+
namespace UnityAgent
|
| 5 |
+
{
|
| 6 |
+
/// <summary>
|
| 7 |
+
/// Lays out a grid road network at runtime: builds thin box meshes for
|
| 8 |
+
/// roads and an intersection marker at every grid crossing.
|
| 9 |
+
/// </summary>
|
| 10 |
+
public class RoadNetwork : MonoBehaviour
|
| 11 |
+
{
|
| 12 |
+
[Header("Grid")]
|
| 13 |
+
public int gridSize = 5;
|
| 14 |
+
public float blockSpacing = 30f;
|
| 15 |
+
public float roadWidth = 6f;
|
| 16 |
+
public float roadThickness = 0.1f;
|
| 17 |
+
|
| 18 |
+
[Header("Materials")]
|
| 19 |
+
public Material roadMaterial;
|
| 20 |
+
public Material intersectionMaterial;
|
| 21 |
+
|
| 22 |
+
private readonly List<GameObject> _roads = new List<GameObject>();
|
| 23 |
+
|
| 24 |
+
private void Start()
|
| 25 |
+
{
|
| 26 |
+
Build();
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
public void Build()
|
| 30 |
+
{
|
| 31 |
+
foreach (var r in _roads) if (r != null) Destroy(r);
|
| 32 |
+
_roads.Clear();
|
| 33 |
+
|
| 34 |
+
float total = (gridSize - 1) * blockSpacing;
|
| 35 |
+
Vector3 origin = transform.position - new Vector3(total * 0.5f, 0f, total * 0.5f);
|
| 36 |
+
|
| 37 |
+
for (int i = 0; i < gridSize; i++)
|
| 38 |
+
{
|
| 39 |
+
// Horizontal road
|
| 40 |
+
_roads.Add(SpawnRoad(
|
| 41 |
+
new Vector3(origin.x + total * 0.5f, 0f, origin.z + i * blockSpacing),
|
| 42 |
+
new Vector3(total + roadWidth, roadThickness, roadWidth),
|
| 43 |
+
roadMaterial, $"Road_H_{i}"));
|
| 44 |
+
|
| 45 |
+
// Vertical road
|
| 46 |
+
_roads.Add(SpawnRoad(
|
| 47 |
+
new Vector3(origin.x + i * blockSpacing, 0f, origin.z + total * 0.5f),
|
| 48 |
+
new Vector3(roadWidth, roadThickness, total + roadWidth),
|
| 49 |
+
roadMaterial, $"Road_V_{i}"));
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Intersections.
|
| 53 |
+
for (int x = 0; x < gridSize; x++)
|
| 54 |
+
{
|
| 55 |
+
for (int z = 0; z < gridSize; z++)
|
| 56 |
+
{
|
| 57 |
+
Vector3 p = new Vector3(origin.x + x * blockSpacing, 0f, origin.z + z * blockSpacing);
|
| 58 |
+
_roads.Add(SpawnRoad(p, new Vector3(roadWidth * 1.2f, roadThickness * 1.5f, roadWidth * 1.2f),
|
| 59 |
+
intersectionMaterial, $"Intersection_{x}_{z}"));
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
private GameObject SpawnRoad(Vector3 pos, Vector3 scale, Material mat, string name)
|
| 65 |
+
{
|
| 66 |
+
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
| 67 |
+
go.name = name;
|
| 68 |
+
go.transform.SetParent(transform, false);
|
| 69 |
+
go.transform.position = pos + transform.position;
|
| 70 |
+
go.transform.localScale = scale;
|
| 71 |
+
if (mat != null) go.GetComponent<MeshRenderer>().sharedMaterial = mat;
|
| 72 |
+
return go;
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
}
|
examples/open_world_city/Assets/Scripts/RoadNetwork.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 4afc638bbfb2ca81ab494f215750fc85
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/SaveSystem.cs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System;
|
| 2 |
+
using System.IO;
|
| 3 |
+
using UnityEngine;
|
| 4 |
+
|
| 5 |
+
namespace UnityAgent
|
| 6 |
+
{
|
| 7 |
+
/// <summary>
|
| 8 |
+
/// JSON save system using <see cref="JsonUtility"/>. Stores a single
|
| 9 |
+
/// save slot per profile in <c>Application.persistentDataPath</c>.
|
| 10 |
+
/// </summary>
|
| 11 |
+
public class SaveSystem : MonoBehaviour
|
| 12 |
+
{
|
| 13 |
+
public static SaveSystem Instance { get; private set; }
|
| 14 |
+
|
| 15 |
+
public string profileName = "default";
|
| 16 |
+
|
| 17 |
+
private void Awake()
|
| 18 |
+
{
|
| 19 |
+
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
| 20 |
+
Instance = this;
|
| 21 |
+
DontDestroyOnLoad(gameObject);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
private string SavePath => Path.Combine(Application.persistentDataPath, profileName + ".save");
|
| 25 |
+
|
| 26 |
+
public void Save<T>(T data) where T : class
|
| 27 |
+
{
|
| 28 |
+
try
|
| 29 |
+
{
|
| 30 |
+
string json = JsonUtility.ToJson(new Wrapper<T> { payload = data }, true);
|
| 31 |
+
File.WriteAllText(SavePath, json);
|
| 32 |
+
Debug.Log($"[SaveSystem] Saved to {SavePath}");
|
| 33 |
+
}
|
| 34 |
+
catch (Exception e)
|
| 35 |
+
{
|
| 36 |
+
Debug.LogError($"[SaveSystem] Save failed: {e}");
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
public T Load<T>() where T : class
|
| 41 |
+
{
|
| 42 |
+
if (!File.Exists(SavePath)) return null;
|
| 43 |
+
try
|
| 44 |
+
{
|
| 45 |
+
string json = File.ReadAllText(SavePath);
|
| 46 |
+
var w = JsonUtility.FromJson<Wrapper<T>>(json);
|
| 47 |
+
return w?.payload;
|
| 48 |
+
}
|
| 49 |
+
catch (Exception e)
|
| 50 |
+
{
|
| 51 |
+
Debug.LogError($"[SaveSystem] Load failed: {e}");
|
| 52 |
+
return null;
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
public bool HasSave => File.Exists(SavePath);
|
| 57 |
+
|
| 58 |
+
public void Delete()
|
| 59 |
+
{
|
| 60 |
+
if (File.Exists(SavePath)) File.Delete(SavePath);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
[Serializable]
|
| 64 |
+
private class Wrapper<T> { public T payload; }
|
| 65 |
+
}
|
| 66 |
+
}
|
examples/open_world_city/Assets/Scripts/SaveSystem.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 713fe181501c9d094612732235d78b10
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/ThirdPersonCamera.cs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
|
| 3 |
+
namespace UnityAgent
|
| 4 |
+
{
|
| 5 |
+
/// <summary>
|
| 6 |
+
/// Smooth third-person camera with collision handling. Attach to a Camera
|
| 7 |
+
/// GameObject and assign the target transform in the inspector.
|
| 8 |
+
/// </summary>
|
| 9 |
+
public class ThirdPersonCamera : MonoBehaviour
|
| 10 |
+
{
|
| 11 |
+
[Header("Target")]
|
| 12 |
+
public Transform target;
|
| 13 |
+
|
| 14 |
+
[Header("Positioning")]
|
| 15 |
+
public Vector3 pivotOffset = new Vector3(0f, 1.5f, 0f);
|
| 16 |
+
public float distance = 5f;
|
| 17 |
+
public float minDistance = 1.5f;
|
| 18 |
+
public float maxDistance = 8f;
|
| 19 |
+
public float height = 1.2f;
|
| 20 |
+
|
| 21 |
+
[Header("Smoothing")]
|
| 22 |
+
public float positionSmooth = 10f;
|
| 23 |
+
public float rotationSmooth = 10f;
|
| 24 |
+
|
| 25 |
+
[Header("Collision")]
|
| 26 |
+
public LayerMask collisionMask = ~0;
|
| 27 |
+
public float collisionRadius = 0.3f;
|
| 28 |
+
|
| 29 |
+
[Header("Input")]
|
| 30 |
+
public bool useMouseOrbit = true;
|
| 31 |
+
public float mouseSensitivity = 3f;
|
| 32 |
+
public float minPitch = -30f;
|
| 33 |
+
public float maxPitch = 75f;
|
| 34 |
+
|
| 35 |
+
private float _yaw;
|
| 36 |
+
private float _pitch = 15f;
|
| 37 |
+
private float _currentDistance;
|
| 38 |
+
|
| 39 |
+
private void Start()
|
| 40 |
+
{
|
| 41 |
+
_currentDistance = distance;
|
| 42 |
+
if (target != null)
|
| 43 |
+
{
|
| 44 |
+
Vector3 e = transform.eulerAngles;
|
| 45 |
+
_yaw = e.y; _pitch = e.x;
|
| 46 |
+
}
|
| 47 |
+
Cursor.lockState = CursorLockMode.Locked;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
private void LateUpdate()
|
| 51 |
+
{
|
| 52 |
+
if (target == null) return;
|
| 53 |
+
|
| 54 |
+
if (useMouseOrbit)
|
| 55 |
+
{
|
| 56 |
+
_yaw += Input.GetAxis("Mouse X") * mouseSensitivity;
|
| 57 |
+
_pitch -= Input.GetAxis("Mouse Y") * mouseSensitivity;
|
| 58 |
+
_pitch = Mathf.Clamp(_pitch, minPitch, maxPitch);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
Quaternion rotation = Quaternion.Euler(_pitch, _yaw, 0f);
|
| 62 |
+
Vector3 pivot = target.position + pivotOffset;
|
| 63 |
+
|
| 64 |
+
// Desired camera position.
|
| 65 |
+
Vector3 desired = pivot + rotation * new Vector3(0f, height, -distance);
|
| 66 |
+
|
| 67 |
+
// Collision: shrink the distance if there's an obstacle.
|
| 68 |
+
if (Physics.SphereCast(
|
| 69 |
+
pivot, collisionRadius, (desired - pivot).normalized,
|
| 70 |
+
out RaycastHit hit, distance, collisionMask, QueryTriggerInteraction.Ignore))
|
| 71 |
+
{
|
| 72 |
+
_currentDistance = Mathf.Clamp(hit.distance, minDistance, distance);
|
| 73 |
+
}
|
| 74 |
+
else
|
| 75 |
+
{
|
| 76 |
+
_currentDistance = Mathf.Lerp(_currentDistance, distance, Time.deltaTime * 4f);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
Vector3 finalPos = pivot + rotation * new Vector3(0f, height, -_currentDistance);
|
| 80 |
+
transform.position = Vector3.Lerp(transform.position, finalPos, positionSmooth * Time.deltaTime);
|
| 81 |
+
transform.rotation = Quaternion.Slerp(
|
| 82 |
+
transform.rotation, rotation, rotationSmooth * Time.deltaTime);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
examples/open_world_city/Assets/Scripts/ThirdPersonCamera.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: d76bc6d6a55fca62d80d72f6cff57325
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/UIManager.cs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
using UnityEngine.UI;
|
| 3 |
+
using TMPro;
|
| 4 |
+
|
| 5 |
+
namespace UnityAgent
|
| 6 |
+
{
|
| 7 |
+
/// <summary>
|
| 8 |
+
/// Lightweight HUD manager using Unity UI + TextMeshPro. Exposes methods
|
| 9 |
+
/// for other systems to update score / health / messages.
|
| 10 |
+
/// </summary>
|
| 11 |
+
public class UIManager : MonoBehaviour
|
| 12 |
+
{
|
| 13 |
+
public static UIManager Instance { get; private set; }
|
| 14 |
+
|
| 15 |
+
[Header("HUD")]
|
| 16 |
+
public TMP_Text scoreText;
|
| 17 |
+
public TMP_Text healthText;
|
| 18 |
+
public TMP_Text messageText;
|
| 19 |
+
public Image healthBarFill;
|
| 20 |
+
|
| 21 |
+
[Header("Message")]
|
| 22 |
+
public float messageDuration = 2f;
|
| 23 |
+
|
| 24 |
+
private float _messageTimer;
|
| 25 |
+
|
| 26 |
+
private void Awake()
|
| 27 |
+
{
|
| 28 |
+
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
| 29 |
+
Instance = this;
|
| 30 |
+
SetMessage(string.Empty);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
private void Update()
|
| 34 |
+
{
|
| 35 |
+
if (_messageTimer > 0f)
|
| 36 |
+
{
|
| 37 |
+
_messageTimer -= Time.deltaTime;
|
| 38 |
+
if (_messageTimer <= 0f && messageText != null) messageText.text = string.Empty;
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
public void SetScore(int score)
|
| 43 |
+
{
|
| 44 |
+
if (scoreText != null) scoreText.text = $"Score: {score}";
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
public void SetHealth(float current, float max)
|
| 48 |
+
{
|
| 49 |
+
if (healthText != null) healthText.text = $"HP: {Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}";
|
| 50 |
+
if (healthBarFill != null) healthBarFill.fillAmount = Mathf.Clamp01(current / max);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
public void SetMessage(string msg)
|
| 54 |
+
{
|
| 55 |
+
if (messageText != null) messageText.text = msg;
|
| 56 |
+
_messageTimer = string.IsNullOrEmpty(msg) ? 0f : messageDuration;
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
examples/open_world_city/Assets/Scripts/UIManager.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: dcbdcd3761369cee5079e3e57f514884
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/UnityAgent.Scripts.asmdef
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "UnityAgent.Scripts",
|
| 3 |
+
"rootNamespace": "UnityAgent",
|
| 4 |
+
"references": [],
|
| 5 |
+
"includePlatforms": [],
|
| 6 |
+
"excludePlatforms": [],
|
| 7 |
+
"allowUnsafeCode": false,
|
| 8 |
+
"autoReferenced": true,
|
| 9 |
+
"defineConstraints": [],
|
| 10 |
+
"versionDefines": []
|
| 11 |
+
}
|
examples/open_world_city/Assets/Scripts/UnityAgent.Scripts.asmdef.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 51397beb9916b964e08c142ac4dd2500
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Assets/Scripts/VehicleController.cs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnityEngine;
|
| 2 |
+
|
| 3 |
+
namespace UnityAgent
|
| 4 |
+
{
|
| 5 |
+
/// <summary>
|
| 6 |
+
/// Arcade car physics: motor torque, steering, braking and a simple
|
| 7 |
+
/// four-wheel raycast suspension. Place this on a root GameObject with
|
| 8 |
+
/// a Rigidbody and four wheel transforms.
|
| 9 |
+
/// </summary>
|
| 10 |
+
[RequireComponent(typeof(Rigidbody))]
|
| 11 |
+
public class VehicleController : MonoBehaviour
|
| 12 |
+
{
|
| 13 |
+
[System.Serializable]
|
| 14 |
+
public class Wheel
|
| 15 |
+
{
|
| 16 |
+
public Transform transform;
|
| 17 |
+
public bool isSteering;
|
| 18 |
+
public bool isPowered;
|
| 19 |
+
public bool isBraking;
|
| 20 |
+
[HideInInspector] public bool isGrounded;
|
| 21 |
+
[HideInInspector] public float suspensionOffset;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
[Header("Wheels")]
|
| 25 |
+
public Wheel[] wheels = new Wheel[4];
|
| 26 |
+
|
| 27 |
+
[Header("Engine")]
|
| 28 |
+
public float motorTorque = 1500f;
|
| 29 |
+
public float brakeTorque = 3000f;
|
| 30 |
+
public float maxSteerAngle = 30f;
|
| 31 |
+
public float maxSpeedKph = 180f;
|
| 32 |
+
|
| 33 |
+
[Header("Suspension")]
|
| 34 |
+
public float suspensionRestLength = 0.5f;
|
| 35 |
+
public float suspensionSpring = 35000f;
|
| 36 |
+
public float suspensionDamper = 4500f;
|
| 37 |
+
public float wheelRadius = 0.35f;
|
| 38 |
+
public LayerMask wheelMask = ~0;
|
| 39 |
+
|
| 40 |
+
[Header("Aerodynamics")]
|
| 41 |
+
public float downforce = 100f;
|
| 42 |
+
public float centerOfMassOffset = -0.5f;
|
| 43 |
+
|
| 44 |
+
private Rigidbody _rb;
|
| 45 |
+
private float _throttle;
|
| 46 |
+
private float _steer;
|
| 47 |
+
private float _brake;
|
| 48 |
+
|
| 49 |
+
private void Awake()
|
| 50 |
+
{
|
| 51 |
+
_rb = GetComponent<Rigidbody>();
|
| 52 |
+
_rb.centerOfMass += Vector3.up * centerOfMassOffset;
|
| 53 |
+
_rb.interpolation = RigidbodyInterpolation.Interpolate;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
private void Update()
|
| 57 |
+
{
|
| 58 |
+
_throttle = Input.GetAxis("Vertical");
|
| 59 |
+
_steer = Input.GetAxis("Horizontal");
|
| 60 |
+
_brake = Input.GetKey(KeyCode.Space) ? 1f : 0f;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
private void FixedUpdate()
|
| 64 |
+
{
|
| 65 |
+
UpdateWheels();
|
| 66 |
+
ApplySuspension();
|
| 67 |
+
ApplyDrive();
|
| 68 |
+
ApplyDownforce();
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
private void UpdateWheels()
|
| 72 |
+
{
|
| 73 |
+
foreach (var w in wheels)
|
| 74 |
+
{
|
| 75 |
+
if (w.transform == null) continue;
|
| 76 |
+
Vector3 origin = w.transform.position;
|
| 77 |
+
bool hit = Physics.Raycast(
|
| 78 |
+
origin, -transform.up, out RaycastHit r,
|
| 79 |
+
suspensionRestLength + wheelRadius, wheelMask,
|
| 80 |
+
QueryTriggerInteraction.Ignore);
|
| 81 |
+
w.isGrounded = hit;
|
| 82 |
+
w.suspensionOffset = hit ? r.distance - wheelRadius : suspensionRestLength;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
private void ApplySuspension()
|
| 87 |
+
{
|
| 88 |
+
foreach (var w in wheels)
|
| 89 |
+
{
|
| 90 |
+
if (!w.isGrounded) continue;
|
| 91 |
+
float force = (suspensionRestLength - w.suspensionOffset) * suspensionSpring;
|
| 92 |
+
Vector3 velocity = _rb.GetPointVelocity(w.transform.position);
|
| 93 |
+
float upwardVel = Vector3.Dot(transform.up, velocity);
|
| 94 |
+
force -= upwardVel * suspensionDamper;
|
| 95 |
+
_rb.AddForceAtPosition(transform.up * Mathf.Max(0f, force), w.transform.position);
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
private void ApplyDrive()
|
| 100 |
+
{
|
| 101 |
+
float kph = _rb.velocity.magnitude * 3.6f;
|
| 102 |
+
float speedFactor = Mathf.Clamp01(1f - kph / maxSpeedKph);
|
| 103 |
+
|
| 104 |
+
foreach (var w in wheels)
|
| 105 |
+
{
|
| 106 |
+
if (!w.isGrounded) continue;
|
| 107 |
+
if (w.isPowered)
|
| 108 |
+
{
|
| 109 |
+
Vector3 force = transform.forward * (_throttle * motorTorque * speedFactor);
|
| 110 |
+
_rb.AddForceAtPosition(force, w.transform.position, ForceMode.Force);
|
| 111 |
+
}
|
| 112 |
+
if (w.isBraking && _brake > 0.01f)
|
| 113 |
+
{
|
| 114 |
+
Vector3 brakeForce = -transform.forward * (_brake * brakeTorque);
|
| 115 |
+
_rb.AddForceAtPosition(brakeForce, w.transform.position, ForceMode.Force);
|
| 116 |
+
}
|
| 117 |
+
if (w.isSteering)
|
| 118 |
+
{
|
| 119 |
+
// Rotate the visual wheel and apply a small yaw torque.
|
| 120 |
+
w.transform.localRotation = Quaternion.Euler(0f, _steer * maxSteerAngle, 0f);
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
// Steering yaw torque.
|
| 124 |
+
if (Mathf.Abs(_steer) > 0.01f)
|
| 125 |
+
{
|
| 126 |
+
float turn = _steer * maxSteerAngle * 0.02f;
|
| 127 |
+
_rb.AddTorque(transform.up * turn * (_rb.velocity.magnitude + 1f), ForceMode.Force);
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
private void ApplyDownforce()
|
| 132 |
+
{
|
| 133 |
+
_rb.AddForce(-transform.up * downforce * _rb.velocity.magnitude);
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
}
|
examples/open_world_city/Assets/Scripts/VehicleController.cs.meta
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fileFormatVersion: 2
|
| 2 |
+
guid: 67179e275140460130f6cafb48739957
|
| 3 |
+
MonoImporter:
|
| 4 |
+
externalObjects: {}
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
defaultReferences: []
|
| 7 |
+
executionOrder: 0
|
| 8 |
+
icon: {instanceID: 0}
|
| 9 |
+
userData:
|
| 10 |
+
assetBundleName:
|
| 11 |
+
assetBundleVariant:
|
examples/open_world_city/Packages/manifest.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dependencies": {
|
| 3 |
+
"com.unity.ai.navigation": "1.1.5",
|
| 4 |
+
"com.unity.cinemachine": "2.9.7",
|
| 5 |
+
"com.unity.collab-proxy": "2.2.0",
|
| 6 |
+
"com.unity.ide.rider": "3.0.27",
|
| 7 |
+
"com.unity.ide.visualstudio": "2.0.22",
|
| 8 |
+
"com.unity.inputsystem": "1.7.0",
|
| 9 |
+
"com.unity.postprocessing": "3.4.0",
|
| 10 |
+
"com.unity.textmeshpro": "3.0.6",
|
| 11 |
+
"com.unity.ugui": "1.0.0",
|
| 12 |
+
"com.unity.modules.ai": "1.0.0",
|
| 13 |
+
"com.unity.modules.animation": "1.0.0",
|
| 14 |
+
"com.unity.modules.audio": "1.0.0",
|
| 15 |
+
"com.unity.modules.imageconversion": "1.0.0",
|
| 16 |
+
"com.unity.modules.particlesystem": "1.0.0",
|
| 17 |
+
"com.unity.modules.physics": "1.0.0",
|
| 18 |
+
"com.unity.modules.physics2d": "1.0.0",
|
| 19 |
+
"com.unity.modules.terrain": "1.0.0",
|
| 20 |
+
"com.unity.modules.terrainphysics": "1.0.0",
|
| 21 |
+
"com.unity.modules.ui": "1.0.0"
|
| 22 |
+
}
|
| 23 |
+
}
|
examples/open_world_city/ProjectSettings/ProjectSettings.asset
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%YAML 1.1
|
| 2 |
+
%TAG !u! tag:unity3d.com,2011:
|
| 3 |
+
--- !u!129 &1
|
| 4 |
+
PlayerSettings:
|
| 5 |
+
m_ObjectHideFlags: 0
|
| 6 |
+
serializedVersion: 26
|
| 7 |
+
companyName: UnityAgent
|
| 8 |
+
productName: open_world_city
|
| 9 |
+
defaultCursor: {instanceID: 0}
|
| 10 |
+
m_Version: 1
|
examples/open_world_city/ProjectSettings/ProjectVersion.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
m_EditorVersion: 2022.3.20f1
|
examples/open_world_city/ProjectSettings/TagManager.asset
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%YAML 1.1
|
| 2 |
+
%TAG !u! tag:unity3d.com,2011:
|
| 3 |
+
--- !u!78 &1
|
| 4 |
+
TagManager:
|
| 5 |
+
serializedVersion: 2
|
| 6 |
+
tags:
|
| 7 |
+
- Player
|
| 8 |
+
- Enemy
|
| 9 |
+
- Pickup
|
| 10 |
+
layers:
|
| 11 |
+
- Default
|
| 12 |
+
- TransparentFX
|
| 13 |
+
- Ignore Raycast
|
| 14 |
+
- Water
|
| 15 |
+
- UI
|
pyproject.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=64", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "unity-agent"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "A Python agent that generates complete Unity game projects (C#, .unity scenes, project config) you can open and play in any Unity 2022+ installation."
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.9"
|
| 11 |
+
license = { text = "MIT" }
|
| 12 |
+
authors = [{ name = "Unity Agent" }]
|
| 13 |
+
keywords = ["unity", "unity3d", "game-development", "code-generation", "agent"]
|
| 14 |
+
|
| 15 |
+
[project.scripts]
|
| 16 |
+
unity-agent = "unity_agent.__main__:main"
|
| 17 |
+
|
| 18 |
+
[tool.setuptools.packages.find]
|
| 19 |
+
where = ["."]
|
| 20 |
+
include = ["unity_agent*"]
|
| 21 |
+
|
| 22 |
+
[tool.setuptools.package-data]
|
| 23 |
+
"unity_agent.knowledge" = ["entries/*.md", "README.md"]
|
unity_agent/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unity Agent - A Python package that generates complete Unity game projects.
|
| 3 |
+
|
| 4 |
+
The agent uses a collection of code-generation tools to emit compilable C#
|
| 5 |
+
scripts, Unity scene (.unity) files, project configuration (.meta, ProjectSettings,
|
| 6 |
+
Packages/manifest.json) and an open-world example game.
|
| 7 |
+
|
| 8 |
+
Quick start
|
| 9 |
+
-----------
|
| 10 |
+
>>> from unity_agent.config import Settings
|
| 11 |
+
>>> from unity_agent.transport.unity_transport import UnityTransport
|
| 12 |
+
>>> from unity_agent.tools.unity_tools import GenerateCompleteGameTool
|
| 13 |
+
>>> settings = Settings(output_dir="my_game")
|
| 14 |
+
>>> transport = UnityTransport(settings)
|
| 15 |
+
>>> GenerateCompleteGameTool(settings, transport).run(game_name="OpenWorldCity")
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from unity_agent.config import Settings
|
| 19 |
+
from unity_agent.transport.unity_transport import UnityTransport
|
| 20 |
+
from unity_agent.tools.base import ToolBase, ToolRegistry
|
| 21 |
+
from unity_agent.orchestrator.prompts import SYSTEM_PROMPT, build_user_prompt
|
| 22 |
+
|
| 23 |
+
__all__ = [
|
| 24 |
+
"Settings",
|
| 25 |
+
"UnityTransport",
|
| 26 |
+
"ToolBase",
|
| 27 |
+
"ToolRegistry",
|
| 28 |
+
"SYSTEM_PROMPT",
|
| 29 |
+
"build_user_prompt",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
__version__ = "0.1.0"
|
unity_agent/__main__.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command-line entry point for the unity_agent package.
|
| 2 |
+
|
| 3 |
+
Usage::
|
| 4 |
+
|
| 5 |
+
python -m unity_agent generate --game OpenWorldCity --preset open_world_city
|
| 6 |
+
python -m unity_agent list-tools
|
| 7 |
+
python -m unity_agent qa examples/open_world_city
|
| 8 |
+
|
| 9 |
+
Run ``python -m unity_agent --help`` for the full list of commands.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import sys
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
from unity_agent.config import Settings
|
| 19 |
+
from unity_agent.transport.unity_transport import UnityTransport
|
| 20 |
+
from unity_agent.tools.base import get_registry
|
| 21 |
+
from unity_agent.tools.unity_tools import GenerateCompleteGameTool, SetupUnityProjectTool
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _cmd_list_tools(args: argparse.Namespace) -> int:
|
| 25 |
+
registry = get_registry()
|
| 26 |
+
for name in registry.all_names():
|
| 27 |
+
cls = registry.get(name)
|
| 28 |
+
print(f" {name:35s} {cls.description}")
|
| 29 |
+
return 0
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _cmd_generate(args: argparse.Namespace) -> int:
|
| 33 |
+
settings = Settings(
|
| 34 |
+
output_dir=args.output_dir,
|
| 35 |
+
product_name=args.game,
|
| 36 |
+
)
|
| 37 |
+
transport = UnityTransport(settings)
|
| 38 |
+
tool = GenerateCompleteGameTool(settings, transport)
|
| 39 |
+
result = tool.run(game_name=args.game, preset=args.preset, project_name=args.game)
|
| 40 |
+
print(result.summary)
|
| 41 |
+
for f in result.files:
|
| 42 |
+
print(f" {f}")
|
| 43 |
+
print()
|
| 44 |
+
print(transport.summary())
|
| 45 |
+
return 0 if result.ok else 1
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _cmd_setup(args: argparse.Namespace) -> int:
|
| 49 |
+
settings = Settings(output_dir=args.output_dir, product_name=args.project_name)
|
| 50 |
+
transport = UnityTransport(settings)
|
| 51 |
+
tool = SetupUnityProjectTool(settings, transport)
|
| 52 |
+
result = tool.run(project_name=args.project_name)
|
| 53 |
+
print(result.summary)
|
| 54 |
+
for f in result.files:
|
| 55 |
+
print(f" {f}")
|
| 56 |
+
return 0 if result.ok else 1
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _cmd_qa(args: argparse.Namespace) -> int:
|
| 60 |
+
from unity_agent.qa import ProjectQA
|
| 61 |
+
qa = ProjectQA(args.project_root)
|
| 62 |
+
report = qa.run()
|
| 63 |
+
print(report.summary())
|
| 64 |
+
for issue in report.issues:
|
| 65 |
+
print(f" [{issue.severity.upper():7}] {issue.category:8} {issue.message} {issue.path}")
|
| 66 |
+
return 0 if report.passed else 1
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 70 |
+
p = argparse.ArgumentParser(prog="unity_agent", description="Unity game agent CLI.")
|
| 71 |
+
sub = p.add_subparsers(dest="cmd", required=True)
|
| 72 |
+
|
| 73 |
+
p_list = sub.add_parser("list-tools", help="List every registered tool.")
|
| 74 |
+
p_list.set_defaults(func=_cmd_list_tools)
|
| 75 |
+
|
| 76 |
+
p_gen = sub.add_parser("generate", help="Generate a complete game.")
|
| 77 |
+
p_gen.add_argument("--game", default="OpenWorldCity", help="Game / project name.")
|
| 78 |
+
p_gen.add_argument("--preset", default="open_world_city",
|
| 79 |
+
choices=["open_world_city", "fps_arena", "minimal"])
|
| 80 |
+
p_gen.add_argument("--output-dir", default="unity_output",
|
| 81 |
+
help="Where to write the project (default: unity_output).")
|
| 82 |
+
p_gen.set_defaults(func=_cmd_generate)
|
| 83 |
+
|
| 84 |
+
p_setup = sub.add_parser("setup", help="Scaffold an empty Unity project.")
|
| 85 |
+
p_setup.add_argument("--project-name", default="GeneratedGame")
|
| 86 |
+
p_setup.add_argument("--output-dir", default="unity_output")
|
| 87 |
+
p_setup.set_defaults(func=_cmd_setup)
|
| 88 |
+
|
| 89 |
+
p_qa = sub.add_parser("qa", help="Run QA checks on a generated project.")
|
| 90 |
+
p_qa.add_argument("project_root", help="Path to the project root.")
|
| 91 |
+
p_qa.set_defaults(func=_cmd_qa)
|
| 92 |
+
|
| 93 |
+
return p
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def main(argv: list[str] | None = None) -> int:
|
| 97 |
+
parser = build_parser()
|
| 98 |
+
args = parser.parse_args(argv)
|
| 99 |
+
return args.func(args)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
if __name__ == "__main__":
|
| 103 |
+
sys.exit(main())
|
unity_agent/config.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Global configuration for the Unity agent.
|
| 2 |
+
|
| 3 |
+
The :class:`Settings` dataclass is the single source of truth for runtime
|
| 4 |
+
parameters such as the Unity installation path, optional LLM API keys, the
|
| 5 |
+
default output directory and project metadata used when scaffolding a new
|
| 6 |
+
Unity project.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
from dataclasses import dataclass, field, asdict
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import Optional
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
DEFAULT_OUTPUT_DIR = "unity_output"
|
| 18 |
+
DEFAULT_UNITY_VERSION = "2022.3.20f1"
|
| 19 |
+
|
| 20 |
+
# Default Unity project metadata. These values are written into
|
| 21 |
+
# ProjectSettings/ProjectSettings.asset so the editor knows what to display
|
| 22 |
+
# in the project wizard and on the splash screen.
|
| 23 |
+
DEFAULT_COMPANY_NAME = "UnityAgent"
|
| 24 |
+
DEFAULT_PRODUCT_NAME = "GeneratedGame"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class Settings:
|
| 29 |
+
"""Runtime configuration shared by every tool.
|
| 30 |
+
|
| 31 |
+
Parameters
|
| 32 |
+
----------
|
| 33 |
+
unity_path:
|
| 34 |
+
Optional path to the Unity executable (``Unity.exe`` / ``Unity``).
|
| 35 |
+
When provided, the agent can optionally invoke Unity in batch mode
|
| 36 |
+
to validate that generated projects compile.
|
| 37 |
+
unity_version:
|
| 38 |
+
Target Unity version written into ``ProjectSettings.asset``.
|
| 39 |
+
output_dir:
|
| 40 |
+
Root directory where generated projects are written. Can be either a
|
| 41 |
+
relative path (resolved against the current working directory) or an
|
| 42 |
+
absolute path.
|
| 43 |
+
company_name / product_name:
|
| 44 |
+
Metadata written into the project settings.
|
| 45 |
+
openai_api_key / anthropic_api_key / zai_api_key:
|
| 46 |
+
Optional LLM provider keys. Only required if the orchestrator is
|
| 47 |
+
wired up to call an LLM directly.
|
| 48 |
+
overwrite:
|
| 49 |
+
If ``True`` existing files are replaced; otherwise a ``FileExistsError``
|
| 50 |
+
is raised when a destination file already exists.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
unity_path: Optional[str] = None
|
| 54 |
+
unity_version: str = DEFAULT_UNITY_VERSION
|
| 55 |
+
output_dir: str = DEFAULT_OUTPUT_DIR
|
| 56 |
+
company_name: str = DEFAULT_COMPANY_NAME
|
| 57 |
+
product_name: str = DEFAULT_PRODUCT_NAME
|
| 58 |
+
|
| 59 |
+
openai_api_key: Optional[str] = field(default_factory=lambda: os.environ.get("OPENAI_API_KEY"))
|
| 60 |
+
anthropic_api_key: Optional[str] = field(default_factory=lambda: os.environ.get("ANTHROPIC_API_KEY"))
|
| 61 |
+
zai_api_key: Optional[str] = field(default_factory=lambda: os.environ.get("ZAI_API_KEY"))
|
| 62 |
+
|
| 63 |
+
overwrite: bool = True
|
| 64 |
+
auto_create_dirs: bool = True
|
| 65 |
+
|
| 66 |
+
# ------------------------------------------------------------------ #
|
| 67 |
+
# Convenience helpers
|
| 68 |
+
# ------------------------------------------------------------------ #
|
| 69 |
+
@property
|
| 70 |
+
def output_path(self) -> Path:
|
| 71 |
+
p = Path(self.output_dir)
|
| 72 |
+
return p if p.is_absolute() else (Path.cwd() / p)
|
| 73 |
+
|
| 74 |
+
def project_path(self, project_name: str) -> Path:
|
| 75 |
+
"""Return the absolute path to a named project inside ``output_dir``."""
|
| 76 |
+
return self.output_path / project_name
|
| 77 |
+
|
| 78 |
+
def to_dict(self) -> dict:
|
| 79 |
+
return asdict(self)
|
| 80 |
+
|
| 81 |
+
@classmethod
|
| 82 |
+
def from_env(cls) -> "Settings":
|
| 83 |
+
"""Build a :class:`Settings` instance populated from environment vars."""
|
| 84 |
+
return cls(
|
| 85 |
+
unity_path=os.environ.get("UNITY_PATH"),
|
| 86 |
+
unity_version=os.environ.get("UNITY_VERSION", DEFAULT_UNITY_VERSION),
|
| 87 |
+
output_dir=os.environ.get("UNITY_AGENT_OUTPUT_DIR", DEFAULT_OUTPUT_DIR),
|
| 88 |
+
company_name=os.environ.get("UNITY_COMPANY_NAME", DEFAULT_COMPANY_NAME),
|
| 89 |
+
product_name=os.environ.get("UNITY_PRODUCT_NAME", DEFAULT_PRODUCT_NAME),
|
| 90 |
+
)
|
unity_agent/knowledge/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Unity Agent Knowledge Base
|
| 2 |
+
|
| 3 |
+
This directory contains markdown knowledge entries the agent consults when
|
| 4 |
+
generating Unity projects. Each file is a short, focused reference on one
|
| 5 |
+
area of Unity development.
|
| 6 |
+
|
| 7 |
+
## Entries
|
| 8 |
+
|
| 9 |
+
| File | Topic |
|
| 10 |
+
|----------------------------|------------------------------------------------------------|
|
| 11 |
+
| `unity_fundamentals.md` | MonoBehaviour lifecycle, GameObjects, components, coroutines |
|
| 12 |
+
| `unity_physics.md` | Rigidbody, colliders, raycasting, physics vs frame step |
|
| 13 |
+
| `unity_rendering.md` | Materials, shaders, lighting, post-processing, LOD, fog |
|
| 14 |
+
| `unity_navigation.md` | NavMesh, NavMeshAgent, AI state machines, obstacles |
|
| 15 |
+
| `unity_input_system.md` | Legacy Input Manager, mouse-look, the new Input System |
|
| 16 |
+
| `unity_ui.md` | Canvas, HUD, health bars, TextMeshPro, layout |
|
| 17 |
+
| `unity_audio.md` | AudioSource, 3D audio, AudioMixer, AudioManager pattern |
|
| 18 |
+
| `open_world_design.md` | Procedural generation, LOD, streaming, day/night, save |
|
| 19 |
+
|
| 20 |
+
## How the agent uses these
|
| 21 |
+
|
| 22 |
+
The orchestrator can embed the contents of these files as context for the
|
| 23 |
+
LLM. The system prompt instructs the LLM to "consult the knowledge entries
|
| 24 |
+
for [topic] when generating code that touches [topic]."
|
| 25 |
+
|
| 26 |
+
For example, when generating a third-person controller the LLM should pull
|
| 27 |
+
`unity_fundamentals.md` (for the lifecycle) and `unity_input_system.md`
|
| 28 |
+
(for the mouse-look recipe).
|
| 29 |
+
|
| 30 |
+
## Adding entries
|
| 31 |
+
|
| 32 |
+
Create a new `.md` file in this directory and update the table above. The
|
| 33 |
+
file name (without `.md`) becomes the entry's slug.
|
unity_agent/knowledge/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Knowledge base loader.
|
| 2 |
+
|
| 3 |
+
Provides a simple API for loading the markdown knowledge entries that ship
|
| 4 |
+
with the package. Entries are stored as ``.md`` files in
|
| 5 |
+
``unity_agent/knowledge/entries/``.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from importlib import resources
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Dict, List, Optional
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def entries_dir() -> Path:
|
| 16 |
+
"""Return the absolute path to the knowledge entries directory."""
|
| 17 |
+
return Path(__file__).resolve().parent / "entries"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def list_entries() -> List[str]:
|
| 21 |
+
"""Return a list of knowledge entry slugs (file names without .md)."""
|
| 22 |
+
d = entries_dir()
|
| 23 |
+
if not d.exists():
|
| 24 |
+
return []
|
| 25 |
+
return sorted(p.stem for p in d.glob("*.md"))
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def load_entry(slug: str) -> Optional[str]:
|
| 29 |
+
"""Return the markdown body of a knowledge entry, or None if missing."""
|
| 30 |
+
p = entries_dir() / f"{slug}.md"
|
| 31 |
+
if not p.exists():
|
| 32 |
+
return None
|
| 33 |
+
return p.read_text(encoding="utf-8")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def load_all() -> Dict[str, str]:
|
| 37 |
+
"""Return a dict mapping slug -> markdown body for every entry."""
|
| 38 |
+
return {slug: load_entry(slug) for slug in list_entries()}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
__all__ = ["entries_dir", "list_entries", "load_entry", "load_all"]
|
unity_agent/knowledge/entries/open_world_design.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Open-World Game Design
|
| 2 |
+
|
| 3 |
+
Designing an open world is fundamentally a trade-off between **density of
|
| 4 |
+
content** and **memory/CPU budget**. This document collects the patterns the
|
| 5 |
+
agent uses when generating the open-world city example.
|
| 6 |
+
|
| 7 |
+
## Procedural generation
|
| 8 |
+
|
| 9 |
+
### Grid + jitter
|
| 10 |
+
|
| 11 |
+
The simplest viable city layout is a grid of blocks where each block holds
|
| 12 |
+
one or more buildings whose position is jittered slightly to break the
|
| 13 |
+
symmetry:
|
| 14 |
+
|
| 15 |
+
```csharp
|
| 16 |
+
for (int x = 0; x < gridX; x++)
|
| 17 |
+
for (int z = 0; z < gridZ; z++)
|
| 18 |
+
{
|
| 19 |
+
Vector3 block = origin + new Vector3(x * blockSpacing, 0f, z * blockSpacing);
|
| 20 |
+
SpawnBuilding(block + Random.insideUnitSphere * 4f);
|
| 21 |
+
}
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
The generated `CityGenerator.cs` follows this pattern. For more variety,
|
| 25 |
+
swap the grid for:
|
| 26 |
+
|
| 27 |
+
* **Voronoi street networks** (Delaunay triangulation, then prune).
|
| 28 |
+
* **L-system road growth** (recursive rewrite rules -- great for organic
|
| 29 |
+
cities).
|
| 30 |
+
* **Wave function collapse** -- tile-based, but expensive to author.
|
| 31 |
+
|
| 32 |
+
### Building composition
|
| 33 |
+
|
| 34 |
+
A building is rarely a single mesh. The generated `BuildingGenerator.cs`
|
| 35 |
+
composes three primitives:
|
| 36 |
+
|
| 37 |
+
1. A wall cube with a Standard material (concrete/glass color).
|
| 38 |
+
2. A slightly smaller cube with an emissive **window material** so windows
|
| 39 |
+
glow at night.
|
| 40 |
+
3. A `BoxCollider` for collision.
|
| 41 |
+
|
| 42 |
+
For more variety:
|
| 43 |
+
|
| 44 |
+
* Vary the footprint (square vs rectangular).
|
| 45 |
+
* Add a flat roof cap with a different material.
|
| 46 |
+
* Add a parapet along the top.
|
| 47 |
+
* Randomly choose between glass-tower, brick-mid-rise, and concrete-low-rise
|
| 48 |
+
palettes.
|
| 49 |
+
|
| 50 |
+
### Runtime vs baked
|
| 51 |
+
|
| 52 |
+
The agent generates everything **at runtime in `Start()`** because:
|
| 53 |
+
|
| 54 |
+
* The Unity project opens with an empty scene and the user immediately
|
| 55 |
+
sees content after pressing Play.
|
| 56 |
+
* No need to ship `.unity` scenes with hundreds of GameObjects.
|
| 57 |
+
* Easy to randomize: just change the seed.
|
| 58 |
+
|
| 59 |
+
The downside is the first-frame hitch on large worlds. Mitigations:
|
| 60 |
+
|
| 61 |
+
* Spread generation across frames using a coroutine that yields after
|
| 62 |
+
every N buildings.
|
| 63 |
+
* Use `Object.Instantiate` from a small set of prefabs instead of building
|
| 64 |
+
primitives from scratch each time.
|
| 65 |
+
* Use `Graphics.DrawMeshInstanced` for buildings once you do not need
|
| 66 |
+
per-building colliders.
|
| 67 |
+
|
| 68 |
+
## Level of Detail (LOD)
|
| 69 |
+
|
| 70 |
+
For an open world you must use LOD. Without it, the GPU processes every
|
| 71 |
+
window pane on every building no matter how far away it is.
|
| 72 |
+
|
| 73 |
+
| LOD | Screen size | Poly count |
|
| 74 |
+
|-----|-------------|-----------------------------|
|
| 75 |
+
| 0 | 60%+ | Full mesh with windows |
|
| 76 |
+
| 1 | 25-60% | Simplified mesh, no windows |
|
| 77 |
+
| 2 | 5-25% | Single box per building |
|
| 78 |
+
| Culled | <5% | Not rendered at all |
|
| 79 |
+
|
| 80 |
+
Add a `LODGroup` to each building and assign LOD meshes. For procedural
|
| 81 |
+
buildings you can build the LOD meshes in code by reducing the window
|
| 82 |
+
count and merging meshes.
|
| 83 |
+
|
| 84 |
+
## Streaming and chunking
|
| 85 |
+
|
| 86 |
+
For worlds larger than ~1km x 1km you must chunk:
|
| 87 |
+
|
| 88 |
+
* Divide the world into a grid of chunks (e.g. 200m x 200m).
|
| 89 |
+
* Load chunks near the player; unload chunks far away.
|
| 90 |
+
* Use `Addressables` or `SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive)`
|
| 91 |
+
to load chunk scenes.
|
| 92 |
+
|
| 93 |
+
The generated example does NOT chunk -- it builds everything in one shot,
|
| 94 |
+
which is fine for an 8x8 block city. Chunking becomes mandatory around
|
| 95 |
+
16x16 blocks.
|
| 96 |
+
|
| 97 |
+
## Fog and far plane
|
| 98 |
+
|
| 99 |
+
Always set `RenderSettings.fog` for open worlds. It hides:
|
| 100 |
+
|
| 101 |
+
* Distant popping when LOD changes happen.
|
| 102 |
+
* The far clip plane edge.
|
| 103 |
+
|
| 104 |
+
A linear fog from 100m to 800m is a good default for a city.
|
| 105 |
+
|
| 106 |
+
## Day/Night cycle
|
| 107 |
+
|
| 108 |
+
The generated `DayNightCycle.cs` rotates a directional light around the X
|
| 109 |
+
axis to simulate the sun:
|
| 110 |
+
|
| 111 |
+
```csharp
|
| 112 |
+
float sunAngle = (_timeOfDay - 0.25f) * 360f; // 0.25 = noon
|
| 113 |
+
sun.transform.rotation = Quaternion.Euler(sunAngle, -30f, 0f);
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
* Keep the day length short (60-180 s) for demo purposes; 24 min real time
|
| 117 |
+
per day for actual gameplay.
|
| 118 |
+
* Lerp ambient light and skybox color between day/night presets so the
|
| 119 |
+
world does not suddenly flip black.
|
| 120 |
+
* Turn street lights on/off based on a sun-intensity threshold.
|
| 121 |
+
|
| 122 |
+
## Player traversal
|
| 123 |
+
|
| 124 |
+
Open worlds need fast traversal. Options:
|
| 125 |
+
|
| 126 |
+
| Method | Speed (m/s) | Use case |
|
| 127 |
+
|-----------------------|-------------|---------------------------------------|
|
| 128 |
+
| Walking | 3-5 | On-foot exploration |
|
| 129 |
+
| Sprinting | 7-10 | Short bursts |
|
| 130 |
+
| Car (arcade) | 15-30 | The generated `VehicleController.cs` |
|
| 131 |
+
| Horse / mount | 8-12 | Fantasy open world |
|
| 132 |
+
| Fast travel | instant | Pre-unlocked locations |
|
| 133 |
+
|
| 134 |
+
The generated project ships both a `PlayerController` (on-foot) and a
|
| 135 |
+
`VehicleController` (car) so the user can switch between them.
|
| 136 |
+
|
| 137 |
+
## NPC traffic
|
| 138 |
+
|
| 139 |
+
For an open-world city, AI traffic should be **lightweight**:
|
| 140 |
+
|
| 141 |
+
* Use `NavMeshAgent` with a low `acceleration` for cars so they look
|
| 142 |
+
smooth, not jerky.
|
| 143 |
+
* Pool the agents -- never `Instantiate` a new car every time one drives
|
| 144 |
+
off-screen.
|
| 145 |
+
* Despawn agents when they are > 100m from the player.
|
| 146 |
+
|
| 147 |
+
The generated `NPCController.cs` covers pedestrians; for cars you would
|
| 148 |
+
extend it with lane-following logic.
|
| 149 |
+
|
| 150 |
+
## Save systems
|
| 151 |
+
|
| 152 |
+
Open worlds need persistent saves. The generated `SaveSystem.cs` writes a
|
| 153 |
+
single JSON file to `Application.persistentDataPath`:
|
| 154 |
+
|
| 155 |
+
```csharp
|
| 156 |
+
[Serializable]
|
| 157 |
+
public class GameSave
|
| 158 |
+
{
|
| 159 |
+
public Vector3 playerPosition;
|
| 160 |
+
public int score;
|
| 161 |
+
public float timeOfDay;
|
| 162 |
+
public List<string> completedQuests;
|
| 163 |
+
}
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
For larger worlds, split saves into:
|
| 167 |
+
|
| 168 |
+
* Player profile (small, frequent).
|
| 169 |
+
* World state (large, infrequent -- chunk ownership, building destruction,
|
| 170 |
+
NPC schedules).
|
| 171 |
+
|
| 172 |
+
## Common pitfalls
|
| 173 |
+
|
| 174 |
+
* **Floating point precision:** Unity uses single-precision floats. Beyond
|
| 175 |
+
~5km from the origin, physics and rendering jitter. Use a **floating
|
| 176 |
+
origin** that re-centres the world on the player every few hundred metres.
|
| 177 |
+
* **Shadow distance:** the default 150m shadow distance is too short for an
|
| 178 |
+
open world. Raise to 300-500m for desktop; lower for mobile.
|
| 179 |
+
* **Lighting rebuild:** make sure all your static city geometry is marked
|
| 180 |
+
**Navigation Static** AND **Lightmap Static** before baking. For runtime-
|
| 181 |
+
generated cities you cannot bake -- rely on realtime lighting and the
|
| 182 |
+
directional sun.
|
| 183 |
+
* **Collider spam:** 50 buildings x 6 faces each = 300 colliders. Mark
|
| 184 |
+
small decoration meshes as `isTrigger = false` and use a single
|
| 185 |
+
`BoxCollider` per building.
|
unity_agent/knowledge/entries/unity_audio.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Unity Audio
|
| 2 |
+
|
| 3 |
+
Unity plays audio through `AudioSource` components that reference an
|
| 4 |
+
`AudioClip` asset. A single `AudioListener` in the scene is required
|
| 5 |
+
(usually on the Main Camera).
|
| 6 |
+
|
| 7 |
+
## AudioClip
|
| 8 |
+
|
| 9 |
+
An `AudioClip` is the audio data. Supported formats: WAV (uncompressed,
|
| 10 |
+
fastest), MP3/Ogg Vorbis (compressed, smaller). Unity imports them and
|
| 11 |
+
lets you choose compression per-platform in the import settings.
|
| 12 |
+
|
| 13 |
+
## AudioSource
|
| 14 |
+
|
| 15 |
+
```csharp
|
| 16 |
+
var src = gameObject.AddComponent<AudioSource>();
|
| 17 |
+
src.clip = jumpClip;
|
| 18 |
+
src.volume = 0.7f;
|
| 19 |
+
src.pitch = 1.0f;
|
| 20 |
+
src.loop = false;
|
| 21 |
+
src.playOnAwake = false;
|
| 22 |
+
src.spatialBlend = 0.0f; // 0=2D, 1=3D
|
| 23 |
+
src.Play();
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
Important properties:
|
| 27 |
+
|
| 28 |
+
| Property | Meaning |
|
| 29 |
+
|-----------------|-----------------------------------------------------------|
|
| 30 |
+
| `clip` | The AudioClip to play |
|
| 31 |
+
| `volume` | 0..1 |
|
| 32 |
+
| `pitch` | Default 1; >1 = higher pitch, <1 = lower |
|
| 33 |
+
| `loop` | Repeat when the clip ends |
|
| 34 |
+
| `playOnAwake` | Play automatically when the AudioSource is enabled |
|
| 35 |
+
| `spatialBlend` | 0=fully 2D, 1=fully 3D (position-based) |
|
| 36 |
+
| `minDistance` | Within this distance the sound is at full volume (3D) |
|
| 37 |
+
| `maxDistance` | Beyond this distance the sound is inaudible (3D) |
|
| 38 |
+
| `rolloffMode` | Logarithmic, Linear, or custom curve (3D falloff) |
|
| 39 |
+
|
| 40 |
+
## 3D audio
|
| 41 |
+
|
| 42 |
+
For positional sounds (footsteps, gunfire, engines), set
|
| 43 |
+
`spatialBlend = 1.0f`. The volume then depends on the distance between the
|
| 44 |
+
AudioSource and the AudioListener.
|
| 45 |
+
|
| 46 |
+
```csharp
|
| 47 |
+
src.spatialBlend = 1.0f;
|
| 48 |
+
src.rolloffMode = AudioRolloffMode.Logarithmic;
|
| 49 |
+
src.minDistance = 1f;
|
| 50 |
+
src.maxDistance = 50f;
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
> Default `maxDistance` is 500 -- way too large for most gameplay. Lower
|
| 54 |
+
> it to 30-50 for SFX.
|
| 55 |
+
|
| 56 |
+
## One-shot SFX
|
| 57 |
+
|
| 58 |
+
For short sound effects (pickups, gunshots, impacts) use `PlayOneShot`:
|
| 59 |
+
|
| 60 |
+
```csharp
|
| 61 |
+
audioSource.PlayOneShot(clip, volumeScale);
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
`PlayOneShot` can layer multiple clips on the same AudioSource without
|
| 65 |
+
cutting each other off.
|
| 66 |
+
|
| 67 |
+
## AudioMixer
|
| 68 |
+
|
| 69 |
+
For volume control and grouping, create an **AudioMixer** asset
|
| 70 |
+
(`Assets > Create > Audio Mixer`). Expose parameters and control them at
|
| 71 |
+
runtime:
|
| 72 |
+
|
| 73 |
+
```csharp
|
| 74 |
+
[SerializeField] private AudioMixer mixer;
|
| 75 |
+
|
| 76 |
+
public void SetMasterVolume(float dB) => mixer.SetFloat("MasterVolume", dB);
|
| 77 |
+
public void SetMusicVolume(float dB) => mixer.SetFloat("MusicVolume", dB);
|
| 78 |
+
public void SetSfxVolume(float dB) => mixer.SetFloat("SfxVolume", dB);
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
Convert linear 0..1 to dB:
|
| 82 |
+
|
| 83 |
+
```csharp
|
| 84 |
+
public static float LinearToDB(float linear) =>
|
| 85 |
+
linear <= 0.0001f ? -80f : Mathf.Log10(linear) * 20f;
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
## The AudioManager pattern
|
| 89 |
+
|
| 90 |
+
The generated `AudioManager.cs` is a singleton with two channels
|
| 91 |
+
(`sfxSource`, `musicSource`). It exposes:
|
| 92 |
+
|
| 93 |
+
```csharp
|
| 94 |
+
AudioManager.Instance.PlaySfx(pickupClip, 1.0f);
|
| 95 |
+
AudioManager.Instance.PlayMusic(menuMusic);
|
| 96 |
+
AudioManager.Instance.StopMusic();
|
| 97 |
+
AudioManager.Instance.ApplyVolumes();
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
`DontDestroyOnLoad` keeps the manager alive across scene loads so music
|
| 101 |
+
does not restart.
|
| 102 |
+
|
| 103 |
+
## AudioListener
|
| 104 |
+
|
| 105 |
+
* Exactly one `AudioListener` per scene. The Main Camera has one by
|
| 106 |
+
default; remove it if you add another.
|
| 107 |
+
* The listener's transform defines "where the player's ears are" for 3D
|
| 108 |
+
audio panning.
|
| 109 |
+
|
| 110 |
+
## Performance
|
| 111 |
+
|
| 112 |
+
* Pool AudioSources for sounds that play many times per second (machine
|
| 113 |
+
guns, rain). Reusing one AudioSource and calling `PlayOneShot` is fine
|
| 114 |
+
up to ~20 simultaneous clips.
|
| 115 |
+
* Use compressed formats (Vorbis) for music; uncompressed (WAV) for
|
| 116 |
+
frequent short SFX.
|
| 117 |
+
* Set `AudioClip.preloadAudioData = true` for short clips and `false` for
|
| 118 |
+
music that should stream from disk.
|
| 119 |
+
* Lower `maxDistance` aggressively; large falloff radii cost CPU on 3D
|
| 120 |
+
sounds.
|
unity_agent/knowledge/entries/unity_fundamentals.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Unity Fundamentals
|
| 2 |
+
|
| 3 |
+
## MonoBehaviour Lifecycle
|
| 4 |
+
|
| 5 |
+
Every gameplay script in Unity derives from `MonoBehaviour`. The engine calls
|
| 6 |
+
a fixed sequence of messages; understanding the order is the difference
|
| 7 |
+
between code that runs smoothly and code that stutters or races.
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
Awake() -> OnEnable() -> Start()
|
| 11 |
+
|
|
| 12 |
+
v
|
| 13 |
+
FixedUpdate() (physics, fixed timestep) --+
|
| 14 |
+
|
|
| 15 |
+
Update() (input, gameplay) --+
|
| 16 |
+
|
|
| 17 |
+
LateUpdate() (camera follow, post-sim) --+
|
| 18 |
+
|
|
| 19 |
+
OnDisable() -> OnDestroy()
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
| Message | When it runs | Typical use |
|
| 23 |
+
|----------------|-----------------------------------------------|-----------------------------------------------|
|
| 24 |
+
| `Awake` | Once, when the script instance is loaded | Cache components, set up singletons |
|
| 25 |
+
| `OnEnable` | Every time the component is enabled | Subscribe to events |
|
| 26 |
+
| `Start` | Once, before the first Update | Cross-object references (other objects' Awake have run) |
|
| 27 |
+
| `FixedUpdate` | Fixed timestep (default 0.02 s = 50 Hz) | Rigidbody physics forces |
|
| 28 |
+
| `Update` | Every rendered frame | Input polling, gameplay logic |
|
| 29 |
+
| `LateUpdate` | After all Update calls have finished | Camera follow, post-processing |
|
| 30 |
+
| `OnDisable` | When the component is disabled | Unsubscribe from events |
|
| 31 |
+
| `OnDestroy` | When the GameObject is destroyed | Cleanup native resources |
|
| 32 |
+
|
| 33 |
+
> **Gotcha:** `Camera.main` searches the scene by tag every call in 2022.3.
|
| 34 |
+
> Cache it in `Awake` if you call it more than once per frame.
|
| 35 |
+
|
| 36 |
+
## GameObjects and Components
|
| 37 |
+
|
| 38 |
+
A `GameObject` is an empty container. Behaviour is added through
|
| 39 |
+
**components**:
|
| 40 |
+
|
| 41 |
+
```csharp
|
| 42 |
+
var go = new GameObject("Enemy");
|
| 43 |
+
go.transform.position = Vector3.zero;
|
| 44 |
+
go.AddComponent<Rigidbody>();
|
| 45 |
+
go.AddComponent<CapsuleCollider>();
|
| 46 |
+
var ai = go.AddComponent<NPCController>();
|
| 47 |
+
ai.player = GameObject.FindGameObjectWithTag("Player").transform;
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
* Every GameObject always has a `Transform` (or `RectTransform` under a
|
| 51 |
+
Canvas). You cannot remove it.
|
| 52 |
+
* Use `GetComponent<T>()` / `GetComponentInChildren<T>()` /
|
| 53 |
+
`GetComponentInParent<T>()` to reach related components. Cache the result
|
| 54 |
+
in `Awake` instead of calling it every frame.
|
| 55 |
+
* `RequireComponent(typeof(T))` makes Unity auto-add the required component
|
| 56 |
+
when the script is added in the inspector.
|
| 57 |
+
|
| 58 |
+
## The Component pattern in practice
|
| 59 |
+
|
| 60 |
+
```csharp
|
| 61 |
+
[RequireComponent(typeof(Rigidbody))]
|
| 62 |
+
public class PlayerController : MonoBehaviour
|
| 63 |
+
{
|
| 64 |
+
private Rigidbody _rb;
|
| 65 |
+
|
| 66 |
+
private void Awake()
|
| 67 |
+
{
|
| 68 |
+
_rb = GetComponent<Rigidbody>(); // safe: RequireComponent guarantees it
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
## Coroutines
|
| 74 |
+
|
| 75 |
+
Coroutines let you write asynchronous-looking code that runs across multiple
|
| 76 |
+
frames:
|
| 77 |
+
|
| 78 |
+
```csharp
|
| 79 |
+
private IEnumerator RespawnAfterDelay(float seconds)
|
| 80 |
+
{
|
| 81 |
+
yield return new WaitForSeconds(seconds);
|
| 82 |
+
transform.position = _spawnPoint;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
private void OnDeath() => StartCoroutine(RespawnAfterDelay(2f));
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
* `yield return null;` waits one frame.
|
| 89 |
+
* `yield return new WaitForFixedUpdate();` waits until the next physics step.
|
| 90 |
+
* `yield return new WaitUntil(() => _isGrounded);` waits for a condition.
|
| 91 |
+
|
| 92 |
+
## Inspector best practices
|
| 93 |
+
|
| 94 |
+
* Use `[Header]`, `[Tooltip]`, `[Range]`, `[SerializeField]` to keep the
|
| 95 |
+
inspector tidy.
|
| 96 |
+
* Prefer `private` + `[SerializeField]` over `public` for fields that should
|
| 97 |
+
not be touched by other scripts but should be editable in the inspector.
|
| 98 |
+
* Use `[System.Serializable]` on structs/classes to make them expandable in
|
| 99 |
+
the inspector.
|
unity_agent/knowledge/entries/unity_input_system.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Unity Input System
|
| 2 |
+
|
| 3 |
+
Unity offers two input systems:
|
| 4 |
+
|
| 5 |
+
1. **Input Manager (legacy)** -- `Input.GetAxis`, `Input.GetKeyDown`. The
|
| 6 |
+
default in 2022.3 LTS. Used by every generated script in this project.
|
| 7 |
+
2. **Input System package** -- the new event-driven API. Available via the
|
| 8 |
+
`com.unity.inputsystem` package.
|
| 9 |
+
|
| 10 |
+
## Input Manager (legacy)
|
| 11 |
+
|
| 12 |
+
### Axes
|
| 13 |
+
|
| 14 |
+
The legacy system reads virtual axes defined in
|
| 15 |
+
`Edit > Project Settings > Input Manager`. The defaults include:
|
| 16 |
+
|
| 17 |
+
| Axis name | Default keys |
|
| 18 |
+
|---------------|---------------------------------|
|
| 19 |
+
| `Horizontal` | A/D, Arrow Left/Right |
|
| 20 |
+
| `Vertical` | W/S, Arrow Up/Down |
|
| 21 |
+
| `Mouse X` | Mouse delta X |
|
| 22 |
+
| `Mouse Y` | Mouse delta Y |
|
| 23 |
+
| `Jump` | Space |
|
| 24 |
+
| `Fire1` | Left Ctrl / Mouse 0 |
|
| 25 |
+
| `Fire2` | Right Alt / Mouse 1 |
|
| 26 |
+
| `Fire3` | Left Shift / Mouse 2 |
|
| 27 |
+
|
| 28 |
+
### Reading axes
|
| 29 |
+
|
| 30 |
+
```csharp
|
| 31 |
+
float x = Input.GetAxisRaw("Horizontal"); // -1, 0, +1 (no smoothing)
|
| 32 |
+
float y = Input.GetAxis("Vertical"); // smoothed, -1..+1
|
| 33 |
+
Vector2 move = new Vector2(x, y);
|
| 34 |
+
move = Vector2.ClampMagnitude(move, 1f); // prevent diagonal speed boost
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
* Use `GetAxisRaw` for responsive arcade movement; `GetAxis` for smooth
|
| 38 |
+
camera-style axes.
|
| 39 |
+
* Always clamp the magnitude of a 2-axis vector -- otherwise diagonal
|
| 40 |
+
movement is ~1.41x faster than cardinal.
|
| 41 |
+
|
| 42 |
+
### Keys and buttons
|
| 43 |
+
|
| 44 |
+
```csharp
|
| 45 |
+
if (Input.GetKeyDown(KeyCode.Space)) Jump();
|
| 46 |
+
if (Input.GetKey(KeyCode.LeftShift)) Sprint();
|
| 47 |
+
if (Input.GetKeyUp(KeyCode.E)) Interact();
|
| 48 |
+
if (Input.GetMouseButtonDown(0)) Fire();
|
| 49 |
+
if (Input.GetMouseButton(0)) FireContinuous();
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
| Method | Fires when |
|
| 53 |
+
|-----------------------|-------------------------------------------|
|
| 54 |
+
| `GetKey` | Every frame the key is held |
|
| 55 |
+
| `GetKeyDown` | Only on the frame the key was pressed |
|
| 56 |
+
| `GetKeyUp` | Only on the frame the key was released |
|
| 57 |
+
| `GetMouseButton` | Every frame the button is held |
|
| 58 |
+
| `GetMouseButtonDown` | Only on the frame the button went down |
|
| 59 |
+
|
| 60 |
+
### Mouse position and delta
|
| 61 |
+
|
| 62 |
+
```csharp
|
| 63 |
+
Vector3 mousePos = Input.mousePosition; // screen-space pixels
|
| 64 |
+
float dx = Input.GetAxis("Mouse X"); // smoothed delta
|
| 65 |
+
float rawDx = Input.GetAxisRaw("Mouse X");
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
To lock the cursor for first-person look:
|
| 69 |
+
|
| 70 |
+
```csharp
|
| 71 |
+
Cursor.lockState = CursorLockMode.Locked; // hide + center
|
| 72 |
+
Cursor.visible = false;
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## Mouse-look recipe (first-person)
|
| 76 |
+
|
| 77 |
+
```csharp
|
| 78 |
+
private float _pitch;
|
| 79 |
+
|
| 80 |
+
private void Update()
|
| 81 |
+
{
|
| 82 |
+
float mx = Input.GetAxisRaw("Mouse X") * sensitivity;
|
| 83 |
+
float my = Input.GetAxisRaw("Mouse Y") * sensitivity;
|
| 84 |
+
|
| 85 |
+
transform.Rotate(Vector3.up * mx);
|
| 86 |
+
_pitch = Mathf.Clamp(_pitch - my, -85f, 85f);
|
| 87 |
+
cameraTransform.localRotation = Quaternion.Euler(_pitch, 0f, 0f);
|
| 88 |
+
}
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
## Mouse-look recipe (third-person orbit)
|
| 92 |
+
|
| 93 |
+
For a third-person camera we accumulate a **yaw** and **pitch** and build a
|
| 94 |
+
rotation each frame:
|
| 95 |
+
|
| 96 |
+
```csharp
|
| 97 |
+
_yaw += Input.GetAxis("Mouse X") * sensitivity;
|
| 98 |
+
_pitch -= Input.GetAxis("Mouse Y") * sensitivity;
|
| 99 |
+
_pitch = Mathf.Clamp(_pitch, -30f, 75f);
|
| 100 |
+
Quaternion rotation = Quaternion.Euler(_pitch, _yaw, 0f);
|
| 101 |
+
Vector3 position = pivot + rotation * new Vector3(0f, height, -distance);
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
The `ThirdPersonCamera.cs` generator follows exactly this pattern.
|
| 105 |
+
|
| 106 |
+
## Input System package (new)
|
| 107 |
+
|
| 108 |
+
When the project has the Input System package active, the legacy `Input`
|
| 109 |
+
class will throw or return zeros unless `Project Settings > Player > Active Input Handling = Both`.
|
| 110 |
+
|
| 111 |
+
The new API is event-based:
|
| 112 |
+
|
| 113 |
+
```csharp
|
| 114 |
+
[SerializeField] private InputActionAsset actions;
|
| 115 |
+
private InputAction _moveAction;
|
| 116 |
+
|
| 117 |
+
private void OnEnable()
|
| 118 |
+
{
|
| 119 |
+
_moveAction = actions.FindAction("Move");
|
| 120 |
+
_moveAction.performed += OnMove;
|
| 121 |
+
_moveAction.Enable();
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
private void OnMove(InputAction.CallbackContext ctx) => _moveInput = ctx.ReadValue<Vector2>();
|
| 125 |
+
private void OnDisable() => _moveAction.Disable();
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
For the generated project we keep Active Input Handling on **Both** so the
|
| 129 |
+
legacy `Input` calls in the generated scripts keep working.
|
| 130 |
+
|
| 131 |
+
## Touch and mobile
|
| 132 |
+
|
| 133 |
+
For mobile, read `Input.touches` (legacy) or the new Input System's
|
| 134 |
+
`Pointer`/`Touchscreen`:
|
| 135 |
+
|
| 136 |
+
```csharp
|
| 137 |
+
foreach (Touch t in Input.touches)
|
| 138 |
+
{
|
| 139 |
+
if (t.phase == TouchPhase.Began) HandleTap(t.position);
|
| 140 |
+
}
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
The generated scripts are desktop-first; you would add a virtual joystick
|
| 144 |
+
overlay for mobile ports.
|
| 145 |
+
|
| 146 |
+
## Common pitfalls
|
| 147 |
+
|
| 148 |
+
* Reading `Input` in `FixedUpdate` -- the input system updates once per
|
| 149 |
+
frame, but `FixedUpdate` may run 0..N times per frame, so you will drop
|
| 150 |
+
or duplicate inputs.
|
| 151 |
+
* Using `Input.GetAxis` for physics forces -- the smoothed value can cause
|
| 152 |
+
floaty controls. Prefer `GetAxisRaw` and apply your own smoothing.
|
| 153 |
+
* Forgetting to lock the cursor in an FPS leads to the camera spinning
|
| 154 |
+
when the cursor leaves the game window.
|