Object Pool Pattern - Performance Optimization

Performance Optimization

The Object Pool Pattern ♻️

CSCI 3213 - Game Programming

Reuse, don't recreate - the key to smooth performance!

Today's Content

πŸ“š Based on Chapter 8

Improving Performance with the Object Pool Pattern

From: Game Development Patterns with Unity 2021 (2nd Edition)
By David Baron

Note: Modern Unity includes native ObjectPool<T> support! We'll learn both the pattern theory and Unity's built-in implementation.

Today's Learning Objectives

What We'll Master

  • 🎯 Understand garbage collection performance issues
  • 🎯 Learn Object Pool Pattern structure
  • 🎯 Implement Unity's ObjectPool<T> system
  • 🎯 Build a drone spawning pool
  • 🎯 Measure performance improvements

Goal: Create a high-performance drone spawner that reuses objects instead of constantly creating/destroying them.

Developer's Note: "Never Panic Early"

⚠️ What to Expect During Implementation

As we implement this pattern, we'll be creating multiple files in a specific order. You will see errors in Unity and your IDE until all files are complete.

How to Handle Development Errors

  • Don't Ignore: Note the errors - they're telling you something
  • Don't Panic: These are expected until all files are created
  • Stay Calm: Follow the implementation order and errors will resolve

As Apollo 13's Fred Haise said: "Never Panic Early"

This is a critical skill for software development. Note problems, but stay focused on the implementation path.

The Problem: Instantiate/Destroy Overhead

❌ Without Object Pooling

