Unity Deep Engineering Guide
Unity is a component-based game engine built around GameObjects, MonoBehaviours, and a C# scripting layer. Modern Unity adds Data-Oriented Technology Stack (DOTS/E.C.S.) and two Scriptable Render Pipelines (URP/HDRP). This guide covers how a lead engineer thinks about Unity: architecture, C# performance, data-oriented design, rendering, physics, multiplayer, and profiling.
1. Engine Architecture
Unity Player/Editor
Application (UnityEngine)
SceneManager -> load/unload Scenes (additive)
GameObject (identity + transform)
Component (MonoBehaviour, Renderer, Collider, Animator...)
PlayerLoop: (fixed update order, engine-driven)
ScriptableRunLoops -> custom systems injected
Module systems: Physics (PhysX/Box2D), Audio (FMOD), UI (UGUI/UI Toolkit),
Rendering (URP/HDRP/Built-in), Input (new Input System)
1.1 The Player Loop & Update Order
Within one frame Unity calls systems in a fixed order:
void Update() // per rendered frame
void FixedUpdate() // fixed timestep: physics, network ticks (default 0.02s)
void LateUpdate() // camera follow, post-movement
Rules:
- Never move Rigidbody in Update(); move via
FixedUpdateor userb.velocityonly in FixedUpdate. Changingtransformin Update fights the physics solver. - Camera interpolation in LateUpdate.
- Scene load is synchronous unless using
Addressables/SceneManager.LoadSceneAsync.
1.2 Multiple Scenes (Additive Loading)
- Split levels into persistent + gameplay scenes; stream with
LoadSceneAsync+AllowSceneActivation = falseto keep the load from blocking. - Use
SceneManager.MergeScenesor DontDestroyOnLoad root object.
2. GameObject & Component Model
public class Health : MonoBehaviour {
public int maxHp = 100;
int current;
void OnEnable() { /* add self to manager lists */ }
void OnDisable() { /* remove self */ }
}
2.1 GetComponent Performance
GetComponent<T>() per frame per object is a hot-path mistake:
- Cache component references in
Awake()once. - Prefer
TryGetComponent. GetComponentsInChildrenonly at load or on structural change.- Hot update loops use managers (single static list of registered components), not per-object
Find.
2.2 FindObjectOfType / GameObject.Find
These are O(n) with allocations. They MUST not run on the per-frame path. For singletons, register in Awake, destroy in OnDestroy:
public static GameManager I; // global accessor
void Awake() { I = this; }
void OnDestroy() { if (I == this) I = null; }
3. The DOTS / ECS Stack
Unity DOTS replaces MonoBehaviours with archetype-based ECS:
// Component - plain struct, no MonoBehaviour
public struct Velocity : IComponentData { public float3 Value; }
// System
[BurstCompile]
partial struct MoveSystem : ISystem {
[BurstCompile]
public void OnUpdate(ref SystemState state) {
foreach (var (transform, v) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<Velocity>>())
{
transform.ValueRW.Position += v.ValueRO.Value * SystemAPI.Time.DeltaTime;
}
}
}
- Entities stored in archetypes (SoA per component) → cache-friendly.
Burst+Jobscompiles to native SIMD-optimized code.- SubScene baking: author GameObject scenes → baked entities at build.
When to use DOTS vs classic GameObjects
| Need | Path |
|---|---|
| 1000s of agents, bullets, particles | DOTS/ECS + Burst + Jobs |
| Rapid prototyping / small games | Classic GameObject/MonoBehaviours |
| UI, gameplay narrative, inventory | Classic (UGUI) |
| Massive open world streaming | DOTS SubScene streaming |
4. Rendering Pipelines
4.1 Choose the Pipeline
| Pipeline | Use |
|---|---|
| Built-in | Legacy, quick prototypes, simple mobile games |
| URP | Default for most new projects (PC+mobile), SRP Batcher optimal |
| HDRP | Film-grade lighting: deferred, ray tracing, volumetrics, high-end PC/console |
ScriptableRenderer (custom) |
You own pass ordering; use with RenderGraph |
4.2 URP vs HDRP in practice
- URP: forward rendering, 2D lights, SRP batcher, simplified.
- HDRP: deferred, clustered lighting, volumetric fog, SSR, RT reflections (DXR).
- Both are
ScriptableRenderPipeline; they run in theRendering.Updateplayer-loop phase.
4.3 Draw Call Reduction
- Enable SRP Batcher (URP/HDRP property
UseSRPBatcher) — batches dynamic shadows+lit objects with compatible materials. - GPU instancing:
Graphics.DrawMeshInstancedfor repeated meshes (grass, crowds, particles). - Static batching: mark static → combined at build.
- Addressables + SDF 2D: not draw-call related, but stream textures to control bandwidth.
- Minimize material variants (
KeywordEnum,MaterialPropertyBlockfor per-object color). - Bake lighting (Lightmapping) instead of dynamic.
4.4 Shader Lab & Shader Graph
- Shader Graph (node-based) is preferred for artists; expose exposed properties only.
- Hand-written shaders: write in HLSL; SRP uses
HLSLPROGRAMblocks and#pragma target3.0+. - Watch out: Unity editor implicit
#pragma multi_compile _ _ALPHATEST_ON; use#pragma shader_feature_localto avoid variants.
5. Physics
- Unity uses PhysX (3D) and Box2D (2D).
- Fixed timestep at 0.02s (50Hz); fast small objects tunnel → use
Physics.Burst/Physics.Solveror a dedicated CCD sensor. - Sleeping: overlapping static triggers should disable colliders (
isTrigger = true) to avoid touches every frame. - Layers & matrix matter:
Physics.IgnoreLayerCollision(playerLayer, enemyLayer, true)reduces contact generation. Physics.SyncTransforms()forces syncing; avoid calling per-frame.
6. Animation & Timing
- Animator state machine with transitions; avoid heavy tree layers per object.
Time.deltaTimemay be 0 on pause; useTime.unscaledDeltaTimefor UI.- Animation events are editor-time; use
OnAnimationEventorAnimationEventbinding. - For 100s of animating characters prefer playables or
AnimationUpdateModefixed with simple lerps.
7. UI (UGUI / UI Toolkit / IMGUI)
- UGUI (Canvas): fine for HUD; keep panels/Canvas to 1-2, enable
Vertex Bufferreuse, disable raycast on static. - UI Toolkit (new): UXML/USS, better for menus and editor tooling; runtime since 2023.
- IMGUI: editor windows and debugging only, not runtime UI.
- Rule: no layout rebuild per frame; cache
RectTransforms, minifyGraphicRaycasterblockers.
8. Asset Pipeline
8.1 Addressables
Addressables = load by key, async, memory-managed:
Addressables.LoadAssetAsync<GameObject>("enemies/wolf").Completed += h => {
var go = Instantiate(h.Result);
go.AddComponent<DeferredUnload>();
};
Addressables.Release(handle);
- Use
AssetReferencefields instead of hardcoded strings. - Set
addressablesremote catalog for updates. - Remember:
Instantiate(referencedObject)body must be released too.
8.2 Streaming & Background Loading
- Texture streaming (
Texture2D.streamingEnabled) or Addressables load inOnTriggerEnter. - Audio:
AudioClip.Createstreaming for ambience. - Never
Resources.Loadin Update; useResources.LoadAllonce.
9. Multiplayer (Netcode for GameObjects / Netcode Transport / UnityTransport)
- Authoritative: server owns state; clients send inputs; interpolate snapshot positions.
[ServerRpc]from client to server;[ClientRpc]from server to clients.- Sim tick = fixed timestep; physics simulation server-side only; clients render interpolated states.
- Use
NetworkVariable<T>— beware of per-frameNetworkVariablewrites (delta encode, avoid 60/s floats). - Prediction:
PredictedPlayerpattern with client-sideNetworkTransforminterpolation.
10. Performance Rules (Lead Level)
- Profile first: Unity Profiler + Burst metrics; GPU profiler (RenderDoc, GPUView).
ExecuteInEditModescripts only where needed; keep editor-only work in#if UNITY_EDITOR.- Avoid
transform.positionin a loop (accessing Transform is cheap-ish, but allocations fromFind/GetComponentsInChildrenare not). - Garbage: minimize per-frame allocations; cache arrays; use
NativeArray/NativeListin jobs. - Broke update: use
FixedUpdateonly for physics; game logic inUpdate. - Root motion vs scripted movement: profile
Animatorcost. - Hidden cost:
Camera.maincached; avoid in Update.
10.1 Frame Budget Table Template
| System | Budget (ms) @ 60fps |
|---|---|
| Simulation (Update) | 2.0 |
| Physics (Fixed) | 2.0 |
| Animation | 2.0 |
| AI / NavMesh | 1.5 |
| Rendering (CPU submit) | 3.0 |
| Rendering (GPU) | 8.3–14.0 |
| UI | 1.0 |
| Netcode | 1.0 |
| Misc / Driver | 2.0 |
Total must fit in 16.6ms (or 1000/refresh).
11. Project Structure Convention
Assets/
Scripts/ # namespaces by system
Scenes/
Prefabs/
Addressables/ # addressable groups
Materials/
Textures/
Audio/
_BuildTools/ # editor scripts, CI
.gitignore: Library/ Temp/ Logs/ UserSettings/
Use .asmdef (assembly definition) per folder to speed up compile times and control references.
12. Anti-Patterns
| Anti-pattern | Consequence | Fix |
|---|---|---|
FindObjectOfType in Update |
GC spikes, slow | cache in Awake |
| Rigidbody move in Update | physics jitter | FixedUpdate + rb.Move |
| Per-frame allocations (strings) | GC pauses | string builders, pooling |
| Resources.Load in loop | load spikes | Addressables + cache |
| One Canvas for everything | layout rebuild | split by update frequency |
| Scripts in Editor without serialization | lost state | [SerializedField] |
| Not using .asmdef | long compile | per-system asmdefs |
| Physics without layer matrix | wasted contacts | IgnoreLayerCollision |
13. When to NOT Use Unity
- Native console/PC-only huge world with full control → custom engine + Vulkan (see
skills/game/game-development/vulkan/SKILL.md). - Web "tiny" canvas game → Engines like Phaser or vanilla canvas might be lighter.
- Deep customization of renderer required beyond URP/HDRP budget → engine-level control required.
- For ECS patterns independent of engine, see
skills/game/game-engine/ecs-pattern/SKILL.md.
14. References
skills/game/game-development/unity-csharp/SKILL.md— Unity C# scripting and DOTS internalsskills/game/game-engine/ecs-pattern/references/unity-dots-in-practice.md— DOTS in real projectsskills/game/game-engine/patterns/references/game-loop-and-timestep.md— update timing patternsskills/game/multiplayer-netcode/SKILL.md— netcode patterns & predictionskills/game/game-development/physics-engine/SKILL.md— physics internals used by engines