Lecture 12: Spatial Partition - Level Editor

๐Ÿ—บ๏ธ Spatial Partition

Implementing a Level Editor

Game Programming - CSCI 3213

Spring 2026 - Lecture 12

๐Ÿ“š Learning Objectives

Developer's Note: "Never Panic Early"

โš ๏ธ What to Expect During Implementation

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.

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.

๐Ÿงฉ Spatial Partitioning Overview

Definition

Spatial partitioning is an optimization technique that divides game space into manageable regions to improve performance.

Key Benefits:

Note: This is an optimization technique, not a Gang of Four design pattern!

๐Ÿ” Partitioning Techniques

1. Grid Partitioning (Uniform Grid)

2. Binary Space Partitioning (BSP)

3. Quadtree (2D) / Octree (3D)

4. Segment-Based (Our Approach)

๐Ÿ Level Editor Design Goals

Challenge: Building Infinite Tracks

Racing games need long, varied tracks without consuming massive memory.

Our Solution:

Key Insight: Player bike stays stationary; track moves toward player!

๐Ÿ“š Stack Fundamentals (LIFO)

Last In, First Out (LIFO)

A stack is a collection where the last item added is the first one removed.

Core Operations:

C# Example:

Stack<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

Stack<T> โ€” generic collection, T is the type of items stored. GameObject means it holds Unity scene objects.

new Stack<>() โ€” creates an empty stack. Items added/removed from the "top".

Push() โ€” adds to top. Pop() โ€” removes and returns the top item. LIFO order.

Think of it like a stack of pancakes โ€” you always grab from the top.

Why Stack? Segments loaded in reverse order = natural LIFO behavior

๐Ÿ—๏ธ System Architecture

Three Core Components:

  1. Track ScriptableObject
    • Stores segment prefabs
    • Defines segment length
    • Designer-friendly configuration
  2. TrackController
    • Manages segment loading/positioning
    • Uses Stack to store segment queue
    • Responds to player progress
  3. SegmentMarker
    • Triggers when player passes segment
    • Signals cleanup to destroy old segments
    • Requests new segment loading

๐Ÿ“ฆ Track ScriptableObject

๐Ÿ“ File Structure Note - PRODUCTION CODE

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.

Designer-Friendly Level Configuration

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

[CreateAssetMenu] โ€” Unity attribute that adds a "Create โ†’ Track" right-click menu in the Project panel.

: ScriptableObject โ€” inherits from Unity's data container class. Lives as a .asset file, not in a scene.

fileName โ€” default name when the asset is created. menuName โ€” the path in the Create menu.

List<GameObject> โ€” ordered collection of segment prefabs. Designers drag prefabs into this list.

No MonoBehaviour, no scene object โ€” ScriptableObject data lives in the Project panel as a reusable asset.

How It Works:

Workflow: Designers create Track assets, add prefabs, no code needed!

๐ŸŽจ Designer Workflow

Steps to Create a Track:

  1. Right-click in Project: Create โ†’ Track
  2. Name the asset: "DesertTrack", "CityTrack", etc.
  3. Set segment length: Usually 40-60 units
  4. Add segment prefabs: Drag prefabs into list
    • Straight segments
    • Curved segments
    • Obstacles
    • Special features
  5. Assign to TrackController: Drag Track asset to controller

๐Ÿ“ธ Screenshot: Unity Project panel showing right-click โ†’ Create โ†’ Track menu

Variety: More prefabs = more track variation!

๐ŸŽฎ TrackController Class (Setup)

๐Ÿ“ File Structure Note - PRODUCTION CODE

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

Fields โ€” track and bikeController are public Inspector references. _segStack holds the segment prefabs to spawn. _zPos tracks where the next segment should be placed in world space.

GameObject.Find("Track") โ€” locates the "Track" empty GameObject in the scene by name. Returns its Transform so we can parent segments to it.

Reverse loop โ€” iterates the segment list from the end to index 0. Because Stack is LIFO, pushing in reverse means Pop() later returns items in original list order.

LoadSegment(3) โ€” called in Start() to pre-load the first 3 segments before gameplay begins.

The reverse push is the key trick โ€” think of loading a magazine backwards so bullets fire in the right order.

๐Ÿ”„ Stack Loading Strategy

The Reverse Order Problem

Challenge: We want segments to appear in list order, but Stack is LIFO (Last In, First Out).

Solution: Load in Reverse!

// 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! โœ…
Key Insight: Reversing during initialization gives us correct order during gameplay

