Game Programming - CSCI 3213
Spring 2026 - Lecture 12
Oklahoma City University
As we implement this system, we'll be creating multiple files in a specific order. You will see errors in Unity and your IDE until all files are complete.
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.
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
Create a new file: Assets/Scripts/Configs/Track.cs
ScriptableObject that defines track configuration and segments.
โ ๏ธ This code goes into your Unity project for Blade Racer.
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 from
Create a new file: Assets/Scripts/Controllers/TrackController.cs
MonoBehaviour that manages segment loading and unloading.
โ ๏ธ This code goes into your Unity project for Blade Racer.
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>();
// 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 movement
Create a new file: Assets/Scripts/Markers/SegmentMarker.cs
Component that triggers segment loading/cleanup on collision.
โ ๏ธ This code goes into your Unity project for Blade Racer.
using 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));
}
In 2009, Markus "Notch" Persson created Minecraft - a game with procedurally generated infinite worlds. The technical challenge was massive: how do you render a world that's literally endless? Notch's solution: spatial partitioning through chunks. The world is divided into 16x256x16 block chunks that load and unload based on player position.
When you move, Minecraft loads chunks ahead and unloads chunks behind. Only visible chunks are kept in memory and rendered. This chunk system - a spatial hash grid - allows virtually unlimited worlds on modest hardware. The "render distance" setting controls how many chunks load around you. 8 chunks = 128 block radius. 32 chunks = 512 block radius. Players with powerful PCs see further; weaker systems stay performant with smaller grids.
Minecraft's chunk system is THE textbook example of Spatial Partitioning in action! Just like our RaceSegmentManager divides a race track into segments and loads/unloads them based on player position, Minecraft divides the infinite world into 16x16 chunks and manages them spatially. Both systems optimize by only processing nearby areas. Without spatial partitioning, Minecraft would try to render millions of blocks simultaneously - impossible! Chunks make infinity playable.
Lecture 13: Additional Design Patterns - Command, Observer, State, and more!