unity-scripting
Unity C# Scripting
Script Fundamentals
C# scripts (.cs files) are stored in the Assets folder. Scripts gain Unity functionality by inheriting from built-in types:
- UnityEngine.Object -- Makes custom types assignable to Inspector fields
- MonoBehaviour -- Attaches to GameObjects as components to control behavior in a scene
- ScriptableObject -- Standalone data assets not attached to GameObjects
Scripts operate in two contexts:
- Runtime scripts -- Execute in the Player build (use
UnityEnginenamespace) - Editor scripts -- Run only in the Editor (use
UnityEditornamespace, place inEditorfolders)
MonoBehaviour Lifecycle
MonoBehaviours always exist as a Component of a GameObject. The lifecycle event functions execute in a strict order. You cannot rely on the order in which the same event function is invoked for different GameObjects unless configured via Script Execution Order settings.
Execution Order (ASCII Diagram)
INITIALIZATION
|
v
[Awake] ---------> Called when script instance loads (once per lifetime)
|
v
[OnEnable] ------> Called when object/component becomes enabled
|
v
(SceneManager.sceneLoaded fires here -- after OnEnable, before Start)
|
v
[Start] ---------> Called before first frame Update (once per lifetime)
|
|
| +===========================================+
| | PHYSICS LOOP (fixed timestep) |
| | |
+->| [FixedUpdate] --> Internal Physics ------>|
| | | |
| | [yield WaitForFixedUpdate resumes] |
| +===========================================+
|
v
[Update] --------> Called once per frame
|
v
[yield null / yield WaitForSeconds resumes]
|
v
(Internal Animation Update)
| [OnAnimatorMove]
| [OnAnimatorIK]
|
v
[LateUpdate] ----> Called after all Update functions complete
|
v
RENDERING
| [OnWillRenderObject]
| [OnPreCull] [OnBecameVisible/Invisible]
| [OnPreRender]
| [OnRenderObject]
| [OnPostRender]
| [OnRenderImage]
|
v
[OnGUI] ---------> Called for GUI rendering events
|
v
[yield WaitForEndOfFrame resumes]
|
v
DEACTIVATION / TEARDOWN
|
v
[OnDisable] -----> Called when component/object is disabled
|
v
[OnDestroy] -----> Called before object destruction
Key Lifecycle Callbacks
| Callback | Timing | Use For |
|---|---|---|
Awake() |
Script instance loads | One-time init, cache references |
OnEnable() |
Component enabled | Subscribe to events |
Start() |
Before first Update | Init that depends on other Awake() calls |
FixedUpdate() |
Fixed timestep (default 0.02s) | Physics calculations, Rigidbody forces |
Update() |
Every frame | Input, non-physics game logic |
LateUpdate() |
After all Update calls | Camera follow, post-Update adjustments |
OnDisable() |
Component disabled | Unsubscribe from events |
OnDestroy() |
Before destruction | Final cleanup |
Physics Callbacks
// 3D Physics
void OnCollisionEnter(Collision collision) { }
void OnCollisionStay(Collision collision) { }
void OnCollisionExit(Collision collision) { }
void OnTriggerEnter(Collider other) { }
void OnTriggerStay(Collider other) { }
void OnTriggerExit(Collider other) { }
// 2D Physics
void OnCollisionEnter2D(Collision2D collision) { }
void OnTriggerEnter2D(Collider2D other) { }
MonoBehaviour Properties (Unity 6)
| Property | Purpose |
|---|---|
destroyCancellationToken |
Token raised when MonoBehaviour is destroyed (for async cancellation) |
didAwake |
Whether Awake has been called |
didStart |
Whether Start has been called |
runInEditMode |
Allow script execution in editor |
Full lifecycle reference: references/monobehaviour-lifecycle.md
Coroutines vs Async/Await
Unity supports two patterns for operations spanning multiple frames.
Coroutines (IEnumerator)
Methods that suspend with yield return and resume based on the yield instruction.
IEnumerator Fade()
{
Color c = renderer.material.color;
for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
{
c.a = alpha;
renderer.material.color = c;
yield return new WaitForSeconds(0.1f);
}
}
void Update()
{
if (Input.GetKeyDown("f"))
{
StartCoroutine(Fade());
}
}
Yield Instructions:
yield return null-- Resume next frame (after Update)yield return new WaitForSeconds(t)-- Resume after t secondsyield return new WaitForFixedUpdate()-- Resume after FixedUpdateyield return new WaitForEndOfFrame()-- Resume after renderingyield return new WaitUntil(() => condition)-- Resume when condition is trueyield return StartCoroutine(other)-- Wait for nested coroutine
Important: Coroutines run on the main thread. Disabling the MonoBehaviour via enabled = false does NOT stop coroutines. Deactivating the GameObject or destroying the MonoBehaviour does stop them.
Awaitable (Unity 6 Async/Await)
Awaitable is Unity's custom async type -- usually more efficient than iterator-based coroutines. It is pooled to limit allocations.
async Awaitable SampleSchedulingJobsForNextFrame()
{
await Awaitable.EndOfFrameAsync();
var jobHandle = ScheduleSomethingWithJobSystem();
await Awaitable.NextFrameAsync();
jobHandle.Complete();
}
Awaitable Methods:
Awaitable.NextFrameAsync()-- Resume next frameAwaitable.FixedUpdateAsync()-- Resume at next FixedUpdateAwaitable.EndOfFrameAsync()-- Resume at end of frameAwaitable.WaitForSecondsAsync(float)-- Resume after delayAwaitable.MainThreadAsync()-- Force continuation on main threadAwaitable.BackgroundThreadAsync()-- Force continuation on background thread
Critical constraint: Awaitable instances are pooled -- never await the same instance more than once. Multiple awaits cause undefined behavior.
| Feature | Coroutine | Awaitable |
|---|---|---|
| Return values | No | Yes (Awaitable<T>) |
| Thread switching | No | Yes |
| Memory allocation | Per-yield overhead | Pooled, minimal |
| Cancellation | Manual StopCoroutine | CancellationToken support |
| Error handling | No try/catch | Full try/catch/finally |
Full async reference: references/coroutines-and-async.md
Events and Communication Patterns
C# Events and Delegates
public class Health : MonoBehaviour
{
public event System.Action<float> OnDamageTaken;
public event System.Action OnDeath;
private float _hp = 100f;
public void TakeDamage(float amount)
{
_hp -= amount;
OnDamageTaken?.Invoke(amount);
if (_hp <= 0f)
OnDeath?.Invoke();
}
}
public class UIHealthBar : MonoBehaviour
{
[SerializeField] private Health _health;
void OnEnable()
{
_health.OnDamageTaken += HandleDamage;
}
void OnDisable()
{
_health.OnDamageTaken -= HandleDamage;
}
private void HandleDamage(float amount)
{
// Update UI
}
}
UnityEvents (Inspector-assignable)
using UnityEngine.Events;
public class GameManager : MonoBehaviour
{
public UnityEvent OnGameStart;
public UnityEvent<int> OnScoreChanged;
public void StartGame()
{
OnGameStart?.Invoke();
}
}
ScriptableObject Event Channels
[CreateAssetMenu(menuName = "Events/Void Event Channel")]
public class VoidEventChannel : ScriptableObject
{
private System.Action _onEventRaised;
public void RaiseEvent()
{
_onEventRaised?.Invoke();
}
public void Subscribe(System.Action listener) => _onEventRaised += listener;
public void Unsubscribe(System.Action listener) => _onEventRaised -= listener;
}
ScriptableObjects
ScriptableObjects are serializable Unity types derived from UnityEngine.Object. They exist as independent project assets, not attached to GameObjects. Use them for shared data, configuration, and event channels.
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/SpawnManagerScriptableObject", order = 1)]
public class SpawnManagerScriptableObject : ScriptableObject
{
public string prefabName;
public int numberOfPrefabsToCreate;
public Vector3[] spawnPoints;
}
Key behaviors:
- In Edit mode, Inspector modifications save automatically; script changes require
EditorUtility.SetDirty() - At runtime, players can read ScriptableObject data but not persist modifications to disk
- Memory efficient: multiple objects reference the same asset instance instead of duplicating data
Full ScriptableObject reference: references/scriptableobjects.md
Serialization Quick Reference
Unity serializes fields that meet ALL conditions:
publicOR has[SerializeField]attribute- Not
static,const, orreadonly - Is a serializable type
Serializable types: primitives, enums (32-bit or smaller), Unity built-in types (Vector2, Vector3, Rect, Color, AnimationCurve, etc.), [Serializable] custom classes/structs, UnityEngine.Object references, List<T> and arrays of any above type.
Key attributes:
[SerializeField] private float _speed = 5f; // Serialize private field
[field: SerializeField] public float Speed { get; private set; } // Auto-property
[NonSerialized] public float tempValue; // Exclude from serialization
[HideInInspector] public float hiddenValue; // Serialize but hide from Inspector
[SerializeReference] private IMyInterface _impl; // Polymorphic serialization
Not supported: Multidimensional arrays, jagged arrays, dictionaries, nested containers. Use ISerializationCallbackReceiver for custom serialization of unsupported types.
Core API Quick Reference
Vector3
// Static direction shortcuts
Vector3.zero; // (0, 0, 0)
Vector3.one; // (1, 1, 1)
Vector3.up; // (0, 1, 0)
Vector3.forward; // (0, 0, 1)
Vector3.right; // (1, 0, 0)
// Common operations
float dist = Vector3.Distance(a, b);
float dot = Vector3.Dot(a.normalized, b.normalized);
Vector3 cross = Vector3.Cross(a, b);
Vector3 smoothed = Vector3.Lerp(from, to, t);
Vector3 moved = Vector3.MoveTowards(current, target, maxDelta);
Vector3 projected = Vector3.ProjectOnPlane(velocity, groundNormal);
Properties: magnitude, sqrMagnitude (use for comparisons -- avoids sqrt), normalized.
Quaternion
// Creation
Quaternion.identity; // No rotation
Quaternion.Euler(0f, 90f, 0f); // From Euler angles (degrees)
Quaternion.LookRotation(direction, Vector3.up); // Face a direction
Quaternion.AngleAxis(45f, Vector3.up); // Rotate around axis
Quaternion.FromToRotation(Vector3.up, normal); // Rotation between directions
// Interpolation
Quaternion.Slerp(from, to, t); // Spherical interpolation
Quaternion.Lerp(from, to, t); // Linear interpolation
// Operations
float angle = Quaternion.Angle(a, b); // Angle in degrees (0-180)
Vector3 rotatedPoint = rotation * point; // Rotate a vector
Quaternion combined = rotA * rotB; // Combine rotations
Never modify x, y, z, w directly. Use Euler(), AngleAxis(), or LookRotation().
Time
Time.deltaTime // Seconds since last frame (use in Update)
Time.fixedDeltaTime // Fixed timestep interval (use in FixedUpdate)
Time.time // Time since game start
Time.timeScale // 0 = paused, 1 = normal, 2 = double speed
Time.unscaledDeltaTime // Ignores timeScale (for UI animations during pause)
Debug
Debug.Log("Message");
Debug.LogWarning("Warning");
Debug.LogError("Error");
Debug.DrawRay(origin, direction, Color.red, duration);
Debug.DrawLine(start, end, Color.green, duration);
Common Patterns
Cached Component References
public class PlayerMovement : MonoBehaviour
{
private Rigidbody _rb;
private Transform _transform;
void Awake()
{
_rb = GetComponent<Rigidbody>();
_transform = transform; // Cache the transform property
}
void FixedUpdate()
{
// Use cached references -- never call GetComponent in Update/FixedUpdate
_rb.AddForce(Vector3.up * 10f);
}
}
Singleton Pattern
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
Async with Cancellation (Unity 6)
public class AsyncExample : MonoBehaviour
{
async void Start()
{
try
{
await LoadAndProcessAsync(destroyCancellationToken);
}
catch (OperationCanceledException) { }
}
async Awaitable LoadAndProcessAsync(CancellationToken token)
{
await Awaitable.BackgroundThreadAsync();
// Heavy computation here (off main thread)
var result = ComputeExpensiveData();
await Awaitable.MainThreadAsync();
// Back on main thread -- safe to use Unity API
ApplyResult(result);
}
}
Conditional Wait (Awaitable replacement for WaitUntil)
public static async Awaitable AwaitableUntil(Func<bool> condition, CancellationToken token)
{
while (!condition())
{
token.ThrowIfCancellationRequested();
await Awaitable.NextFrameAsync();
}
}
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
GetComponent<T>() in Update() |
Allocates and searches every frame | Cache in Awake() |
GameObject.Find() in Update() |
Expensive string search every frame | Cache reference or use serialized field |
Repeated new Vector3() in hot paths |
Unnecessary constructor overhead each frame | Use Vector3.zero, Vector3.one, or cache reusable values |
Modifying Quaternion.x/y/z/w directly |
Produces invalid rotations | Use Euler(), AngleAxis(), LookRotation() |
Physics logic in Update() |
Inconsistent at variable framerates | Use FixedUpdate() for Rigidbody forces |
Time.deltaTime in FixedUpdate() |
Works (Unity returns fixedDeltaTime implicitly) but is unclear to readers |
Use Time.fixedDeltaTime explicitly for clarity |
Forgetting to unsubscribe events in OnDisable |
Memory leaks, null reference errors | Always unsubscribe in OnDisable() |
await-ing same Awaitable twice |
Undefined behavior (pooled instances) | Await once, or wrap with .AsTask() |
Empty Update() / FixedUpdate() methods |
Unity still calls them (overhead) | Remove empty event functions |
String-based Invoke("MethodName", t) |
No compile-time safety, breaks on rename | Use coroutines or Awaitable instead |
Related Skills
- unity-foundations -- GameObjects, Components, Transforms, Scenes, Prefabs
- unity-physics -- Rigidbody, Colliders, Raycasting, Physics materials
- unity-input -- Input System, InputActions, PlayerInput component
Additional Resources
- Scripting | Execution Order | Coroutines | Async/Await
- ScriptableObjects | MonoBehaviour | Vector3
- For object pooling (
ObjectPool<T>), see references/object-pool.md