void Update() { if (Time.time > nextSpawn) { // Create new drone - EXPENSIVE! GameObject drone = Instantiate(dronePrefab); // After 5 seconds, destroy it - TRIGGERS GARBAGE COLLECTION! Destroy(drone, 5f); nextSpawn = Time.time + spawnInterval; } }

Problems: GC SPIKES, memory allocation overhead, framerate stutters, poor performance

⚠️ Real Impact: Spawning 100 bullets per second = 100 Instantiate() calls + 100 Destroy() calls = major performance hit!

Understanding Garbage Collection

What Happens When You Instantiate/Destroy

1. Instantiate()

β€’ Allocates memory on the heap

β€’ Creates GameObject, components, and scripts

β€’ Registers in Unity's scene hierarchy

β€’ Cost: Expensive!

2. Destroy()

β€’ Marks object for deletion

β€’ Leaves memory as "garbage"

β€’ Doesn't immediately free memory

β€’ Cost: Creates garbage!

3. Garbage Collection (GC)

β€’ Unity pauses your game periodically

β€’ Scans for unused memory

β€’ Frees garbage back to system

β€’ Cost: FRAME RATE STUTTER!

What is the Object Pool Pattern?

Object Pool Pattern

A creational pattern that manages a set of reusable objects, handing them out when needed and reclaiming them when done, instead of creating and destroying objects repeatedly.

βœ… Key Concept

Recycle instead of recreate! Create objects once, disable them when not in use, re-enable when needed again.

In Simple Terms: Like a library - books (objects) are checked out, used, returned, and checked out again by someone else!

Object Pool: Benefits & Drawbacks

βœ… Benefits

  • Performance: Eliminates GC spikes
  • Memory: Predictable memory usage
  • Framerate: Consistent, smooth performance
  • Mobile-Friendly: Critical for mobile games
  • Control: Set max pool size

⚠️ Drawbacks

  • Complexity: More code to manage
  • Memory Trade-off: Holds objects in memory
  • Reset Logic: Must clean object state
  • Over-pooling: Can waste memory if oversized
  • Debugging: Objects never fully destroyed
Best For: Frequently spawned objects - bullets, enemies, particles, VFX, pickups

Object Pool Lifecycle

CREATE POOL
β†’
GET OBJECT
β†’
USE OBJECT
β†’
RELEASE BACK
β†Ί

Step-by-Step Process

  1. Initialization: Pre-create a collection of inactive objects
  2. Get: Activate an object from the pool (or create if pool empty)
  3. Use: Object performs its function (bullet flies, drone patrols)
  4. Release: Deactivate object and return to pool
  5. Repeat: Same object gets reused many times!

Building a Simple Object Pool

Understanding the Mechanics (We'll delete this next!)

πŸ“ File Structure Note - INSTRUCTIONAL EXAMPLE

Create a new file: Assets/Scripts/Testing/SimpleObjectPool.cs
This is a learning exercise to understand pool mechanics - we'll delete this later!
⚠️ Temporary code for understanding only - not for production use.

using UnityEngine; using System.Collections.Generic; public class SimpleObjectPool : MonoBehaviour { public GameObject prefab; public int poolSize = 10; private List<GameObject> pool = new List<GameObject>(); void Start() { // Pre-create all pool objects for (int i = 0; i < poolSize; i++) { GameObject obj = Instantiate(prefab); obj.SetActive(false); // Start inactive pool.Add(obj); } } public GameObject Get() { // Find first inactive object foreach (GameObject obj in pool) { if (!obj.activeInHierarchy) { obj.SetActive(true); // Activate it return obj; } } return null; // Pool exhausted! } public void Release(GameObject obj) { obj.SetActive(false); // Deactivate and return to pool } }

How It Works

Start() - Pre-creates all objects at startup. Each one is Instantiated once, then immediately deactivated and stored in the list.

SetActive(false) - Unity's way to "hide" an object. It stops rendering, physics, and Update() calls but the object stays in memory ready to reuse.

Get() - Scans the list for the first inactive object. Linear search (O(n)) - simple but slow for large pools.

!obj.activeInHierarchy - Checks if the object is currently inactive (available). The ! means "not active" = available for reuse.

Release() - Simply deactivates the object. No Destroy() call - the object returns to the pool for reuse.

This works but has limitations: fixed size, no callbacks, no type safety. Unity's ObjectPool<T> solves all of these!

Note: This is a learning exercise only. You'll delete this code and use Unity's ObjectPool<T> in the next slide, which handles edge cases and is production-ready!

Unity's Native ObjectPool<T>

Built-In Pool System

Unity includes UnityEngine.Pool.ObjectPool<T> - a production-ready pooling system!

  • βœ“ Type-Safe: Generic implementation
  • βœ“ Callbacks: OnCreate, OnGet, OnRelease, OnDestroy
  • βœ“ Flexible: Works with any object type
  • βœ“ Tested: Battle-tested by Unity engineers
  • βœ“ Thread-Safe Options: For advanced scenarios

Our Approach: Use Unity's built-in system - no need to reinvent the wheel!

Object Pool Pattern Structure

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   DroneSpawner          β”‚  ← Pool Manager
│─────────────────────────│
β”‚ - pool: ObjectPool<T>  β”‚
β”‚ - dronePrefab           β”‚
β”‚ + SpawnDrone()          β”‚
β”‚ + ReleaseDrone()        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ manages
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ObjectPool<Drone>      │────────▢│      Drone       β”‚  ← Pooled Object
│─────────────────────────│         │──────────────────│
β”‚ + Get()                 β”‚         β”‚ + OnSpawn()      β”‚
β”‚ + Release(drone)        β”‚         β”‚ + OnDespawn()    β”‚
β”‚ + Clear()               β”‚         β”‚ + Patrol()       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”‚ callbacks
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Pool Callbacks        β”‚
│─────────────────────────│
β”‚ + OnCreateDrone()       β”‚  ← Create new instance
β”‚ + OnGetDrone()          β”‚  ← Activate from pool
β”‚ + OnReleaseDrone()      β”‚  ← Deactivate to pool
β”‚ + OnDestroyDrone()      β”‚  ← Clean up on Clear()
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                

Implementation Step 1: Drone Object

πŸ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Enemies/Drone.cs
Enemy drone object that works with Unity's ObjectPool.
⚠️ This code goes into your Unity project for Blade Racer.

using UnityEngine; public class Drone : MonoBehaviour { public float speed = 5f; public float maxHeight = 10f; public float wobbleAmount = 2f; // Called when drone is spawned from pool public void OnSpawn() { gameObject.SetActive(true); Debug.Log("[Drone] Spawned and activated"); } // Called when drone is returned to pool public void OnDespawn() { gameObject.SetActive(false); Debug.Log("[Drone] Despawned and deactivated"); } void Update() { // Simple upward movement with wobble float wobble = Mathf.Sin(Time.time * wobbleAmount); transform.Translate(Vector3.up * speed * Time.deltaTime); transform.Translate(Vector3.right * wobble * Time.deltaTime); // Release back to pool when reaching max height if (transform.position.y > maxHeight) { // Note: DroneSpawner doesn't exist yet - you'll create it in Step 2! // Your editor will show errors until then - this is expected (Never Panic Early!) DroneSpawner.Instance.ReleaseDrone(this); } } }

Drone Lifecycle

Public fields - speed, maxHeight, wobbleAmount are editable in the Unity Inspector. Tweak without changing code!

OnSpawn() - Called by the pool's OnGet callback. Activates the GameObject so it becomes visible and starts running Update().

OnDespawn() - Called by the pool's OnRelease callback. Deactivates the GameObject - it stops rendering and updating but stays in memory.

ReleaseDrone(this) - The drone releases itself back to the pool when it reaches max height. this refers to the current Drone instance.

Notice: no Destroy() call anywhere! The drone is deactivated and recycled, never destroyed.

Implementation Step 2: Spawner Setup

πŸ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Spawners/DroneSpawner.cs
Spawner that manages drone pooling using Unity's ObjectPool.
⚠️ This code goes into your Unity project for Blade Racer.

using UnityEngine; using UnityEngine.Pool; public class DroneSpawner : MonoBehaviour { // Singleton access public static DroneSpawner Instance { get; private set; } // Pool configuration public Drone dronePrefab; public int defaultCapacity = 10; public int maxPoolSize = 50; public bool collectionCheck = true; // The object pool private ObjectPool<Drone> _pool; void Awake() { Instance = this; // Create the pool with callbacks _pool = new ObjectPool<Drone>( createFunc: CreateDrone, actionOnGet: OnGetDrone, actionOnRelease: OnReleaseDrone, actionOnDestroy: OnDestroyDrone, collectionCheck: collectionCheck, defaultCapacity: defaultCapacity, maxSize: maxPoolSize ); } }

Key Concepts

using UnityEngine.Pool - Required import to access Unity's built-in ObjectPool<T> class

Singleton - Instance lets any script access the spawner via DroneSpawner.Instance without needing a reference.

ObjectPool<Drone> - Generic pool typed to Drone. Ensures type safety - you can only pool/retrieve Drone objects.

Constructor params:

  • createFunc - How to make a new drone
  • actionOnGet - What to do when retrieving
  • actionOnRelease - What to do when returning
  • actionOnDestroy - How to clean up
  • collectionCheck - Warns if you release twice
  • maxSize - Cap to prevent memory bloat

Implementation Step 3: Pool Callbacks

// Continuing DroneSpawner class... // Called when pool needs to create a new drone private Drone CreateDrone() { Drone drone = Instantiate(dronePrefab); drone.gameObject.SetActive(false); Debug.Log("[Pool] Created new drone"); return drone; } // Called when drone is taken from pool private void OnGetDrone(Drone drone) { drone.OnSpawn(); Debug.Log("[Pool] Drone retrieved from pool"); } // Called when drone is returned to pool private void OnReleaseDrone(Drone drone) { drone.OnDespawn(); Debug.Log("[Pool] Drone returned to pool"); } // Called when pool is cleared (optional cleanup) private void OnDestroyDrone(Drone drone) { Destroy(drone.gameObject); Debug.Log("[Pool] Drone destroyed"); }

Four Pool Callbacks

Pool Empty + Get() called
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  CreateDrone()   β”‚ ← Instantiate new
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

_pool.Get()
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  OnGetDrone()    β”‚ ← Activate
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

_pool.Release()
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ OnReleaseDrone() β”‚ ← Deactivate
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

_pool.Clear() or maxSize exceeded
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ OnDestroyDrone() β”‚ ← Destroy
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Debug.Log in each callback lets you trace pool behavior in the Console during testing.

Quick Note: GameObject vs gameObject

Understanding Unity's Naming Patterns

Notice the Capitalization?

In the previous slide, you saw: drone.gameObject.SetActive(false)

Why gameObject (lowercase g) instead of GameObject (capital G)?

GameObject (Capital G)

  • What it is: A class type
  • Used for: Declaring variables, type references
  • Examples:
    • GameObject obj;
    • Instantiate(prefab) returns GameObject
    • new GameObject("MyObject")

gameObject (lowercase g)

  • What it is: A property on MonoBehaviour
  • Used for: Accessing the GameObject this script is attached to
  • Examples:
    • gameObject.SetActive(false)
    • gameObject.name = "Drone"
    • drone.gameObject.transform

The Pattern in Unity

Unity follows this pattern for built-in types:

  • Transform (class) vs transform (property on Component)
  • Rigidbody (class) vs rigidbody (cached component reference)
  • Renderer (class) vs renderer (cached component reference)
Rule of Thumb: Capital = Type/Class, lowercase = instance property. When you see drone.gameObject, you're accessing the GameObject that the Drone component is attached to!

Implementation Step 4: Public API

// Continuing DroneSpawner class... // Spawn a drone at specified position public Drone SpawnDrone(Vector3 position) { Drone drone = _pool.Get(); drone.transform.position = position; return drone; } // Release drone back to pool public void ReleaseDrone(Drone drone) { _pool.Release(drone); } // Get pool statistics public int GetActiveCount() { return _pool.CountActive; } public int GetInactiveCount() { return _pool.CountInactive; } void OnDestroy() { // Clean up pool when spawner is destroyed _pool.Clear(); }

Public API Methods

SpawnDrone() - Wraps _pool.Get() and sets the spawn position. Callers never touch the pool directly.

ReleaseDrone() - Wraps _pool.Release(). The Drone calls this on itself when it reaches max height.

Statistics - CountActive and CountInactive are built-in ObjectPool properties. Useful for debugging and GUI.

OnDestroy() - Unity lifecycle method called when the GameObject is destroyed. _pool.Clear() triggers OnDestroyDrone for each pooled object.

Encapsulation: other scripts only call SpawnDrone/ReleaseDrone, never interact with ObjectPool<T> directly.

TestPanel: Object Pool Fields & Input

πŸ“ Update TestPanel.cs

Add these fields and keyboard shortcuts to your existing TestPanel.cs.

// Add to TestPanel fields private bool _poolExpanded = true; private DroneSpawner _spawner; private bool _autoSpawning = false; private float _spawnInterval = 0.5f; private float _spawnRange = 5f; private float _nextSpawnTime; // Add to Start() _spawner = FindFirstObjectByType<DroneSpawner>(); // Add to Update() - Object Pool shortcuts if (_spawner != null) { if (Input.GetKeyDown(KeyCode.G)) { Vector3 pos = new Vector3( Random.Range(-_spawnRange, _spawnRange), 0f, Random.Range(-_spawnRange, _spawnRange)); _spawner.SpawnDrone(pos); } if (Input.GetKeyDown(KeyCode.T)) _autoSpawning = !_autoSpawning; // Auto-spawn when toggled on if (_autoSpawning && Time.time > _nextSpawnTime) { Vector3 pos = new Vector3( Random.Range(-_spawnRange, _spawnRange), 0f, Random.Range(-_spawnRange, _spawnRange)); _spawner.SpawnDrone(pos); _nextSpawnTime = Time.time + _spawnInterval; } }

Code Breakdown

New Fields: Store spawner reference, auto-spawn state, and timing config. _poolExpanded controls the collapsible GUI section.

FindFirstObjectByType<DroneSpawner>() - Finds the spawner in the scene at startup. Safer than using the Singleton in Start() since execution order isn't guaranteed.

G key: Spawns one drone at a random position. T key: Toggles continuous auto-spawning on/off (every 0.5s).

Auto-spawn timer: The Time.time > _nextSpawnTime cooldown pattern spawns a drone every 0.5 seconds while auto-spawn is toggled on.

Note: Null check on _spawner prevents errors if DroneSpawner isn't in the scene.

TestPanel: GUI Section & Keymap

Add DrawObjectPoolSection() and call it from DrawWindow(). Update the keymap.

void DrawObjectPoolSection() { if (_spawner == null) return; GUI.backgroundColor = Color.cyan; _poolExpanded = GUILayout.Toggle( _poolExpanded, "β–Ό Object Pool", "button"); GUI.backgroundColor = Color.white; if (_poolExpanded) { GUILayout.BeginVertical("box"); if (GUILayout.Button("Spawn Drone (G)")) { Vector3 pos = new Vector3( Random.Range(-_spawnRange, _spawnRange), 0f, Random.Range(-_spawnRange, _spawnRange)); _spawner.SpawnDrone(pos); } string label = _autoSpawning ? "Stop Auto-Spawn (T)" : "Start Auto-Spawn (T)"; if (GUILayout.Button(label)) _autoSpawning = !_autoSpawning; GUILayout.Space(5); GUILayout.Label($"Active: {_spawner.GetActiveCount()}"); GUILayout.Label($"Pooled: {_spawner.GetInactiveCount()}"); GUILayout.EndVertical(); } } // In DrawWindow(), add: DrawObjectPoolSection(); // Updated DrawKeymapWindow(): void DrawKeymapWindow(int windowID) { GUILayout.Label("--- Event Bus ---"); GUILayout.Label("C = Countdown"); GUILayout.Label("S = Stop"); GUILayout.Label("R = Restart"); GUILayout.Label("F = Finish"); GUILayout.Label("P = Pause"); GUILayout.Label("Q = Quit"); GUILayout.Space(10); GUILayout.Label("--- Command Pattern ---"); GUILayout.Label("A = Turn Left"); GUILayout.Label("D = Turn Right"); GUILayout.Label("1 = Start Recording"); GUILayout.Label("2 = Stop Recording"); GUILayout.Label("3 = Play Replay"); GUILayout.Space(10); // NEW: Object Pool shortcuts GUILayout.Label("--- Object Pool ---"); GUILayout.Label("G = Spawn Drone"); GUILayout.Label("T = Toggle Auto-Spawn"); GUILayout.Space(10); GUILayout.Label("--- General ---"); GUILayout.Label("K = Toggle this keymap"); GUILayout.Space(10); if (GUILayout.Button("Close")) _showKeymap = false; GUI.DragWindow(); }

GUI Section Pattern

Collapsible header: Cyan toggle button expands/collapses the section. Same pattern as the green Command Pattern header.

Spawn + Auto-Spawn buttons: Mirror the keyboard shortcuts (G and T). Button label changes based on auto-spawn state.

Live pool stats: Active and Pooled counts update every frame - watch objects recycle in real-time!

TestPanel Evolution

  • Lecture 4: Event Bus section + keymap
  • Lecture 5: + Command Pattern section
  • Lecture 6: + Object Pool section
  • Future: Add sections as needed!
Don't forget: Call DrawObjectPoolSection() in DrawWindow()!

Critical: Object Reset Logic

⚠️ Always Reset Object State!

When objects are reused, they carry over their previous state. You MUST reset them to clean initial conditions!

πŸ“ File Structure Note - PRODUCTION CODE

Update the OnSpawn() method in: Assets/Scripts/Enemies/Drone.cs
Add this expanded reset logic to ensure proper object state when reused.
⚠️ This code goes into your Unity project for Blade Racer.

public void OnSpawn() { gameObject.SetActive(true); // CRITICAL: Reset all state! transform.rotation = Quaternion.identity; // Reset rotation // Reset velocity if Rigidbody exists Rigidbody rb = GetComponent<Rigidbody>(); if (rb != null) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } // Reset any other state variables your drone might have // Example: health = maxHealth; // Example: currentState = DroneState.Idle; }

Why Check for Null?

GetComponent<Rigidbody>() might return null!

If your drone doesn't have a Rigidbody component attached, trying to access rb.velocity would crash the game.

Unsafe approach:
rb.velocity = Vector3.zero;
❌ Crashes if rb is null!

Safe approach:
if (rb != null) { rb.velocity = Vector3.zero; }
βœ… Safely skips if no Rigidbody

Best practice: Always check for null when using GetComponent!

Common Bug: Forgetting to reset velocity on Rigidbody objects, causing "ghost momentum" on respawn!

Testing the Object Pool

Test Setup Steps

  1. Create new Unity scene
  2. Create Drone prefab (Cube with Drone script)
  3. Create empty GameObject "DroneSpawner"
  4. Attach DroneSpawner script, assign drone prefab
  5. Verify TestPanel has Object Pool section (added in previous step)
  6. Run and watch drones spawn/recycle!

What to Observe

  • Press T to start auto-spawning (or click in TestPanel)
  • Drones rise to max height, then disappear
  • Watch console logs: "Created" only appears once per unique drone!
  • TestPanel shows live Active/Pooled counts

Performance Comparison

Without Pooling

~15ms

GC spike every 2 seconds

Frame drops visible

With Pooling

~0.1ms

No GC spikes

Smooth 60 FPS

Performance Metrics (100 drones/sec)

Memory Allocations: 150x reduction
GC Frequency: 95% reduction
Frame Time Consistency: 10x improvement
Mobile Battery Life: ~20% longer

Hands-On Implementation πŸ’»

30-Minute Implementation Challenge

Implement the Object Pool Pattern:

  1. Create Drone MonoBehaviour with OnSpawn/OnDespawn
  2. Create DroneSpawner with ObjectPool<Drone>
  3. Implement all four pool callbacks
  4. Add Object Pool section to TestPanel
  5. Add pool stats and spawn controls to TestPanel GUI
  6. Test and verify objects are recycled (check console logs)
Success Metric: Console should show "Created new drone" only 10-15 times even after spawning 100+ drones!

Gaming History Moment πŸ•ΉοΈ

Golden Age Arcades: Space Invaders (1978)

In 1978, Tomohiro Nishikado created Space Invaders for Taito, and it became the first blockbuster arcade game. Running on extremely limited hardware, the game had to manage dozens of aliens and player bullets on screen simultaneously. With only 8KB of ROM and 1KB of RAM, creating and destroying objects was expensive.

Nishikado's solution? Reuse everything. Bullets weren't created and destroyed - they were recycled! When you fired, the game grabbed an inactive bullet from a fixed pool, activated it, and sent it upward. When it hit an alien or reached the top, it returned to the pool. The game pre-allocated a maximum of 3 player bullets and dozens of alien projectiles at startup, then reused them constantly.

Connection to Object Pool Pattern

Space Invaders pioneered object pooling out of necessity! With such limited memory, the game couldn't afford to allocate and deallocate bullets every frame. Pre-allocating a pool and recycling objects - exactly what we're implementing today - was essential to making the game run smoothly on 1978 hardware. This pattern became fundamental to game development and remains critical for performance even on modern systems!

Learn More: Museum of Play: Golden Age Arcade | The King of Kong (Documentary)

Determining Pool Size

Key Questions

  • What's the maximum concurrent objects?
  • What's the typical spawn rate?
  • How long does each object "live"?
  • What's acceptable memory overhead?

Formula: defaultCapacity = spawnRate Γ— avgLifetime Γ— 1.2

Example: 10 bullets/sec Γ— 3 second lifetime Γ— 1.2 buffer = 36 bullets

Formula: maxSize = defaultCapacity Γ— 2 (or peak concurrent)

Example: 36 default Γ— 2 safety = 72 max bullets

Pro Tip: Use Unity Profiler to measure actual peak concurrent objects in gameplay!

What to Pool in Games

πŸ”« Projectiles

Bullets, arrows, missiles, lasers

Why: Spawned frequently, short-lived

πŸ‘Ύ Enemies

Minions, drones, zombies

Why: Wave-based spawning

πŸ’₯ VFX

Explosions, impacts, trails

Why: Particle systems are expensive

🎡 Audio Sources

Sound effect players

Why: Many sounds play simultaneously

πŸ’Ž Pickups

Coins, health, power-ups

Why: Spawn/despawn continuously

πŸ“„ UI Elements

Damage numbers, notifications

Why: Instantiate/Destroy is slow

Advanced: Pool Warm-Up

πŸ“ File Structure Note - OPTIONAL ENHANCEMENT

Update the Start() method in: Assets/Scripts/Spawners/DroneSpawner.cs
Add warm-up logic to pre-populate the pool at startup.
⚠️ This is an optional optimization - implement if needed for your game.

// Pre-populate pool at startup for zero initial lag void Start() { // Create pool _pool = new ObjectPool<Drone>( createFunc: CreateDrone, actionOnGet: OnGetDrone, actionOnRelease: OnReleaseDrone, actionOnDestroy: OnDestroyDrone, defaultCapacity: 20 ); // WARM-UP: Pre-create objects var tempList = new List<Drone>(); for (int i = 0; i < 20; i++) { tempList.Add(_pool.Get()); } // Return all to pool immediately foreach (var drone in tempList) { _pool.Release(drone); } Debug.Log("[Pool] Warmed up with 20 drones"); }

Warm-Up Strategy

Why warm up? - The first _pool.Get() triggers CreateDrone() which calls Instantiate(). Warming up moves all that cost to the loading screen instead of mid-gameplay.

_pool.Get() - Triggers CreateDrone callback, creating a real drone instance

_pool.Release(drone) - Immediately returns each drone to the pool as inactive

The pattern - Get all β†’ store in temp list β†’ release all back. After this, the pool has 20 pre-created drones ready to go with zero allocation cost.

Essential for games where the first seconds of gameplay must be perfectly smooth (e.g., wave-based shooters).

Benefit: Eliminates first-spawn lag. Crucial for tight gameplay!

Homework Part 2: Performance Comparison

What You'll Build

To prove that object pooling actually improves performance, you'll create two identical scenes β€” one with pooling, one without β€” and compare them using Unity's Profiler.

Scene A: No Pooling

  • NaiveDrone.cs - Self-destructs with Destroy()
  • NaiveSpawner.cs - Creates with Instantiate()
  • No reuse β€” every drone is created and destroyed

Scene B: With Pooling

  • Drone.cs - Returns to pool via ReleaseDrone()
  • DroneSpawner.cs - Uses ObjectPool<T>
  • Objects recycled β€” created once, reused many times
Key: Both scenes must spawn at the same rate (10 drones/sec for 10 seconds = 100 total) for a fair comparison. The only difference is pooling vs. no pooling.

Non-Pooled Drone: NaiveDrone.cs

πŸ“ File Structure Note - TESTING CODE

Create a new file: Assets/Scripts/Testing/NaiveDrone.cs
A simplified drone that self-destructs β€” the "old" way, without pooling.
⚠️ This is for your comparison scene only β€” not for production use.

using UnityEngine; public class NaiveDrone : MonoBehaviour { public float speed = 5f; public float maxHeight = 10f; public float wobbleAmount = 2f; void Update() { // Same movement as pooled Drone float wobble = Mathf.Sin(Time.time * wobbleAmount); transform.Translate( Vector3.up * speed * Time.deltaTime); transform.Translate( Vector3.right * wobble * Time.deltaTime); if (transform.position.y > maxHeight) { // THE KEY DIFFERENCE: Destroy(gameObject); } } }

Pooled vs Non-Pooled

Same movement - Identical speed, height, and wobble logic. The only difference is what happens when the drone reaches max height.

Destroy(gameObject) - Marks the object for deletion. Unity will free its memory later during garbage collection β€” causing the GC spikes we want to measure.

No pool reference - NaiveDrone doesn't know about DroneSpawner or ObjectPool. It's completely standalone β€” simpler code, but worse performance.

Pooled Drone:     β†’ pool.Release()  β†’ deactivated
NaiveDrone:       β†’ Destroy()       β†’ garbage collected

This contrast is exactly what the Profiler will reveal!

Non-Pooled Spawner: NaiveSpawner.cs

πŸ“ File Structure Note - TESTING CODE

Create a new file: Assets/Scripts/Testing/NaiveSpawner.cs
A simple spawner that creates drones with Instantiate β€” no pool, no reuse.
⚠️ This is for your comparison scene only β€” not for production use.

using UnityEngine; public class NaiveSpawner : MonoBehaviour { public GameObject dronePrefab; public float spawnInterval = 0.1f; public float spawnRange = 5f; private float _nextSpawnTime; void Update() { if (Time.time > _nextSpawnTime) { Vector3 pos = new Vector3( Random.Range(-spawnRange, spawnRange), 0f, Random.Range(-spawnRange, spawnRange)); // Create brand new drone every time! Instantiate(dronePrefab, pos, Quaternion.identity); _nextSpawnTime = Time.time + spawnInterval; } } }

Why This Is Expensive

Instantiate() - Every call allocates new memory: GameObject, Transform, MonoBehaviour, and all component data. At 10/sec, that's 10 heap allocations per second.

No callbacks - Unlike DroneSpawner, there's no OnGet/OnRelease/OnDestroy. No control over the object lifecycle at all.

spawnInterval = 0.1f - Spawns 10 drones/sec. Over 10 seconds that's 100 Instantiate() calls + 100 Destroy() calls = 200 expensive operations.

DroneSpawner:   pool.Get() β†’ reuse existing object
NaiveSpawner:   Instantiate() β†’ allocate new memory

The Profiler will show the cumulative cost of all these allocations.

Setting Up Scene A (No Pooling)

Scene A Setup Steps

  1. File β†’ New Scene, save as SceneA_NoPool
  2. Create a Cube in the scene
  3. Add the NaiveDrone script to the Cube
  4. Drag the Cube into your Prefabs folder to make a prefab
  5. Delete the Cube from the scene (keep the prefab!)
  6. Create an empty GameObject, name it "NaiveSpawner"
  7. Attach NaiveSpawner script to it
  8. Drag your NaiveDrone prefab into the Drone Prefab field
  9. Set Spawn Interval to 0.1 (10 drones/sec)
Scene A setup - NaiveSpawner in hierarchy with Inspector showing prefab and interval

Setting Up Scene B (With Pooling)

You already have most of this from Part 1! Just verify everything is in place.

Scene B Setup Steps

  1. File β†’ New Scene, save as SceneB_WithPool
  2. Add your Drone prefab (with the pooled Drone script)
  3. Create empty GameObject "DroneSpawner", attach DroneSpawner script
  4. Assign the Drone prefab to DroneSpawner's Drone Prefab field
  5. Verify TestPanel is in the scene with the Object Pool section
  6. In TestPanel, set Spawn Interval to 0.1 (match Scene A!)
Scene B setup - DroneSpawner and TestPanel in hierarchy with Inspector
Critical: Both scenes MUST use the same spawn rate (0.1s interval = 10/sec). If the rates differ, your comparison is invalid!

Unity Profiler: Getting Started

Opening the Profiler

Window β†’ Analysis β†’ Profiler (or press Ctrl+7 / Cmd+7)

Unity Profiler - Window > Analysis > Profiler showing CPU Usage module

Key Controls

  • Record button (red circle) - Start/stop recording
  • Clear - Reset profiler data between tests
  • Deep Profile - Leave OFF (too slow for this test)
  • Click a frame in the timeline to see its details

Modules to Enable

  • CPU Usage - Shows frame time and GC allocations
  • Memory - Shows total memory and GC frequency
  • The Hierarchy view (bottom panel) shows per-method details
  • Look for the GC.Alloc column!
Pro Tip: Dock the Profiler window next to the Game view so you can watch both simultaneously!

Running the Test & Reading Results

Test Procedure

  1. Open Scene A (No Pooling)
  2. Open the Profiler, click Clear, then Record
  3. Press Play and let it run for 30 seconds
  4. Stop playback, screenshot the Profiler timeline
  5. Repeat steps 1-4 for Scene B (With Pooling)

Scene A: No Pooling

Scene A Profiler - GC spikes during Instantiate/Destroy

Look for: Irregular spikes in the timeline, high GC.Alloc values, inconsistent frame times

Scene B: With Pooling

Scene B Profiler - smooth, consistent frame times with Object Pooling

Look for: Flat, consistent timeline, near-zero GC.Alloc, stable frame times

For your video: Show both Profiler screenshots side-by-side and explain the difference. Note the FPS average, GC.Alloc totals, and frame time consistency in your narration.

Homework Assignment πŸ“

Assignment: Drone Pool with Performance Testing

Due: Next class
Submission: Unity project + video to D2L

Part 1: Core Implementation (50 points)

  1. Implement Drone class with OnSpawn/OnDespawn + custom reset logic
  2. Implement DroneSpawner with Unity ObjectPool<Drone>
  3. Add all four pool callbacks with debug logs
  4. Add Object Pool section to TestPanel with auto-spawn (2 drones/sec)
  5. Display active/inactive pool counts in TestPanel GUI

Homework Assignment (Continued)

Part 2: Performance Comparison (50 points)

  1. Create Two Scenes:
    • Scene A: WITHOUT pooling (Instantiate/Destroy)
    • Scene B: WITH pooling (ObjectPool)
  2. Both scenes spawn 100 drones over 10 seconds
  3. Record performance metrics:
    • Open Unity Profiler (Window β†’ Analysis β†’ Profiler)
    • Record FPS average
    • Record GC.Alloc count
    • Take screenshots of Profiler CPU usage
  4. Document findings: Which scene performs better? By how much?

Optional Enhancement:

Implement pool warm-up that pre-creates 20 drones at Start() - great for eliminating first-spawn lag!

Video Submission Guidelines

Recording Checklist (3-5 minutes)

  1. Show Code: Briefly show Drone and DroneSpawner classes
  2. Show Scene Setup: Hierarchy and Inspector settings
  3. Test Scene A (No Pool):
    • Show Profiler recording
    • Run for 30 seconds
    • Point out GC spikes in Profiler
  4. Test Scene B (With Pool):
    • Show Profiler recording
    • Run for 30 seconds
    • Show smooth performance, no GC spikes
    • Show GUI with pool statistics
  5. Compare Results: Side-by-side Profiler screenshots
  6. Narration: Explain the performance difference

Grading Rubric (100 points)

Point Breakdown

25 pts: DroneSpawner correctly implements ObjectPool<T>
25 pts: Profiler data showing performance improvement
15 pts: Drone class with proper OnSpawn/OnDespawn and reset logic
15 pts: All four pool callbacks implemented and logged
10 pts: Two comparison scenes (with/without pooling)
10 pts: Video demonstrates all features with clear narration

Additional Resources

πŸ“š Further Reading

πŸ”§ Tools

  • Unity Profiler: Window β†’ Analysis β†’ Profiler
  • Memory Profiler: Package Manager β†’ Memory Profiler
  • Frame Debugger: Window β†’ Analysis β†’ Frame Debugger
Office Hours: Questions about Profiler or performance testing? Come see me!
A -->

Questions & Discussion πŸ’¬

Open Floor

  • How does garbage collection impact performance?
  • When should I use object pooling?
  • How do I determine optimal pool size?
  • What's the difference between defaultCapacity and maxSize?
  • How do I use the Unity Profiler?
  • Homework clarifications?

Performance Optimized! ♻️

Today's Achievements:

Homework Due Next Class:

Drone Pool + Performance Comparison + Profiler Data

Video submission to D2L

Next: Observer Pattern for decoupled component communication! πŸ‘€