| name | Unity Fundamentals |
| description | This skill should be used when the user asks about "Unity lifecycle", "MonoBehaviour methods", "Awake vs Start", "Update vs FixedUpdate", "Unity serialization", "[SerializeField]", "GetComponent", "component references", "Unity prefabs", "prefab workflow", or needs guidance on fundamental Unity patterns and best practices. |
| version | 0.1.0 |
Unity Fundamentals
Comprehensive guidance on Unity's core systems, MonoBehaviour lifecycle, serialization, component architecture, and prefab workflows.
Overview
Unity development centers around MonoBehaviour components, the Component pattern, and a specific lifecycle of callback methods. Understanding these fundamentals is critical for writing correct, performant Unity code. This skill covers:
- MonoBehaviour lifecycle methods and their proper usage
- Serialization system and Inspector integration
- Component-based architecture and GetComponent patterns
- Prefab workflows and best practices
MonoBehaviour Lifecycle
Unity calls specific methods on MonoBehaviour scripts in a predetermined order. Using the wrong method leads to bugs, null references, and poor performance.
Initialization Methods
Awake()
Called when the script instance loads, before any Start() calls. Use for initializing this object's state.
private void Awake()
{
rigidbody = GetComponent<Rigidbody>();
animator = GetComponent<Animator>();
currentHealth = maxHealth;
inventory = new List<Item>();
}
Use Awake() for:
- Caching component references
- Initializing private fields
- Setting up internal state
- Creating singletons/managers
Don't use Awake() for:
- Referencing other GameObjects (use Start instead)
- Performing operations that depend on other scripts being ready
Start()
Called before the first Update(), after all Awake() calls complete. Use for setup that references other objects.
private void Start()
{
playerTransform = GameObject.FindWithTag("Player").transform;
gameManager = FindObjectOfType<GameManager>();
GameManager.Instance.RegisterEnemy(this);
SetupWeapon(gameManager.GetStartingWeapon());
}
Use Start() for:
- Finding and referencing other GameObjects
- Calling initialization methods on other components
- Registering with managers or systems
- Setup that depends on scene being fully initialized
Common Pattern:
private Camera mainCamera;
private Transform target;
private void Awake()
{
mainCamera = Camera.main;
}
private void Start()
{
target = GameObject.FindWithTag("Target").transform;
}
OnEnable() / OnDisable()
Called when GameObject becomes active/inactive. Use for subscribing/unsubscribing from events.
private void OnEnable()
{
GameEvents.OnPlayerDied += HandlePlayerDeath;
InputManager.OnJumpPressed += HandleJump;
StartCoroutine(SpawnEnemies());
}
private void OnDisable()
{
GameEvents.OnPlayerDied -= HandlePlayerDeath;
InputManager.OnJumpPressed -= HandleJump;
StopAllCoroutines();
}
Critical: Always unsubscribe in OnDisable() to prevent memory leaks.
Update Methods
Update()
Called every frame. Use sparingly - performance critical.
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
Jump();
UpdateUI();
}
Avoid Update() when possible. Prefer event-driven approaches.
FixedUpdate()
Called at fixed timestep (default 50 FPS). Use for physics operations only.
private void FixedUpdate()
{
rigidbody.AddForce(moveDirection * moveSpeed);
rigidbody.MovePosition(transform.position + velocity * Time.fixedDeltaTime);
}
Rule: If it touches Rigidbody or physics, use FixedUpdate(). Everything else uses Update() or event-driven approaches.
LateUpdate()
Called after all Update() calls. Use for camera follow and final adjustments.
private void LateUpdate()
{
transform.position = target.position + offset;
ClampToBounds();
UpdateHealthBar();
}
Destruction Methods
OnDestroy()
Called when GameObject is destroyed. Use for final cleanup.
private void OnDestroy()
{
GameEvents.OnPlayerDied -= HandlePlayerDeath;
if (texture != null)
Destroy(texture);
GameManager.Instance.UnregisterEnemy(this);
}
Lifecycle Order Summary
- Awake() - Initialize self
- OnEnable() - Subscribe to events
- Start() - Reference others, final setup
- FixedUpdate() - Physics (50 FPS)
- Update() - Per-frame logic (variable FPS)
- LateUpdate() - Camera, final adjustments
- OnDisable() - Unsubscribe from events
- OnDestroy() - Final cleanup
Serialization System
Unity's serialization system controls what appears in the Inspector and how data persists.
Serialized Fields
Make private fields editable in Inspector:
[SerializeField] private int maxHealth = 100;
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private GameObject prefab;
Benefits:
- Keeps encapsulation (private access)
- Inspector editing
- Prefab/scene data persistence
Best Practice: Prefer [SerializeField] private over public fields.
Header and Tooltip
Organize Inspector sections:
[Header("Movement Settings")]
[Tooltip("Maximum movement speed in units per second")]
[SerializeField] private float moveSpeed = 5f;
[Tooltip("Rotation speed in degrees per second")]
[SerializeField] private float rotationSpeed = 180f;
[Header("Combat Settings")]
[SerializeField] private int attackDamage = 25;
Serialization Rules
What Unity serializes:
- Public fields (unless [HideInInspector])
- Private fields with [SerializeField]
- Supported types: primitives, Unity objects, structs, arrays, Lists
What Unity doesn't serialize:
- Properties
- Private fields without [SerializeField]
- Dictionaries
- Interfaces
- Static fields
Properties vs Serialized Fields
public int health;
[SerializeField] private int health = 100;
public int Health
{
get => health;
set
{
health = Mathf.Clamp(value, 0, maxHealth);
OnHealthChanged?.Invoke(health);
}
}
Component Architecture
Unity uses the Component pattern - functionality composed from multiple components.
GetComponent Pattern
Cache references in Awake() to avoid repeated lookups:
private void Update()
{
GetComponent<Rigidbody>().velocity = Vector3.forward;
}
private Rigidbody rb;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
private void Update()
{
rb.velocity = Vector3.forward;
}
Component Variations
rb = GetComponent<Rigidbody>();
animator = GetComponentInChildren<Animator>();
canvas = GetComponentInParent<Canvas>();
Collider[] colliders = GetComponents<Collider>();
Performance: Cache all GetComponent results. Never call in Update/FixedUpdate.
Required Components
Declare component dependencies:
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(Collider))]
public class PlayerMovement : MonoBehaviour
{
private Rigidbody rb;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
}
Component Communication
Direct Reference (Inspector):
[SerializeField] private HealthDisplay healthDisplay;
private void TakeDamage(int amount)
{
health -= amount;
healthDisplay.UpdateHealth(health);
}
GetComponent (Runtime):
private void OnTriggerEnter(Collider other)
{
var health = other.GetComponent<Health>();
if (health != null)
health.TakeDamage(10);
}
Events (Decoupled):
public event Action<int> OnHealthChanged;
private void TakeDamage(int amount)
{
health -= amount;
OnHealthChanged?.Invoke(health);
}
Prefab Workflows
Prefabs are reusable GameObject templates. Understanding prefab workflows prevents common issues.
Creating Prefabs
Drag GameObject from Hierarchy to Project window. Blue text in Hierarchy indicates prefab instance.
Prefab Instances
Changes to prefab instances:
- Override - Changes to this instance only (bold blue)
- Apply - Push changes to prefab asset (affects all instances)
- Revert - Discard instance changes, match prefab
Prefab Variants
Create variations of a base prefab:
Base Prefab: Enemy
├── Variant: FastEnemy (increased speed)
├── Variant: TankEnemy (increased health)
└── Variant: FlyingEnemy (added flight)
Changes to base prefab propagate to variants.
Nested Prefabs
Prefabs can contain other prefabs:
Car Prefab
├── Wheel Prefab (x4)
├── Engine Prefab
└── Door Prefab (x4)
Edit nested prefabs independently.
Programmatic Prefab Usage
[SerializeField] private GameObject enemyPrefab;
[SerializeField] private Transform spawnPoint;
private void SpawnEnemy()
{
GameObject enemy = Instantiate(enemyPrefab, spawnPoint.position, Quaternion.identity);
enemy.GetComponent<Enemy>().SetTarget(player);
enemy.transform.SetParent(enemyContainer);
}
Prefab Best Practices
- Use prefabs for anything spawned at runtime (enemies, projectiles, UI panels)
- Create prefab variants instead of duplicating prefabs
- Apply changes carefully - affects all instances
- Keep prefabs in organized folders - Prefabs/Characters, Prefabs/UI, etc.
- Test prefab changes before applying to all instances
Common Patterns
Singleton Manager
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
Object Initialization
public class Enemy : MonoBehaviour
{
[SerializeField] private int health = 100;
private Rigidbody rb;
private Transform target;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
private void Start()
{
target = GameObject.FindWithTag("Player").transform;
}
private void OnEnable()
{
GameEvents.OnWaveComplete += HandleWaveComplete;
}
private void OnDisable()
{
GameEvents.OnWaveComplete -= HandleWaveComplete;
}
}
Additional Resources
Reference Files
For detailed guidance on specific topics:
references/lifecycle-detailed.md - Complete lifecycle method reference
references/serialization-guide.md - Advanced serialization patterns
references/component-patterns.md - Component architecture best practices
references/prefab-workflows.md - Comprehensive prefab usage guide
Quick Reference
Lifecycle Order: Awake → OnEnable → Start → FixedUpdate → Update → LateUpdate → OnDisable → OnDestroy
Serialization: [SerializeField] private over public
Components: Cache in Awake(), never call GetComponent in Update()
Prefabs: Use for reusable objects, test before applying changes
Follow these fundamentals to build solid Unity projects that are performant, maintainable, and bug-free.