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 } }
Key Concepts: Pre-create objects, use SetActive for get/release, reuse instead of Destroy(). This is the basic pattern - but Unity's version is much better!
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); } } }

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 ); } }

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"); }

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(); }

Implementation Step 5: Test Client

πŸ“ File Structure Note - TESTING CODE

Create a new file: Assets/Scripts/Testing/TestPool.cs
Test client that spawns drones and displays pool statistics.
⚠️ This is temporary testing code - you can remove it after testing your implementation.

using UnityEngine; public class TestPool : MonoBehaviour { public float spawnInterval = 0.5f; public float spawnRange = 5f; private float _nextSpawnTime; void Update() { // Spawn drones periodically if (Time.time > _nextSpawnTime) { // Random spawn position Vector3 randomPos = new Vector3( Random.Range(-spawnRange, spawnRange), 0f, Random.Range(-spawnRange, spawnRange) ); DroneSpawner.Instance.SpawnDrone(randomPos); _nextSpawnTime = Time.time + spawnInterval; } } void OnGUI() { // Display pool statistics GUILayout.Label($"Active Drones: {DroneSpawner.Instance.GetActiveCount()}"); GUILayout.Label($"Pooled Drones: {DroneSpawner.Instance.GetInactiveCount()}"); } }
πŸ’‘ Alternative: When you have multiple test scripts with overlapping GUI buttons, consider using TestPanel.cs to combine all test controls into one draggable window. See the Event Bus lecture for the unified TestPanel implementation.

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. Create empty GameObject "TestPool"
  6. Attach TestPool script
  7. Run and watch drones spawn/recycle!

What to Observe

  • Drones spawn every 0.5 seconds
  • Drones rise to max height, then disappear
  • Watch console logs: "Created" only appears once per unique drone!
  • GUI shows active vs pooled count

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. Create TestPool spawning script
  5. Add GUI to display pool statistics
  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"); }
Benefit: Eliminates first-spawn lag. Crucial for tight gameplay!

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. Create TestPool that spawns 2 drones/second
  5. Add GUI showing active/inactive pool counts

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! πŸ‘€