unity
Use when writing, reviewing or structuring Unity game code in C# for Unity 6 LTS — GameObjects and the Transform hierarchy, the MonoBehaviour lifecycle, prefabs and variants, ScriptableObjects for dat
SKILL.mdskills/unity/SKILL.md
GitHubUnity + C# (Unity 6 LTS)
Idiomatic C# for the current Unity engine — 2D and 3D. Owns engine-side patterns (components, lifecycle, prefabs, scenes, data, input, async, builds). Not Godot (
godot) or Unreal (unreal); stops at the netcode wire (gamedev-multiplayer).
Version contract — read first
Target: Unity 6 LTS, versioned 6000.x. Current LTS at authoring is Unity 6.3 LTS (6000.3)
(supported to Dec 2027); Unity 6.0 LTS (6000.0) to Oct 2026. Unity 6 dropped the year-based names
(2020/2021/2022 LTS) for the 6000.x scheme. Confirm the project version in
ProjectSettings/ProjectVersion.txt before assuming an API exists.
Ban-list — never emit these; use the modern replacement:
| Deprecated / removed | Use instead |
|---|---|
Input.GetAxis / GetKey / GetButton (legacy Input Manager) |
new Input System (com.unity.inputsystem): InputActionAsset / InputAction / PlayerInput — §6 |
UnityEngine.Networking (UNet), NetworkServer/NetworkClient, its NetworkBehaviour |
removed since 2022.2. → Netcode for GameObjects (com.unity.netcode.gameobjects) + gamedev-multiplayer |
Resources.Load(...) for game content |
Addressables (com.unity.addressables) — §8 |
FindObjectOfType<T>() / FindObjectsOfType<T>() |
FindFirstObjectByType<T>() / FindAnyObjectByType<T>() / FindObjectsByType<T>(FindObjectsSortMode.None) (old ones obsolete since 2023.1) |
WWW |
UnityWebRequest |
OnGUI / IMGUI for runtime game UI |
UI Toolkit (runtime) or uGUI; keep IMGUI for editor tools only |
Setting transform.position on a Rigidbody in Update |
Rigidbody.MovePosition/MoveRotation in FixedUpdate — §4 |
GameObject.Find/FindWithTag by string in Update |
cache the ref in Awake, or [SerializeField] it |
Prefer the new Input System even for single-player prototypes. Keep Debug.Log out of hot loops and
strip it from ships.
1. GameObject / Component model & Transform hierarchy
A GameObject is an empty container; behaviour comes from Components you attach
(Transform, Rigidbody, MeshRenderer, and your MonoBehaviour scripts). Every GameObject has
exactly one Transform (or RectTransform under a Canvas) giving position/rotation/scale.
- Get components:
TryGetComponent(out var rb)(no-alloc, preferred),GetComponent<T>(),GetComponentInChildren/InParent<T>(). Cache inAwake—GetComponentinUpdateis a perf sink. - Hierarchy:
transform.parent,transform.SetParent(newParent, worldPositionStays: true),transform.childCount,foreach (Transform child in transform). Child world = parent × local. - Same object model for 2D and 3D: 2D uses
Rigidbody2D/Collider2D/SpriteRenderer; 3D usesRigidbody/Collider/MeshRenderer. Never mix 2D and 3D physics components on one body.
[RequireComponent(typeof(Rigidbody))]
public sealed class Mover : MonoBehaviour
{
Rigidbody _rb;
void Awake() => _rb = GetComponent<Rigidbody>(); // cache once
}
2. Prefabs & prefab variants; instantiation
A Prefab is a reusable GameObject template (an asset). A Prefab Variant inherits from a base
prefab and overrides selected properties/children — like subclassing for assets (e.g. Enemy base →
EnemyFast variant). Edit the base and variants inherit the change unless they override that value.
- Spawn with
Instantiate(prefab, position, rotation, parent); it returns the concrete type (var e = Instantiate(_enemyPrefab);with[SerializeField] Enemy _enemyPrefab;). Store the serialized reference; do notResources.Loadit. - Destroy with
Destroy(gameObject)(end of frame) orDestroy(obj, delay). NeverDestroyImmediateat runtime. For frequently spawned objects (bullets, VFX) pool them (UnityEngine.Pool.ObjectPool<T>) instead of Instantiate/Destroy churn. - Editor-only prefab authoring uses
PrefabUtility(wrap in#if UNITY_EDITOR); absent in builds.
3. Scenes & scene management (additive loading)
A Scene holds a set of GameObjects. Register scenes in Build Profiles → Scene List (Unity 6; formerly Build Settings) so they can load by index or name.
using UnityEngine.SceneManagement;
// Additive: keep the current scene, layer another on top (e.g. UI, a level chunk).
var op = SceneManager.LoadSceneAsync("Level_02", LoadSceneMode.Additive);
op.completed += _ => Debug.Log("Level_02 loaded");
await op; // Unity 6: AsyncOperation is awaitable
// Unload just that scene later:
await SceneManager.UnloadSceneAsync("Level_02");
LoadSceneMode.Single(default) unloads everything first;Additivecomposes scenes — the basis of streaming worlds, persistent manager scenes, and separate UI scenes.SceneManager.MoveGameObjectToScenemoves an object between loaded scenes;DontDestroyOnLoadobjects surviveSingleloads (use for a bootstrap/services scene). PreferLoadSceneAsyncto avoid a hitch.
4. MonoBehaviour lifecycle
Order per frame and where each concern belongs (full ordering in references/lifecycle-deep-dive.md):
| Method | When | Put here |
|---|---|---|
Awake |
once, on instantiation (even if disabled) | cache GetComponent, self-init, no cross-object refs |
OnEnable |
each time the object/component enables | subscribe to events, register callbacks |
Start |
once, before first Update, only if active |
cross-object wiring (other objects' Awake has run) |
FixedUpdate |
fixed timestep (physics tick), 0..n / frame | all physics — Rigidbody forces/MovePosition, ApplyForce |
Update |
every frame | input reads, game logic, timers (scale by Time.deltaTime) |
LateUpdate |
every frame, after all Update |
camera follow, look-at, anything tracking moved objects |
OnDisable |
each disable / before destroy | unsubscribe everything subscribed in OnEnable |
OnDestroy |
once, on destruction | release native/unmanaged resources, final cleanup |
Rules that bite:
- Physics goes in
FixedUpdate, neverUpdate. Reading input inFixedUpdatedrops events — read input inUpdate, cache intent, apply forces inFixedUpdate. - Frame-rate independence: multiply per-frame movement by
Time.deltaTime(Update) — butFixedUpdatealready runs at a fixedTime.fixedDeltaTime. Awakeruns even on inactive objects;Startdoes not run until the object is active. Cross-object references belong inStart, notAwake.- Every
OnEnablesubscription needs a matchingOnDisableunsubscription or you leak and get double-fires after re-enable.
5. ScriptableObjects for data/config; serialization
A ScriptableObject is a data asset that lives outside any scene — ideal for config, tuning tables, enemy/item definitions, and event channels. One asset is shared by reference (no per-instance copy), so it saves memory and lets designers tweak values without touching code.
[CreateAssetMenu(fileName = "WeaponDef", menuName = "Game/Weapon Definition")]
public sealed class WeaponDefinition : ScriptableObject
{
[SerializeField] float _damage = 10f;
[SerializeField] float _fireRate = 2f;
public float Damage => _damage; // expose read-only
public float FireRate => _fireRate;
}
[SerializeField] private vs public: default to [SerializeField] private + a public
property/method. It shows in the Inspector and survives serialization without letting arbitrary code
mutate the field — encapsulation intact. Use bare public only for genuine data bags.
Serialization rules (they trip everyone):
- Unity serializes:
publicor[SerializeField]fields of supported types (primitives,string, Unity types, enums, arrays/List<T>of those, and[Serializable]plain classes/structs). - Unity does not serialize: properties (use
[field: SerializeField] public float X { get; private set; }),static,readonly,const,Dictionary<,>(use two lists or a serializable wrapper), or polymorphic references — for interface/abstract fields use[SerializeReference]. - Do not hold runtime mutable state on a shared ScriptableObject expecting it to reset — it persists in the Editor between play sessions. Keep SOs for config; put mutable per-instance state on MonoBehaviours.
6. Input System (new) — with a legacy contrast
The new Input System (com.unity.inputsystem) is enabled by default in Unity 6 and is device-agnostic
and event-driven. Define an Input Action Asset with Action Maps (e.g. "Player", "UI") and
Actions (e.g. "Move" = Value/Vector2, "Jump" = Button). Generate a C# wrapper or use the
PlayerInput component. Full setup, PlayerInput, and rebinding in references/input-system.md.
// New Input System — subscribe to the generated actions (no per-frame polling).
using UnityEngine.InputSystem;
public sealed class PlayerController : MonoBehaviour
{
InputSystem_Actions _controls; // generated C# class from the .inputactions asset
Vector2 _move;
void Awake() => _controls = new InputSystem_Actions();
void OnEnable()
{
_controls.Player.Enable();
_controls.Player.Move.performed += ctx => _move = ctx.ReadValue<Vector2>();
_controls.Player.Move.canceled += _ => _move = Vector2.zero;
_controls.Player.Jump.performed += _ => Jump();
}
void OnDisable() => _controls.Player.Disable();
}
Legacy contrast (do not write this in new code):
// LEGACY Input Manager — string axes, per-frame polling, device-blind. Avoid.
float x = Input.GetAxis("Horizontal");
if (Input.GetButtonDown("Jump")) Jump();
The new system also polls if you prefer: _controls.Player.Move.ReadValue<Vector2>() in Update.
7. Coroutines vs async/await vs Awaitable
| Use | Reach for | Why |
|---|---|---|
| Frame-sequenced gameplay (timers, tweens, "wait then spawn") | Coroutine or Awaitable | frame-aware yields; auto-tie to object lifetime |
| Unity-native async in Unity 6 (default for new async gameplay) | Awaitable |
main-thread, low-alloc (pooled), frame + thread awaits, cancels via destroyCancellationToken |
True background CPU work / library Task APIs |
async Task + await |
thread pool; but marshal back — Unity APIs are main-thread only |
// Coroutine — classic, MonoBehaviour-bound, stops when the object disables/destroys.
IEnumerator SpawnWave()
{
yield return new WaitForSeconds(1f);
Instantiate(_enemyPrefab);
}
void Start() => StartCoroutine(SpawnWave());
// Awaitable (Unity 6) — the modern replacement. Cancels automatically on destroy.
async Awaitable SpawnWaveAsync()
{
await Awaitable.WaitForSecondsAsync(1f, destroyCancellationToken);
await Awaitable.NextFrameAsync(destroyCancellationToken);
Instantiate(_enemyPrefab);
}
Rules:
- Coroutines run only while the MonoBehaviour is enabled; they die silently on disable. No return
value; exceptions across
yieldare awkward. Awaitable: neverawaitthe same instance twice (pooled → undefined behavior). PassdestroyCancellationTokenso it stops when the object is destroyed. Switch threads withAwaitable.BackgroundThreadAsync()/Awaitable.MainThreadAsync().Task: does not auto-cancel on destroy — thread aCancellationTokenyourself, and never touch Unity API off the main thread (it throws). PreferAwaitablein Unity 6 unless you needTask.
8. Addressables (intro) + the Resources anti-pattern
Addressables (com.unity.addressables) is the modern content-loading system: assets get string
addresses, load asynchronously, and can ship in the build or from a remote CDN with dependency tracking
and memory-managed release.
using UnityEngine.AddressableAssets;
var handle = Addressables.InstantiateAsync("Prefabs/Boss");
GameObject boss = await handle.Task; // or handle.Completed += ...
// when done: Addressables.ReleaseInstance(boss); // release to free memory
Resources/ is an anti-pattern for game content: everything under it is forced into the build,
loaded synchronously, bloats size/startup, and can't be patched or streamed. Reserve it for tiny
always-needed defaults; migrate real content to Addressables.
9. Build settings, platforms & player settings
- Build Profiles (Unity 6,
File → Build Profiles) supersede the old Build Settings window: per-platform scene list, target platform, and overridable player settings. - Scripting backend: IL2CPP (AOT — required for iOS/consoles/WebGL, faster, harder to reverse) vs Mono (JIT, Editor/desktop, faster iteration). Set per platform in Player Settings.
- Player Settings: product/company name, bundle identifier, icons, orientation, API compatibility
level (
.NET Standard 2.1default), managed stripping. 2D vs 3D is a template choice, not a different build path. - WebGL/mobile specifics and the shipping checklist:
references/build-and-platforms.md+gamedev-shipping.
Guardrails / gotchas
==/!= nullon a destroyedUnityEngine.Objectworks (overloaded), but?.and cached refs treat a destroyed object as non-null. Re-check withif (obj)after possible destruction.- Don't mix input systems: with
PlayerInput, don't alsoInput.GetKey— in "New" mode legacy calls throw. - One
Rigidbodyper body (child colliders fine); non-uniform scale breaks physics bodies. - Coroutines/
InvokeRepeatingstop onSetActive(false); asyncTasks keep running unless cancelled. - No allocation (
new, LINQ, boxing) inUpdate/FixedUpdatehot paths.
Related skills
gamedev-multiplayer— networking/netcode (Netcode for GameObjects); this skill hands off the wire.gamedev-physics— deep physics tuning (joints, solver, determinism) beyond "put it in FixedUpdate".gamedev-shaders— Shader Graph / HLSL, URP/HDRP materials and rendering.gamedev-pathing— NavMesh, agents, steering, path solving.gamedev-shipping— store builds, signing, size budgets, platform certification.godot/unreal— the other engines; route Godot/GDScript and Unreal/Blueprint/C++ there.
Checklist
- Confirmed the project's Unity version (
ProjectVersion.txt); no year-based-LTS assumptions. - No banned API emitted (
Input.GetAxis,UnityEngine.Networking,Resources.Loadfor content,FindObjectOfType,WWW, runtimeOnGUI). -
GetComponentresults cached inAwake; no stringFindinUpdate. - Physics in
FixedUpdate; input read inUpdate; camera/tracking inLateUpdate. - Every
OnEnablesubscription has a matchingOnDisableunsubscription. -
[SerializeField] private+ property used instead of barepublicwhere encapsulation matters. - New Input System used (actions/maps), not legacy polling; systems not mixed.
- Async uses
Awaitable(or coroutine) withdestroyCancellationToken; no double-await. - Content via Addressables, not
Resources/. - Scenes registered in Build Profiles; async/additive loading where it avoids a hitch.