Game Programming - CSCI 3213
Spring 2026 - Lecture 12
Oklahoma City University
Spatial partitioning is an optimization technique that divides game space into manageable regions to improve performance.
Racing games need long, varied tracks without consuming massive memory.
A stack is a collection where the last item added is the first one removed.
Push(item) - Add item to top of stackPop() - Remove and return top itemPeek() - View top item without removingCount - Number of items in stackStack<GameObject> segments = new Stack<GameObject>();
segments.Push(segment1); // Add to stack
segments.Push(segment2); // Add another
GameObject top = segments.Pop(); // Returns segment2
// segment1 is still in stack
using UnityEngine;
using System.Collections.Generic;
[CreateAssetMenu(fileName = "New Track", menuName = "Track")]
public class Track : ScriptableObject
{
public string trackName;
public float segmentLength = 40f;
public List<GameObject> segments;
}
trackName - Identifies the track (e.g., "Desert Circuit")segmentLength - Distance between segments (40 units)segments - List of prefabs to randomly select fromusing UnityEngine;
using System.Collections.Generic;
public class TrackController : MonoBehaviour
{
public Track track;
public BikeController bikeController;
private Stack<GameObject> _segStack;
private Transform _segParent;
private float _zPos;
private int _segCount;
void Start()
{
_segParent = GameObject.Find("Track").transform;
_segStack = new Stack<GameObject>();
// Initialize stack with segments in REVERSE order
for (int i = track.segments.Count - 1; i >= 0; i--)
{
_segStack.Push(track.segments[i]);
}
LoadSegment(3); // Load first 3 segments
}
}
Challenge: We want segments to appear in list order, but Stack is LIFO (Last In, First Out).
// If list is: [Segment1, Segment2, Segment3]
// Push in reverse:
_segStack.Push(Segment3); // Bottom of stack
_segStack.Push(Segment2); // Middle
_segStack.Push(Segment1); // Top
// Now Pop() returns Segment1 first! โ
private void LoadSegment(int amount)
{
for (int i = 0; i < amount; i++)
{
if (_segStack.Count > 0)
{
// Get next segment from stack
GameObject segment = Instantiate(
_segStack.Pop(),
Vector3.zero,
Quaternion.identity
);
// Set as child of Track parent
segment.transform.SetParent(_segParent);
// Position segment ahead of player
segment.transform.localPosition =
new Vector3(0, 0, _zPos);
// Move position for next segment
_zPos += track.segmentLength;
_segCount++;
}
}
}
// Assume segmentLength = 40
LoadSegment(1): Position at Z = 0
LoadSegment(1): Position at Z = 40
LoadSegment(1): Position at Z = 80
// _zPos increments by 40 each time
// Creates continuous track ahead of player
Player (stationary at Z=0)
โ
[Segment 0]โโโโโ[Segment 1]โโโโโ[Segment 2]
Z=0 Z=40 Z=80
Track moves BACKWARD toward player
private void LoadSegment(int amount)
{
for (int i = 0; i < amount; i++)
{
// Check if stack is empty
if (_segStack.Count == 0)
{
RepopulateStack();
}
// Rest of loading code...
}
}
private void RepopulateStack()
{
// Refill stack in reverse order again
for (int i = track.segments.Count - 1; i >= 0; i--)
{
_segStack.Push(track.segments[i]);
}
}
void Update()
{
// Move entire track backward at bike's current speed
_segParent.Translate(
Vector3.back *
bikeController.CurrentSpeed *
Time.deltaTime,
Space.World
);
}
Vector3.back = negative Z direction (toward player)CurrentSpeed = player's velocityTime.deltaTime = frame-independent movementusing UnityEngine;
public class SegmentMarker : MonoBehaviour
{
public TrackController trackController;
private void OnTriggerExit(Collider other)
{
// Did the bike pass through this marker?
if (other.GetComponent<BikeController>())
{
// Load 1 new segment ahead
trackController.LoadSegment(1);
// Destroy this entire segment
Destroy(transform.parent.gameObject);
}
}
}
SegmentPrefab (Parent)
โโโ Road Mesh
โโโ Obstacles (trees, barriers, etc.)
โโโ Decorations (lights, signs, etc.)
โโโ SegmentMarker (child)
โโโ Box Collider (Is Trigger = true)
โโโ SegmentMarker.cs script
Typical Setup:
Without Spatial Partitioning:
- 100 segments ร 10,000 polygons = 1,000,000 polys
- High memory usage, low FPS
With Spatial Partitioning:
- 3 segments ร 10,000 polygons = 30,000 polys
- 97% memory reduction! ๐
using UnityEngine;
using System.Collections.Generic;
public class TrackController : MonoBehaviour
{
public Track track;
public BikeController bikeController;
private Stack<GameObject> _segStack;
private Transform _segParent;
private float _zPos;
private int _segCount;
void Start()
{
_segParent = GameObject.Find("Track").transform;
_segStack = new Stack<GameObject>();
for (int i = track.segments.Count - 1; i >= 0; i--)
_segStack.Push(track.segments[i]);
LoadSegment(3);
}
void Update()
{
_segParent.Translate(
Vector3.back * bikeController.CurrentSpeed * Time.deltaTime,
Space.World
);
}
public void LoadSegment(int amount)
{
for (int i = 0; i < amount; i++)
{
if (_segStack.Count == 0)
RepopulateStack();
GameObject segment = Instantiate(
_segStack.Pop(),
Vector3.zero,
Quaternion.identity
);
segment.transform.SetParent(_segParent);
segment.transform.localPosition = new Vector3(0, 0, _zPos);
_zPos += track.segmentLength;
_segCount++;
}
}
private void RepopulateStack()
{
for (int i = track.segments.Count - 1; i >= 0; i--)
_segStack.Push(track.segments[i]);
}
}
// In LoadSegment():
Debug.Log($"Loaded segment {_segCount}, Stack count: {_segStack.Count}");
// In SegmentMarker.OnTriggerExit():
Debug.Log("Segment destroyed, loading next...");
// โ Wrong - segments load backwards!
for (int i = 0; i < track.segments.Count; i++)
_segStack.Push(track.segments[i]);
// โ
Correct - reverse for LIFO behavior
for (int i = track.segments.Count - 1; i >= 0; i--)
_segStack.Push(track.segments[i]);
transform.parent.gameObjectSegments always appear in same order = predictable tracks
private void RepopulateStack()
{
// Create shuffled copy of segments
List<GameObject> shuffled = new List<GameObject>(track.segments);
// Shuffle using Fisher-Yates algorithm
for (int i = shuffled.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
GameObject temp = shuffled[i];
shuffled[i] = shuffled[j];
shuffled[j] = temp;
}
// Push shuffled segments
for (int i = shuffled.Count - 1; i >= 0; i--)
_segStack.Push(shuffled[i]);
}
public class Track : ScriptableObject
{
public List<GameObject> easySegments;
public List<GameObject> mediumSegments;
public List<GameObject> hardSegments;
}
// In TrackController:
private void RepopulateStack()
{
List<GameObject> segments;
if (_segCount < 10)
segments = track.easySegments;
else if (_segCount < 30)
segments = track.mediumSegments;
else
segments = track.hardSegments;
// Push selected difficulty segments
for (int i = segments.Count - 1; i >= 0; i--)
_segStack.Push(segments[i]);
}
Creating and destroying segments every few seconds causes GC spikes.
// Instead of Instantiate:
GameObject segment = _segmentPool.GetObject();
segment.transform.SetParent(_segParent);
segment.transform.localPosition = new Vector3(0, 0, _zPos);
segment.SetActive(true);
// Instead of Destroy in SegmentMarker:
private void OnTriggerExit(Collider other)
{
if (other.GetComponent<BikeController>())
{
trackController.LoadSegment(1);
// Return to pool instead of destroying
_segmentPool.ReturnObject(transform.parent.gameObject);
}
}
Instead of pre-made segments, create them algorithmically.
private GameObject GenerateSegment()
{
GameObject segment = new GameObject("Generated Segment");
// Add road mesh based on rules
AddRoadMesh(segment, GetRandomCurve());
// Procedurally place obstacles
AddObstacles(segment, Random.Range(3, 8));
// Add decorations
AddScenery(segment);
return segment;
}
Track Length: 5000 units
Segment Count: 125 segments (40 units each)
Active Objects: 125 segments always rendered
Memory: ~500 MB
FPS: 15-20 (unplayable)
Track Length: Infinite
Segment Count: 3 active at a time
Active Objects: 3 segments dynamically loaded
Memory: ~15 MB
FPS: 60+ (smooth gameplay)
"Premature optimization is the root of all evil" - Donald Knuth
// Click on a frame spike to see:
- Hierarchy of function calls
- Time spent in each method
- Memory allocations per frame
Unlike previous lectures, this code is simplified and NOT production-ready.
// Add validation:
if (track == null || track.segments.Count == 0)
{
Debug.LogError("Track not configured!");
enabled = false;
return;
}
// Make configurable:
public int initialSegmentCount = 3;
private Queue<GameObject> _segQueue;
// Initialize (no reverse needed!)
for (int i = 0; i < track.segments.Count; i++)
_segQueue.Enqueue(track.segments[i]);
// Load segment
GameObject segment = Instantiate(_segQueue.Dequeue());
// Add debug logs:
Debug.Log($"Stack count: {_segStack.Count}");
Debug.Log($"Loading segment at Z: {_zPos}");
Debug.Log(bikeController.CurrentSpeed)// In Scene view, enable Gizmos
private void OnDrawGizmos()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireCube(transform.position,
new Vector3(10, 5, track.segmentLength));
}
Lecture 13: Additional Design Patterns - Command, Observer, State, and more!