๐Ÿ“ฅ LoadSegment Method

public 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++; } } }

for (int i = 0; i < amount; i++) โ€” loops amount times, creating one segment per iteration. Calling LoadSegment(3) at start loads the first 3.

Instantiate(prefab, position, rotation) โ€” spawns a copy of the prefab at world origin. Position and rotation are set manually right after.

SetParent(_segParent) โ€” makes the new segment a child of the "Track" GameObject so all segments move together.

localPosition โ€” sets position relative to the parent. _zPos advances by segmentLength each iteration, placing segments end-to-end.

_zPos is the running "cursor" โ€” it marks where the next segment's front edge should begin.

๐Ÿ“ Positioning Segments

Track Layout Example

// 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

Visual Representation:

Player (stationary at Z=0) โ†“ [Segment 0]โ”€โ”€โ”€โ”€โ”€[Segment 1]โ”€โ”€โ”€โ”€โ”€[Segment 2] Z=0 Z=40 Z=80 Track moves BACKWARD toward player
Remember: Player doesn't move; track comes to player!

โ™ป๏ธ Refilling the Stack

Repopulate When Empty

public void LoadSegment(int amount) { for (int i = 0; i < amount; i++) { // Refill stack if empty before popping 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]); }

if (_segStack.Count == 0) โ€” checked before every Pop. If the stack is empty we refill it first, then continue. This replaces the if (Count > 0) guard from the previous slide โ€” instead of skipping, we guarantee the segment is always loaded.

RepopulateStack() โ€” re-pushes all segments from the Track asset (in reverse) so the cycle begins again. The track "repeats" automatically.

Infinite loop โ€” every time the player exhausts all segments, the stack silently refills. From the player's view the track never ends.

This is the heart of the infinite-track illusion โ€” the cycle is invisible to the player.

Result: Infinite track by cycling through segments!

๐Ÿšด Moving the Track

Track Moves Toward Player

void Update() { // Move entire track backward at bike's current speed _segParent.Translate( Vector3.back * bikeController.CurrentSpeed * Time.deltaTime, Space.World ); }

Translate(direction, Space.World) โ€” moves the Transform by a vector each frame. Space.World means the direction is in world coordinates, not local.

Vector3.back โ€” shorthand for (0, 0, -1). Moving the track in the negative Z direction makes it appear to come toward the player.

bikeController.CurrentSpeed * Time.deltaTime โ€” frame-rate independent speed. Faster bike = track moves faster.

_segParent โ€” moving the parent moves all child segments together in one call.

The player's bike never moves. The entire track slides toward the camera โ€” a classic racing game illusion.

How It Works:

Illusion: Player feels like they're racing forward!

๐Ÿšฉ SegmentMarker Class

๐Ÿ“ File Structure Note - PRODUCTION CODE

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.

Automatic Segment Cleanup

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

OnTriggerExit(Collider other) โ€” Unity callback fired when another collider leaves a trigger zone. "Exit" fires after the bike fully passes through, not when it enters.

GetComponent<BikeController>() โ€” checks if the exiting object has a BikeController. Returns null if not, so non-bike colliders (environment, etc.) are ignored.

LoadSegment(1) โ€” tells TrackController to spawn the next segment ahead. One in = one out keeps 3 segments active at all times.

Destroy(transform.parent.gameObject) โ€” destroys the parent segment prefab, not just the marker. Since marker is a child, this removes the whole segment in one call.

The marker is a "trip wire" โ€” when the bike crosses it, new content loads ahead and old content disappears behind.

How to Set Up:

๐Ÿงฑ Segment Prefab Anatomy

Hierarchy:

SegmentPrefab (Parent) โ”œโ”€โ”€ Road Mesh โ”œโ”€โ”€ Obstacles (trees, barriers, etc.) โ”œโ”€โ”€ Decorations (lights, signs, etc.) โ””โ”€โ”€ SegmentMarker (child) โ”œโ”€โ”€ Box Collider (Is Trigger = true) โ””โ”€โ”€ SegmentMarker.cs script

๐Ÿ“ธ Screenshot: Unity Hierarchy showing SegmentPrefab with SegmentMarker as a child object with Box Collider (Is Trigger checked)

Marker Positioning:

Important: Marker must be CHILD of segment so both destroy together

๐Ÿ’พ Memory Efficiency

How Many Segments in Memory?

Typical Setup:

Memory Comparison:

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! ๐ŸŽ‰
Scalability: Can create "infinite" tracks with minimal memory

๐Ÿ“‹ TrackController โ€” Fields, Start & Update

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

Class structure โ€” TrackController is a MonoBehaviour. Two public fields for Inspector assignment; four private fields for internal state.

Start() โ€” one-time setup: find the Track parent, create the Stack, reverse-push all segments, pre-load 3.

Update() โ€” every frame: slide the entire track toward the player using the bike's current speed.

Start sets up the board; Update runs the game loop.

๐Ÿ“‹ TrackController Methods

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

LoadSegment(int amount) โ€” public so SegmentMarker can call it. Loops amount times; refills stack if empty before each pop.

Instantiate โ†’ SetParent โ†’ localPosition โ€” the three-step pattern for every new segment: create it, parent it, place it at _zPos, then advance _zPos.

RepopulateStack() โ€” private helper; re-adds all segments in reverse order so the track cycles endlessly.

These two methods are the engine of the whole system โ€” everything else just calls them.

โš™๏ธ Unity Setup Steps

1. Create Track Parent

2. Create Track ScriptableObject

3. Add TrackController Script

4. Configure Segment Prefabs

๐Ÿ“ธ Screenshot: Unity Inspector showing TrackController component with Track asset and BikeController reference assigned

๐Ÿงช Testing Your Track

What to Verify:

  1. Initial Load: 3 segments appear at start
  2. Track Movement: Track moves backward smoothly
  3. Segment Loading: New segment appears when passing marker
  4. Cleanup: Old segment destroys after passing
  5. Stack Refill: Segments cycle when stack empties
  6. Performance: Consistent FPS with only 3 segments active

Debug Tips:

// In LoadSegment(): Debug.Log($"Loaded segment {_segCount}, Stack count: {_segStack.Count}"); // In SegmentMarker.OnTriggerExit(): Debug.Log("Segment destroyed, loading next...");

โš ๏ธ Common Pitfalls

1. Forgetting Reverse Order

// โŒ 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]);

2. Marker Not a Child

3. Trigger Not Set

๐ŸŽฒ Adding Randomization

Problem:

Segments always appear in same order = predictable tracks

Solution: Random Selection

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

Fisher-Yates Shuffle โ€” a well-known algorithm that produces an unbiased random permutation. Each element swaps with a random element at or before its position.

new List<>(track.segments) โ€” creates a copy of the original list so the Track asset's order is preserved for next time.

Random.Range(0, i + 1) โ€” picks a random index from 0 to i (inclusive). The + 1 is because Range's max is exclusive.

temp swap โ€” classic three-variable swap: save one value, overwrite it, restore from temp. No built-in swap in C#.

Pushing the shuffled list in reverse maintains LIFO order โ€” same trick as initialization, applied to the shuffled copy.

๐Ÿ“ˆ Progressive Difficulty

Increase Challenge Over Time

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

Three segment lists โ€” easySegments, mediumSegments, hardSegments replace the single segments list. Designers fill each list with appropriately themed prefabs.

_segCount โ€” the running total of segments spawned. Used as a proxy for "how far into the game" the player is.

if / else if / else โ€” selects the difficulty tier based on segment count. < 10 = easy start, < 30 = mid-game, else = late game. Tune these thresholds per game.

This is a simple but effective difficulty ramp โ€” no separate level system needed.

โ™ป๏ธ Object Pool Integration

Problem: Instantiate/Destroy is Expensive

Creating and destroying segments every few seconds causes GC spikes.

Solution: Pool Segments

// Add to TrackController fields: public ObjectPool _segmentPool; // Assign your pool in Inspector // Then in LoadSegment, 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); } }

๐ŸŽจ Procedural Track Generation

Beyond Prefabs: Generate Segments

Instead of pre-made segments, create them algorithmically.

Advantages:

Concept:

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; }
Examples: No Man's Sky, Minecraft, Spelunky

๐ŸŒ Beyond Racing Tracks

Where Else to Use Spatial Partitioning?

1. Open World Games

2. Multiplayer Games

3. Physics Optimization

4. AI Perception

5. Rendering Culling

๐Ÿ”ง Unity's Spatial Tools

Unity Already Uses Spatial Partitioning!

1. Occlusion Culling

2. LOD (Level of Detail) Groups

3. Lightmap UVs

4. Physics Layers

Takeaway: Learn from Unity's optimizations!

๐Ÿ“Š Performance Metrics

Before Spatial Partitioning:

Track Length: 5000 units Segment Count: 125 segments (40 units each) Active Objects: 125 segments always rendered Memory: ~500 MB FPS: 15-20 (unplayable)

After Spatial Partitioning:

Track Length: Infinite Segment Count: 3 active at a time Active Objects: 3 segments dynamically loaded Memory: ~15 MB FPS: 60+ (smooth gameplay)

Key Improvements:

๐Ÿšซ When to Skip Spatial Partitioning

Not Always Necessary!

Skip if:

Premature Optimization Warning:

"Premature optimization is the root of all evil" - Donald Knuth
Best Practice: Profile first, optimize only if needed

๐Ÿ“ˆ Unity Profiler

Measure Before Optimizing

Opening the Profiler:

What to Look For:

Profiler Deep Dive:

// Click on a frame spike to see: - Hierarchy of function calls - Time spent in each method - Memory allocations per frame

๐Ÿ” Code Review Observations

This Was a Code Review Lecture!

Unlike previous lectures, this code is simplified and NOT production-ready.

Missing Elements:

Production Improvements:

// Add validation: if (track == null || track.segments.Count == 0) { Debug.LogError("Track not configured!"); enabled = false; return; } // Make configurable: public int initialSegmentCount = 3;

๐Ÿš€ Advanced Features

Ideas to Expand the Level Editor:

1. Branching Paths

2. Vertical Segments

3. Special Events

4. Weather/Time Transitions

5. Track Editor UI

๐ŸŽฎ Games Using Spatial Partitioning

1. Temple Run / Subway Surfers

2. Mario Kart Series

3. Minecraft

4. Grand Theft Auto V

5. Dark Souls

๐Ÿ“š Stack vs Queue Comparison

Why Stack Instead of Queue?

Stack (LIFO):

Queue (FIFO):

Queue Alternative:

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

Queue<GameObject> โ€” FIFO collection. First item enqueued is the first item dequeued. No reverse-push needed.

Enqueue(item) โ€” adds to the back of the queue. Items come out in the same order they went in.

Dequeue() โ€” removes and returns the front item. Compare to Stack's Pop().

Why Stack then? โ€” Stack was chosen so students learn LIFO. Queue is arguably simpler for this use case; both are valid.

Queue reads more naturally for sequential loading โ€” but understanding why Stack works here reinforces how LIFO behaves.

Either works! Stack chosen for learning LIFO concept

๐Ÿ› Debugging Your Track System

Common Issues and Solutions:

1. Segments Not Loading

// Add debug logs: Debug.Log($"Stack count: {_segStack.Count}"); Debug.Log($"Loading segment at Z: {_zPos}");

2. Track Not Moving

3. Segments Not Destroying

4. Visual Debugging

// Add to TrackController โ€” uses 'track' field private void OnDrawGizmos() { Gizmos.color = Color.yellow; Gizmos.DrawWireCube(transform.position, new Vector3(10, 5, track.segmentLength)); }

โœ… Best Practices

Design Guidelines:

  1. Profile First: Measure before optimizing
  2. Consistent Segment Size: Makes calculations predictable
  3. Tune Loading Distance: Load far enough ahead to avoid pop-in
  4. Pool Objects: Combine with Object Pool pattern
  5. Use ScriptableObjects: Designer-friendly configuration
  6. Add Visual Feedback: Loading spinner during heavy operations
  7. Test Edge Cases: Empty lists, null references
  8. Document Decisions: Why Stack? Why 3 segments?
Remember: Spatial partitioning is about performance, not patterns

Gaming History Moment ๐Ÿ•น๏ธ

Indie Renaissance: Minecraft's Chunk System (2011)

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.

Connection to Spatial Partition Pattern

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.

Learn More: Minecraft: The Story of Mojang (Documentary) | Blood, Sweat, and Pixels by Jason Schreier

๐Ÿ’ช Practice Challenge

Build Your Own Level Editor

Requirements:

  1. Create 3 different segment prefabs (straight, left curve, right curve)
  2. Build a Track ScriptableObject with all 3 segments
  3. Implement TrackController with Stack-based loading
  4. Add SegmentMarker to each prefab for cleanup
  5. Make track move at 20 units/second
  6. Add randomization to RepopulateStack()

Bonus Challenges:

๐ŸŽ“ Lecture Summary

What We Learned:

Key Takeaways:

Next Lecture:

Lecture 13: Additional Design Patterns - Command, Observer, State, and more!