Introduction: The Building Blocks of Unity
Series Overview: This is Part 3 of our 16-part Unity Game Engine Series. Here we dive deep into the fundamental building blocks of every Unity project -- GameObjects and their Components. Understanding this system thoroughly is what separates beginners from intermediate Unity developers.
1
Unity Basics & Interface
Editor overview, assets, prefabs, architecture
2
C# Scripting Fundamentals
MonoBehaviour, coroutines, input systems, patterns
3
GameObjects & Components
Transforms, renderers, custom components
You Are Here
4
Physics & Collisions
Rigidbody, colliders, raycasting, forces
5
UI Systems
Canvas, uGUI, UI Toolkit, responsive design
6
Animation & State Machines
Animator, blend trees, IK, Timeline
7
Audio & Visual Effects
AudioSource, particles, VFX Graph, post-processing
8
Building & Publishing
Build pipeline, optimization, platforms, monetization
9
Rendering Pipelines
URP, HDRP, Shader Graph, lighting systems
10
Data-Oriented Tech Stack
ECS, Jobs System, Burst Compiler
11
AI & Gameplay Systems
NavMesh, FSMs, behavior trees, procedural gen
12
Multiplayer & Networking
Netcode, RPCs, latency, prediction
13
Tools & Editor Scripting
Custom editors, debug tools, CI/CD
14
Architecture & Clean Code
Service locators, DI, ScriptableObject architecture
15
Performance Optimization
CPU/GPU profiling, memory, object pooling
16
Production & Industry Practices
Git, Agile, asset pipelines, debugging at scale
In Unity, everything in your scene is a GameObject. The player character, every enemy, every tree, every UI element, every invisible trigger zone, every camera, every light -- all GameObjects. On their own, GameObjects are empty containers. They become meaningful when you attach Components to them.
This is the Entity-Component architecture at the heart of Unity. A player character is not a special "Player" class -- it is a plain GameObject with a Transform, a MeshRenderer, a Rigidbody, a PlayerController script, and an AudioSource attached. This architecture is what makes Unity so flexible: the same engine systems that render a sword can render a skyscraper.
Key Insight: Every GameObject automatically has one component that cannot be removed: the Transform. This component defines where the object is in 3D space (position), how it is oriented (rotation), and how large it is (scale). The Transform is so fundamental that gameObject.transform and just transform are built-in shortcuts in every MonoBehaviour.
1. The Transform System
1.1 Position, Rotation & Scale
The Transform component stores three fundamental properties that define an object's spatial state:
| Property |
Type |
Description |
Default |
| position |
Vector3 |
World-space position (x, y, z) in meters |
(0, 0, 0) |
| localPosition |
Vector3 |
Position relative to parent transform |
(0, 0, 0) |
| rotation |
Quaternion |
World-space rotation (as quaternion) |
Quaternion.identity |
| localRotation |
Quaternion |
Rotation relative to parent |
Quaternion.identity |
| eulerAngles |
Vector3 |
World rotation in degrees (x, y, z) |
(0, 0, 0) |
| localScale |
Vector3 |
Scale relative to parent (no world scale property) |
(1, 1, 1) |
| lossyScale |
Vector3 (read-only) |
Approximate world scale (affected by parent chain) |
(1, 1, 1) |
using UnityEngine;
public class TransformBasics : MonoBehaviour
{
[SerializeField] private Transform target;
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float rotateSpeed = 120f;
private void Update()
{
// === POSITION ===
// Move forward in the direction the object is facing
transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
// Move toward a target in world space
transform.position = Vector3.MoveTowards(
transform.position, target.position, moveSpeed * Time.deltaTime);
// Smoothly interpolate position (easing)
transform.position = Vector3.Lerp(
transform.position, target.position, 3f * Time.deltaTime);
// === ROTATION ===
// Rotate around Y axis (turning left/right)
float horizontal = Input.GetAxis("Horizontal");
transform.Rotate(0, horizontal * rotateSpeed * Time.deltaTime, 0);
// Look at a target (instant)
transform.LookAt(target);
// Smoothly rotate toward target
Vector3 direction = (target.position - transform.position).normalized;
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(
transform.rotation, targetRotation, 5f * Time.deltaTime);
// === SCALE ===
// Pulsating scale effect
float scale = 1f + Mathf.Sin(Time.time * 3f) * 0.1f;
transform.localScale = Vector3.one * scale;
}
}
1.2 Local vs World Space
Analogy: Imagine you are standing on a train. Your local position is "3 meters from the front of the train car." Your world position is "47.3 degrees latitude, -122.1 degrees longitude." When the train moves, your local position stays the same, but your world position changes constantly. Parent-child transforms in Unity work the same way.
| Concept |
Local Space |
World Space |
| Reference |
Relative to parent transform |
Relative to scene origin (0,0,0) |
| Position |
transform.localPosition |
transform.position |
| Rotation |
transform.localRotation |
transform.rotation |
| Use Case |
Offsetting a child (weapon in hand, hat on head) |
Placing objects in the scene, raycasting |
using UnityEngine;
public class SpaceConversions : MonoBehaviour
{
[SerializeField] private Transform weaponMount; // Child of player
private void Example()
{
// TransformPoint: Convert local position to world position
// "Where in the world is the point 2 meters in front of me?"
Vector3 worldPoint = transform.TransformPoint(Vector3.forward * 2f);
// InverseTransformPoint: Convert world position to local position
// "Where is the enemy relative to me?"
Vector3 localEnemyPos = transform.InverseTransformPoint(enemyWorldPos);
// localEnemyPos.z > 0 means enemy is in front of me
// localEnemyPos.x > 0 means enemy is to my right
// TransformDirection: Convert local direction to world direction
Vector3 worldForward = transform.TransformDirection(Vector3.forward);
// Equivalent to: transform.forward
// Setting child position in parent's local space
weaponMount.localPosition = new Vector3(0.5f, 0f, 0.8f); // Right hand offset
weaponMount.localRotation = Quaternion.Euler(-10f, 0f, 0f); // Slight tilt
}
private Vector3 enemyWorldPos; // Placeholder for example
}
1.3 Quaternions vs Euler Angles
Rotations in Unity are stored internally as Quaternions -- a mathematical representation using four components (x, y, z, w). While Euler angles (degrees around X, Y, Z axes) are more human-readable, they suffer from a critical problem called gimbal lock:
Gimbal Lock: When two rotation axes align (e.g., after rotating 90 degrees on X), you lose a degree of freedom -- the object can no longer rotate in certain directions. This causes sudden "flips" and unpredictable rotation behavior. Quaternions are immune to gimbal lock, which is why Unity uses them internally. Rule of thumb: Use Euler angles for Inspector-friendly values and simple rotations. Use Quaternion methods (Quaternion.Slerp, Quaternion.LookRotation) for runtime rotation logic.
using UnityEngine;
public class RotationExamples : MonoBehaviour
{
[SerializeField] private Transform target;
private void Update()
{
// === EULER ANGLES (human-readable, susceptible to gimbal lock) ===
// Set rotation to 45 degrees on Y axis
transform.eulerAngles = new Vector3(0, 45, 0);
// Rotate by 90 degrees around Y
transform.Rotate(0, 90f * Time.deltaTime, 0);
// === QUATERNION METHODS (gimbal-lock free, interpolatable) ===
// Create rotation from Euler angles
Quaternion rot = Quaternion.Euler(0, 90, 0);
// Smooth rotation toward target (Slerp = Spherical Linear Interpolation)
Vector3 direction = (target.position - transform.position).normalized;
if (direction != Vector3.zero)
{
Quaternion lookRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(
transform.rotation, lookRotation, 5f * Time.deltaTime);
}
// Rotate from current rotation by an angle around an axis
transform.rotation = Quaternion.RotateTowards(
transform.rotation, target.rotation, 180f * Time.deltaTime);
// Angle between two rotations (useful for "is the enemy facing me?")
float angle = Quaternion.Angle(transform.rotation, target.rotation);
if (angle < 10f)
Debug.Log("Nearly aligned!");
// LookAt with a specific up direction
transform.LookAt(target, Vector3.up);
}
}
2. Core Components
2.1 MeshRenderer, MeshFilter & SpriteRenderer
Renderers are what make things visible. Without a renderer, a GameObject exists in the scene but is completely invisible:
| Component |
Purpose |
Key Properties |
| MeshFilter |
Holds the 3D mesh data (vertices, triangles) |
mesh, sharedMesh |
| MeshRenderer |
Renders the mesh with materials |
materials[], shadowCastingMode, receiveShadows |
| SpriteRenderer |
Renders 2D sprites |
sprite, color, sortingLayerName, sortingOrder, flipX/Y |
| SkinnedMeshRenderer |
Renders deformable meshes (animated characters) |
bones[], rootBone, blendShapes, updateWhenOffscreen |
| LineRenderer |
Renders lines in 3D space |
positionCount, width, material, useWorldSpace |
| TrailRenderer |
Renders a trail behind a moving object |
time, startWidth, endWidth, material |
using UnityEngine;
public class RendererExamples : MonoBehaviour
{
// === 3D MESH RENDERING ===
[Header("3D Settings")]
[SerializeField] private MeshRenderer meshRenderer;
[SerializeField] private Material damageMaterial;
// === 2D SPRITE RENDERING ===
[Header("2D Settings")]
[SerializeField] private SpriteRenderer spriteRenderer;
[SerializeField] private Sprite[] walkFrames;
public void FlashDamage()
{
StartCoroutine(DamageFlash());
}
private System.Collections.IEnumerator DamageFlash()
{
// Store original materials
Material[] originals = meshRenderer.materials;
// Replace all materials with damage material
Material[] damageMats = new Material[originals.Length];
for (int i = 0; i < damageMats.Length; i++)
damageMats[i] = damageMaterial;
meshRenderer.materials = damageMats;
yield return new WaitForSeconds(0.1f);
// Restore originals
meshRenderer.materials = originals;
}
// 2D: Sorting order determines draw order (higher = drawn on top)
public void SetSortingOrder(int order)
{
spriteRenderer.sortingOrder = order;
}
// 2D: Flip sprite for direction changes (no rotation needed)
public void FaceDirection(float horizontalInput)
{
if (horizontalInput < 0) spriteRenderer.flipX = true;
else if (horizontalInput > 0) spriteRenderer.flipX = false;
}
// Visibility check (useful for culling, AI awareness)
public bool IsVisibleToCamera()
{
return meshRenderer.isVisible; // True if ANY camera can see it
}
}
2.2 Camera: Projection, Culling & Depth
The Camera component determines what the player sees. Understanding its settings is essential for both visual quality and performance:
| Property |
Options |
Use Case |
| Projection |
Perspective / Orthographic |
3D games / 2D games, UI, isometric views |
| Field of View |
1-179 degrees (perspective only) |
60 = natural, 90 = wide, 110 = FPS/VR |
| Orthographic Size |
Float (half-height in units) |
2D zoom level -- smaller = more zoomed in |
| Clipping Planes |
Near (0.01+) / Far (1000+) |
Performance: smaller range = fewer objects rendered |
| Culling Mask |
Layer selection |
Choose which layers this camera renders |
| Depth |
Integer |
Render order when multiple cameras exist |
using UnityEngine;
public class CameraController : MonoBehaviour
{
[Header("Follow Target")]
[SerializeField] private Transform target;
[SerializeField] private Vector3 offset = new Vector3(0, 8, -6);
[SerializeField] private float smoothSpeed = 5f;
[Header("Zoom")]
[SerializeField] private float minFOV = 40f;
[SerializeField] private float maxFOV = 80f;
[SerializeField] private float zoomSpeed = 10f;
private Camera cam;
private void Awake()
{
cam = GetComponent<Camera>();
}
// LateUpdate ensures camera follows AFTER all objects have moved
private void LateUpdate()
{
if (target == null) return;
// Smooth follow
Vector3 desiredPosition = target.position + offset;
transform.position = Vector3.Lerp(
transform.position, desiredPosition, smoothSpeed * Time.deltaTime);
// Always look at target
transform.LookAt(target);
// Scroll wheel zoom
float scroll = Input.GetAxis("Mouse ScrollWheel");
cam.fieldOfView = Mathf.Clamp(
cam.fieldOfView - scroll * zoomSpeed, minFOV, maxFOV);
}
// Useful utility: Convert screen point to world position
public Vector3 ScreenToWorldPoint(Vector2 screenPos)
{
Vector3 pos = new Vector3(screenPos.x, screenPos.y, cam.nearClipPlane);
return cam.ScreenToWorldPoint(pos);
}
// Check if a world position is visible on screen
public bool IsWorldPointVisible(Vector3 worldPoint)
{
Vector3 viewportPoint = cam.WorldToViewportPoint(worldPoint);
return viewportPoint.x >= 0 && viewportPoint.x <= 1 &&
viewportPoint.y >= 0 && viewportPoint.y <= 1 &&
viewportPoint.z > 0; // In front of camera
}
}
2.3 Light Types, Shadows & Baking
| Light Type |
Description |
Performance |
Example |
| Directional |
Parallel rays, infinite distance (like the sun) |
Cheap (one per scene) |
Sun, moon, global illumination |
| Point |
Emits in all directions from a point |
Moderate (affects nearby geometry) |
Light bulbs, torches, explosions |
| Spot |
Cone-shaped emission |
Moderate |
Flashlights, stage lights, headlights |
| Area |
Emits from a rectangular surface (baked only) |
Expensive (baked only) |
Windows, TV screens, fluorescent panels |
Baked vs Realtime Lighting: Realtime lights calculate shadows every frame (expensive but dynamic). Baked lights pre-calculate lighting into lightmaps (free at runtime but static -- objects cannot move). Mixed lights combine both: baked indirect lighting with realtime direct/shadows. For mobile games, bake as much as possible. For PC/console, use a mix. Always bake ambient occlusion.
Case Study
Firewatch -- Atmosphere Through Lighting
Campo Santo's Firewatch used Unity's lighting system masterfully to create its iconic visual style. The game features a full day-night cycle where the same environment transforms from warm golden mornings to eerie blue twilights. They achieved this primarily through directional light color/intensity curves driven by a time-of-day system, combined with carefully authored skybox gradients and fog color transitions. The result: a visually stunning game that runs on modest hardware because most of the "beauty" comes from smart lighting configuration, not polygon count.
Day-Night Cycle
Atmospheric Lighting
Performance-Conscious
3. Writing Custom Components
3.1 Reusable Script Patterns
The best Unity components are small, focused, and reusable. A component should do one thing well, be configurable through the Inspector, and make no assumptions about the GameObject it is attached to:
using UnityEngine;
// GOOD: Small, focused, reusable on any object
public class Rotator : MonoBehaviour
{
[SerializeField] private Vector3 rotationSpeed = new Vector3(0, 90, 0);
[SerializeField] private Space space = Space.Self;
private void Update()
{
transform.Rotate(rotationSpeed * Time.deltaTime, space);
}
}
// GOOD: Reusable health component with no assumptions about what it is on
public class Damageable : MonoBehaviour
{
[SerializeField] private float maxHealth = 100f;
[SerializeField] private bool destroyOnDeath = true;
[SerializeField] private float destroyDelay = 0f;
[SerializeField] private GameObject deathEffectPrefab;
public float CurrentHealth { get; private set; }
public bool IsAlive => CurrentHealth > 0;
public event System.Action<float, float> OnDamaged; // damage, remaining
public event System.Action OnDeath;
private void Awake() => CurrentHealth = maxHealth;
public void ApplyDamage(float amount)
{
if (!IsAlive) return;
CurrentHealth = Mathf.Max(0, CurrentHealth - amount);
OnDamaged?.Invoke(amount, CurrentHealth);
if (!IsAlive)
{
OnDeath?.Invoke();
if (deathEffectPrefab != null)
Instantiate(deathEffectPrefab, transform.position, Quaternion.identity);
if (destroyOnDeath)
Destroy(gameObject, destroyDelay);
}
}
}
// GOOD: Separate visual feedback, attached alongside Damageable
[RequireComponent(typeof(Damageable))]
public class DamageVisuals : MonoBehaviour
{
[SerializeField] private Renderer targetRenderer;
[SerializeField] private Color flashColor = Color.red;
[SerializeField] private float flashDuration = 0.1f;
private Damageable damageable;
private Color originalColor;
private void Awake()
{
damageable = GetComponent<Damageable>();
if (targetRenderer != null)
originalColor = targetRenderer.material.color;
}
private void OnEnable() => damageable.OnDamaged += HandleDamage;
private void OnDisable() => damageable.OnDamaged -= HandleDamage;
private void HandleDamage(float amount, float remaining)
{
if (targetRenderer != null)
StartCoroutine(Flash());
}
private System.Collections.IEnumerator Flash()
{
targetRenderer.material.color = flashColor;
yield return new WaitForSeconds(flashDuration);
targetRenderer.material.color = originalColor;
}
}
3.2 Dependency Injection with Interfaces
Interfaces allow your components to communicate without knowing each other's concrete types. This is the foundation of testable, modular game code:
using UnityEngine;
// === INTERFACES: Define contracts, not implementations ===
public interface IDamageable
{
void TakeDamage(float amount);
float CurrentHealth { get; }
bool IsAlive { get; }
}
public interface IInteractable
{
string InteractionPrompt { get; }
void Interact(GameObject interactor);
}
// === IMPLEMENTATIONS: Different objects implement the same interface ===
public class BreakableObject : MonoBehaviour, IDamageable
{
[SerializeField] private float health = 50f;
public float CurrentHealth => health;
public bool IsAlive => health > 0;
public void TakeDamage(float amount)
{
health -= amount;
if (!IsAlive)
{
// Spawn debris, play sound, destroy
Destroy(gameObject);
}
}
}
public class TreasureChest : MonoBehaviour, IInteractable
{
[SerializeField] private GameObject[] lootPrefabs;
private bool isOpened = false;
public string InteractionPrompt => isOpened ? "Empty" : "Open Chest";
public void Interact(GameObject interactor)
{
if (isOpened) return;
isOpened = true;
// Spawn loot
foreach (var prefab in lootPrefabs)
{
Vector3 spawnPos = transform.position + Vector3.up * 1.5f;
Instantiate(prefab, spawnPos, Quaternion.identity);
}
}
}
// === CONSUMER: Works with ANY IDamageable, not specific types ===
public class ProjectileDamager : MonoBehaviour
{
[SerializeField] private float damage = 25f;
private void OnCollisionEnter(Collision collision)
{
// Works on enemies, breakable walls, explosive barrels -- anything damageable
var damageable = collision.gameObject.GetComponent<IDamageable>();
damageable?.TakeDamage(damage);
Destroy(gameObject); // Destroy the projectile
}
}
4. Scene Organization
A well-organized hierarchy is not just about aesthetics -- it directly impacts performance and maintainability:
| Practice |
Why |
Performance Impact |
| Use empty GameObjects as folders |
Group related objects (--- Enemies ---, --- UI ---) |
None (empty GOs are free) |
| Flatten deep hierarchies |
Every child transform recalculates when parent moves |
Deep nesting = slower transform updates |
| Avoid parenting under moving objects |
All children recalculate world transforms each frame |
Static children under moving parents = wasted CPU |
| Use Static flags on non-moving objects |
Enables batching, lightmap baking, nav mesh, occlusion |
Major rendering performance improvement |
# Recommended scene hierarchy organization
Scene Root
├── --- Managers ---
│ ├── GameManager
│ ├── AudioManager
│ └── UIManager
├── --- Environment ---
│ ├── Terrain
│ ├── Static_Props (all flagged as Static)
│ │ ├── Tree_01
│ │ ├── Rock_03
│ │ └── Building_West
│ └── Dynamic_Props
│ ├── Door_01 (animated)
│ └── Elevator_01
├── --- Characters ---
│ ├── Player
│ └── Enemies
│ ├── Enemy_Goblin_01
│ └── Enemy_Goblin_02
├── --- Lighting ---
│ ├── Directional Light (Sun)
│ ├── Point Lights
│ └── Reflection Probes
├── --- Cameras ---
│ ├── Main Camera
│ └── Minimap Camera
├── --- UI ---
│ ├── HUD Canvas
│ ├── Pause Menu Canvas
│ └── Dialogue Canvas
└── --- Audio ---
├── BGM_Source
└── Ambient_Source
Tags are string identifiers for GameObjects. Layers are bit-field categories used by physics and rendering systems:
| Feature |
Tags |
Layers |
| Purpose |
Identify individual objects by category |
Control physics collisions and camera rendering |
| Access |
gameObject.CompareTag("Player") |
gameObject.layer == LayerMask.NameToLayer("Enemy") |
| Limit |
Unlimited custom tags |
32 layers total (0-31), 8 built-in |
| Use For |
Finding specific objects (Player, Respawn, Finish) |
Physics filtering (Project Settings > Physics matrix) |
using UnityEngine;
public class TagsAndLayers : MonoBehaviour
{
// TAGS: Use CompareTag (not == "string") -- it's faster and safer
private void OnTriggerEnter(Collider other)
{
// GOOD: CompareTag is 0-allocation and handles missing tags gracefully
if (other.CompareTag("Player"))
{
Debug.Log("Player entered trigger!");
}
// BAD: String comparison allocates garbage and throws if tag doesn't exist
// if (other.tag == "Player") { }
}
// LAYERS: Used for physics filtering and raycasting
[SerializeField] private LayerMask groundLayer;
[SerializeField] private LayerMask enemyLayer;
private bool IsGrounded()
{
// Raycast only against the ground layer (ignores everything else)
return Physics.Raycast(transform.position, Vector3.down, 1.1f, groundLayer);
}
private void FindEnemiesInRange(float range)
{
// OverlapSphere only checks objects on the enemy layer
Collider[] enemies = Physics.OverlapSphere(transform.position, range, enemyLayer);
Debug.Log($"Found {enemies.Length} enemies within {range}m");
}
}
5. Advanced Patterns
5.1 Composition Over Inheritance
This is perhaps the single most important architectural principle in Unity development. Instead of deep class hierarchies, compose complex behaviors from simple, independent components:
using UnityEngine;
// ====== BAD: Deep inheritance hierarchy ======
// public class Entity : MonoBehaviour { }
// public class Character : Entity { }
// public class Enemy : Character { }
// public class FlyingEnemy : Enemy { }
// public class FlyingBossEnemy : FlyingEnemy { }
// Problem: What if you want a ground boss? Or a flying non-enemy NPC?
// You'd need to restructure the entire hierarchy.
// ====== GOOD: Composition with small, focused components ======
// Movement behaviors (attach one or more)
public class GroundMovement : MonoBehaviour
{
[SerializeField] private float speed = 5f;
[SerializeField] private float gravity = -20f;
private CharacterController controller;
private void Awake() => controller = GetComponent<CharacterController>();
public void Move(Vector3 direction)
{
Vector3 move = direction.normalized * speed;
move.y = gravity;
controller.Move(move * Time.deltaTime);
}
}
public class FlyingMovement : MonoBehaviour
{
[SerializeField] private float speed = 8f;
[SerializeField] private float hoverHeight = 5f;
[SerializeField] private float hoverAmplitude = 0.5f;
public void Move(Vector3 direction)
{
Vector3 move = direction.normalized * speed * Time.deltaTime;
float hover = Mathf.Sin(Time.time * 2f) * hoverAmplitude;
transform.position += move;
transform.position = new Vector3(
transform.position.x,
hoverHeight + hover,
transform.position.z);
}
}
// Compose your entities freely:
// Ground Enemy: Damageable + GroundMovement + PatrolAI + MeleeAttack
// Flying Enemy: Damageable + FlyingMovement + ChaseAI + RangedAttack
// Flying Boss: Damageable(hp=5000) + FlyingMovement + BossAI + MeleeAttack + RangedAttack + ShieldAbility
// Ground NPC: GroundMovement + DialogueComponent + QuestGiver
// No inheritance needed. Maximum flexibility.
5.2 Event-Driven Communication with UnityEvent
UnityEvents are Inspector-configurable event hooks that allow designers to wire up interactions without writing code:
using UnityEngine;
using UnityEngine.Events;
public class PressurePlate : MonoBehaviour
{
[Header("Events")]
[SerializeField] private UnityEvent OnActivated;
[SerializeField] private UnityEvent OnDeactivated;
[SerializeField] private UnityEvent<float> OnWeightChanged;
[Header("Settings")]
[SerializeField] private float activationWeight = 1f;
[SerializeField] private string requiredTag = "Player";
private float currentWeight = 0f;
private bool isActivated = false;
private void OnTriggerEnter(Collider other)
{
if (!other.CompareTag(requiredTag)) return;
var rb = other.GetComponent<Rigidbody>();
float weight = rb != null ? rb.mass : 1f;
currentWeight += weight;
OnWeightChanged?.Invoke(currentWeight);
if (!isActivated && currentWeight >= activationWeight)
{
isActivated = true;
OnActivated?.Invoke();
}
}
private void OnTriggerExit(Collider other)
{
if (!other.CompareTag(requiredTag)) return;
var rb = other.GetComponent<Rigidbody>();
float weight = rb != null ? rb.mass : 1f;
currentWeight = Mathf.Max(0, currentWeight - weight);
OnWeightChanged?.Invoke(currentWeight);
if (isActivated && currentWeight < activationWeight)
{
isActivated = false;
OnDeactivated?.Invoke();
}
}
}
// In the Inspector, drag a Door object onto OnActivated and select Door.Open()
// No code changes needed -- designers can wire up any combination of responses:
// Pressure plate -> opens door, plays sound, enables light, starts particle effect
5.3 Modular Design Principles
Case Study
Subnautica -- Component-Driven Ecosystem
Unknown Worlds' Subnautica, built in Unity, demonstrates component-based architecture at scale. Every creature in the game's vast ocean is composed from the same building blocks: a Locomotion component (swim/walk/float), a CreatureActions component (flee/hunt/idle), a LiveMixin component (health/damage), and a BehaviourLOD component (reduces AI complexity when far from the player). A tiny fish and a massive Reaper Leviathan share the same component types -- only the data and configuration differ. This allowed a small team to populate an entire alien ocean with diverse, believable creatures without writing unique code for each one.
Component Architecture
Data-Driven
LOD System
Modular AI
The Single Responsibility Principle: Each component should have one reason to change. If you find a component handling movement AND health AND audio AND UI updates, it is doing too much. Split it into MovementComponent, HealthComponent, AudioFeedback, and HealthUI. Each can be tested, replaced, and reused independently.
Exercises & Self-Assessment
Exercise 1
Transform Playground
Build a solar system simulation using only Transform operations:
- Create a Sun (yellow sphere at origin, scale 3)
- Create Earth orbiting the Sun (child of an empty "EarthOrbit" pivot that rotates). Use
Rotate() on the pivot
- Create Moon orbiting Earth (child of "MoonOrbit" pivot parented to Earth)
- Add a
Rotator component to each planet for self-rotation (different speeds)
- Log Earth's
localPosition vs position each frame -- observe how local stays constant while world changes
- Use
Transform.TransformPoint to place a space station 2 units above Earth in Earth's local space
Exercise 2
Component Composition Challenge
Build an interactive scene using ONLY reusable components (no one-off scripts):
- Create these reusable components:
Damageable, DamageVisuals, Rotator, Bobber (sinusoidal vertical movement), Collector (collects tagged objects)
- Compose a "Coin" from: SpriteRenderer + Rotator + Bobber + a tag "Collectible"
- Compose a "Player" from: MeshRenderer + Collector(tag="Collectible") + Damageable + DamageVisuals
- Compose a "Turret" from: MeshRenderer + a simple aim-at-player script + Damageable + DamageVisuals
- Notice how no component "knows about" the others -- they communicate through events and interfaces
Exercise 3
Camera System
Implement three switchable camera modes:
- Third-person follow: Camera behind and above the player, smooth follow with LateUpdate
- Top-down: Camera directly above, orthographic projection
- First-person: Camera as child of the player, at head height
- Switch between modes with number keys 1/2/3
- Add smooth transitions between camera positions using
Vector3.Lerp
Exercise 4
Reflective Questions
- Explain the difference between
transform.position and transform.localPosition. When does it matter which you use?
- Why does Unity store rotations as Quaternions instead of Euler angles? What is gimbal lock and when would you encounter it?
- You have a character with a sword held in the right hand. How would you organize the hierarchy so the sword moves with the hand during animations?
- Compare using
CompareTag("Enemy") vs checking a layer with a LayerMask for finding enemies in a raycast. When is each appropriate?
- Design a door system using only UnityEvents and existing components (no new scripts). What components would you use and how would you wire them?
Conclusion & Next Steps
You now have a thorough understanding of Unity's GameObject and Component architecture. Here are the key takeaways from Part 3:
- The Transform is the only mandatory component -- master local vs world space, use
TransformPoint/InverseTransformPoint for coordinate conversions, and prefer Quaternion methods for rotation
- Quaternions prevent gimbal lock -- use Euler angles for Inspector values, Quaternion.Slerp/LookRotation for runtime rotation logic
- Core components (renderers, cameras, lights) have many powerful properties -- learn their settings to maximize visual quality and performance
- Custom components should be small and focused -- one responsibility per component, configurable through [SerializeField], no assumptions about the host GameObject
- Interfaces enable decoupled communication -- IDamageable, IInteractable, IPickup allow systems to interact without knowing concrete types
- Scene organization matters -- use empty GameObjects as folders, flatten deep hierarchies, and mark non-moving objects as Static for performance
- Composition over inheritance is Unity's core philosophy -- compose behaviors from independent components instead of building fragile class hierarchies
Next in the Series
In Part 4: Physics & Collisions, we'll explore Unity's physics engines (PhysX and Box2D), Rigidbodies, colliders, forces, collision detection modes, raycasting, joints, ragdolls, and how games like Celeste and Angry Birds achieve their legendary physics feel.
Continue the Series
Part 2: C# Scripting Fundamentals
Master C# for Unity: MonoBehaviour, coroutines, the New Input System, code architecture patterns, ScriptableObjects, and debugging.
Read Article
Part 4: Physics & Collisions
Rigidbodies, colliders, raycasting, forces, joints, and building physics-driven gameplay.
Read Article
Part 5: UI Systems
Canvas, uGUI, UI Toolkit, responsive design, and building polished game interfaces.
Read Article