Lecture 9: Visitor Pattern - Power-Ups

๐ŸŽฎ Visitor Pattern

Implementing Power-Ups

Game Programming - CSCI 3213

Spring 2026 - Lecture 9

๐Ÿ“š Learning Objectives

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.

๐Ÿ•น๏ธ Power-Ups: A Gaming Staple

Classic Examples

Key Difference: Power-ups activate immediately on contact, while items are collected and activated later by player choice.

๐ŸŽฏ Design Requirements

Power-Up Specifications

๐Ÿ” Understanding Visitor Pattern

Core Concept

A Visitable object permits a Visitor to operate on specific elements of its structure.

Key Benefit: Add new functionality to objects without modifying them

Real-World Analogy

Imagine a power-up flowing through a bike like an electric current:

๐Ÿ—๏ธ Pattern Structure

Key Participants

// 1. Visitor Interface public interface IVisitor { void Visit(BikeShield shield); void Visit(BikeEngine engine); void Visit(BikeWeapon weapon); } // 2. Visitable Interface public interface IBikeElement { void Accept(IVisitor visitor); }
Note: Each visitable type gets its own Visit() method

โœ… Benefits of Visitor Pattern

โš ๏ธ Potential Drawbacks

๐Ÿ”„ Double Dispatch

What is Double Dispatch?

A mechanism that delegates method calls to different concrete methods based on the runtime types of two objects.

In our power-up system:

  1. PowerUp visits BikeController
  2. BikeController dispatches visitor to components
  3. Each component calls correct Visit() method
  4. Method resolution uses types of both visitor and visited
Don't worry: You don't need to fully understand Double Dispatch to use the Visitor pattern effectively!

๐Ÿ’ป Implementation: Visitor Interface

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Patterns/IVisitor.cs
Interface that defines the visitor operations for each bike element type.
โš ๏ธ This code goes into your Unity project for Blade Racer.

public interface IVisitor { // One Visit method per visitable element void Visit(BikeShield bikeShield); void Visit(BikeEngine bikeEngine); void Visit(BikeWeapon bikeWeapon); }

interface - Defines a contract that visitor classes must implement

Method Overloading - Same method name Visit() with different parameter types

Type-Specific Logic - Each Visit() handles one component type differently

Adding a new bike component? Add a new Visit() method here.

๐Ÿ’ป Implementation: Visitable Interface

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Patterns/IBikeElement.cs
Interface for bike elements that can be visited by power-ups.
โš ๏ธ This code goes into your Unity project for Blade Racer.

public interface IBikeElement { // Entry point for visitor void Accept(IVisitor visitor); }

Accept(IVisitor) - Gateway method that lets visitors interact with this element

Double Dispatch Setup - Element receives visitor, then calls visitor.Visit(this)

BikeShield, BikeEngine, and BikeWeapon will all implement this interface.

๐Ÿ’ป PowerUp ScriptableObject (1/2)

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Items/PowerUp.cs
ScriptableObject that implements the visitor pattern to apply effects to bike elements.
โš ๏ธ This code goes into your Unity project for Blade Racer.

using UnityEngine; [CreateAssetMenu(fileName = "PowerUp", menuName = "PowerUp")] public class PowerUp : ScriptableObject, IVisitor { public string powerupName; public GameObject powerupPrefab; public string powerupDescription; [Tooltip("Fully heal shield")] public bool healShield; [Range(0.0f, 50f)] [Tooltip("Boost turbo settings up to 50 mph")] public float turboBoost; [Range(0.0f, 25)] [Tooltip("Boost weapon range up to 25 units")] public int weaponRange;

๐Ÿ’ป PowerUp ScriptableObject (2/2)

[Range(0.0f, 50f)] [Tooltip("Boost weapon strength up to 50%")] public float weaponStrength; // Visitor methods on next slides... }
ScriptableObject Benefits:
  • Create power-ups from Asset menu
  • Configure in Inspector (no code!)
  • Designer-friendly workflow

๐Ÿ’ป Visiting BikeShield

๐Ÿ“ Continuing PowerUp.cs

Add this Visit method to the PowerUp class from Slide 12.

public void Visit(BikeShield bikeShield) { if (healShield) bikeShield.health = 100.0f; }

What happens here?

  • PowerUp receives reference to BikeShield
  • Checks if healShield is enabled
  • If yes, restores shield to full health
  • Simple, focused behavior

๐Ÿ’ป Visiting BikeWeapon

๐Ÿ“ Continuing PowerUp.cs

Add this Visit method to the PowerUp class (second of three Visit methods).

public void Visit(BikeWeapon bikeWeapon) { // Boost range (respect max) int range = bikeWeapon.range += weaponRange; if (range >= bikeWeapon.maxRange) bikeWeapon.range = bikeWeapon.maxRange; else bikeWeapon.range = range; // Boost strength by percentage (respect max) float strength = bikeWeapon.strength += Mathf.Round(bikeWeapon.strength * weaponStrength / 100); if (strength >= bikeWeapon.maxStrength) bikeWeapon.strength = bikeWeapon.maxStrength; else bikeWeapon.strength = strength; }

๐Ÿ’ป Visiting BikeEngine

๐Ÿ“ Continuing PowerUp.cs

Add this Visit method to the PowerUp class (third and final Visit method).

public void Visit(BikeEngine bikeEngine) { float boost = bikeEngine.turboBoost += turboBoost; // Ensure non-negative if (boost < 0.0f) bikeEngine.turboBoost = 0.0f; // Respect maximum if (boost >= bikeEngine.maxTurboBoost) bikeEngine.turboBoost = bikeEngine.maxTurboBoost; }
Pattern Observation: Each Visit() method encapsulates the specific logic for modifying one component type.

๐Ÿ’ป BikeShield - Visitable Component

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Bike/BikeShield.cs
Bike shield component that can be visited by power-ups.
โš ๏ธ This code goes into your Unity project for Blade Racer.

using UnityEngine; public class BikeShield : MonoBehaviour, IBikeElement { public float health = 50.0f; // Percentage public float Damage(float damage) { health -= damage; return health; } public void Accept(IVisitor visitor) { visitor.Visit(this); // Double dispatch! } }

: IBikeElement - Implements the visitable interface

health - Public field the visitor can modify (restore to 100%)

Accept() - Calls visitor.Visit(this) passing itself

The this keyword enables double dispatch - visitor knows the exact type.

๐Ÿ’ป BikeWeapon - Visitable Component

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Bike/BikeWeapon.cs
Bike weapon component that can be visited by power-ups.
โš ๏ธ This code goes into your Unity project for Blade Racer.

using UnityEngine; public class BikeWeapon : MonoBehaviour, IBikeElement { [Header("Range")] public int range = 5; public int maxRange = 25; [Header("Strength")] public float strength = 25.0f; public float maxStrength = 50.0f; public void Fire() { Debug.Log("Weapon fired!"); } public void Accept(IVisitor visitor) { visitor.Visit(this); } }

[Header] - Unity attribute that groups fields in Inspector

range/maxRange - Visitor can boost range up to max

strength/maxStrength - Visitor can boost damage output

Max values prevent power-ups from creating overpowered weapons.

๐Ÿ’ป BikeEngine - Visitable Component

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Bike/BikeEngine.cs
Bike engine component that can be visited by power-ups.
โš ๏ธ This code goes into your Unity project for Blade Racer.

using UnityEngine; public class BikeEngine : MonoBehaviour, IBikeElement { public float turboBoost = 25.0f; // mph public float maxTurboBoost = 200.0f; private bool _isTurboOn; private float _defaultSpeed = 300.0f; public float CurrentSpeed { get { if (_isTurboOn) return _defaultSpeed + turboBoost; return _defaultSpeed; } } public void Accept(IVisitor visitor) { visitor.Visit(this); } }

turboBoost - Additional speed in mph, modified by visitor

maxTurboBoost - Cap prevents unlimited speed stacking

CurrentSpeed - Property calculates total speed from base + turbo

Same Accept() pattern - all three components work identically.

๐Ÿ’ป BikeController - The Coordinator

๐Ÿ“ File Structure Note - PRODUCTION CODE

Modify existing file: Assets/Scripts/Controllers/BikeController.cs
Add IBikeElement implementation and Accept method to coordinate visitor pattern.
โš ๏ธ This code goes into your Unity project for Blade Racer.

using UnityEngine; using System.Collections.Generic; public class BikeController : MonoBehaviour, IBikeElement { private List<IBikeElement> _bikeElements = new List<IBikeElement>(); void Start() { _bikeElements.Add( gameObject.AddComponent<BikeShield>()); _bikeElements.Add( gameObject.AddComponent<BikeWeapon>()); _bikeElements.Add( gameObject.AddComponent<BikeEngine>()); }

List<IBikeElement> - Holds all visitable bike components

Start() - Creates and registers all bike components

AddComponent - Dynamically adds components at runtime

BikeController is ALSO an IBikeElement - it coordinates visiting.

๐Ÿ’ป BikeController - Accept Method

public void Accept(IVisitor visitor) { // Forward visitor to all bike elements foreach (IBikeElement element in _bikeElements) { element.Accept(visitor); } } }

The Flow:

  1. Bike collides with power-up
  2. PowerUp calls BikeController.Accept()
  3. BikeController forwards visitor to all components
  4. Each component's Visit() method is called

๐Ÿ’ป Making Pickups Collectable

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Items/Pickup.cs
Pickup object that applies power-ups to the bike on collision.
โš ๏ธ This code goes into your Unity project for Blade Racer.

using UnityEngine; public class Pickup : MonoBehaviour { public PowerUp powerup; private void OnTriggerEnter(Collider other) { if (other.GetComponent<BikeController>()) { other.GetComponent<BikeController>() .Accept(powerup); Destroy(gameObject); } } }

OnTriggerEnter - Unity callback when colliders overlap (IsTrigger=true)

GetComponent - Checks if colliding object has BikeController

Accept(powerup) - PowerUp visits all bike components!

This is where the visitor pattern kicks in - one call affects multiple components.

๐Ÿงช Testing the System

Quick Test Steps:

  1. Create a new Unity scene
  2. Add an empty GameObject
  3. Attach the TestPanel script (updated on next slide)
  4. Create 3 PowerUp assets via Assets/Create/PowerUp
  5. Configure each PowerUp in Inspector:
    • Shield PowerUp: healShield = true
    • Engine PowerUp: turboBoost = 25
    • Weapon PowerUp: weaponRange = 5, weaponStrength = 10
  6. Drag PowerUp assets to TestPanel component
  7. Press Play and use buttons or keyboard shortcuts (V/E/W)

๐ŸŽฎ TestPanel: Visitor + Window Enhancements

๐Ÿ“ Evolving TestPanel.cs

Add Visitor Pattern section and window management features (close, minimize, resize) to TestPanel.cs.

Visitor Pattern Additions

// Add to TestPanel fields private bool _visitorExpanded = true; public PowerUp shieldPowerUp; public PowerUp enginePowerUp; public PowerUp weaponPowerUp; // Add to Update() - Visitor shortcuts if (_bikeController != null) { if (Input.GetKeyDown(KeyCode.V)) _bikeController.Accept(shieldPowerUp); if (Input.GetKeyDown(KeyCode.E)) _bikeController.Accept(enginePowerUp); if (Input.GetKeyDown(KeyCode.W)) _bikeController.Accept(weaponPowerUp); }
// Add DrawVisitorSection() void DrawVisitorSection() { if (_bikeController == null) return; GUI.backgroundColor = Color.red; _visitorExpanded = GUILayout.Toggle( _visitorExpanded, "โ–ผ Visitor Pattern", "button"); GUI.backgroundColor = Color.white; if (_visitorExpanded) { GUILayout.BeginVertical("box"); if (GUILayout.Button("Shield PowerUp (V)")) _bikeController.Accept(shieldPowerUp); if (GUILayout.Button("Engine PowerUp (E)")) _bikeController.Accept(enginePowerUp); if (GUILayout.Button("Weapon PowerUp (W)")) _bikeController.Accept(weaponPowerUp); GUILayout.EndVertical(); } }

Window Management Enhancements

// Add to fields private bool _showTestPanel = true; private bool _isResizing; private Vector2 _minSize = new Vector2(200, 100); private Vector2 _maxSize = new Vector2(400, 800); // Add to Update() - Toggle panel if (Input.GetKeyDown(KeyCode.BackQuote)) _showTestPanel = !_showTestPanel;
// Update OnGUI() void OnGUI() { if (!_showTestPanel) return; _windowRect = GUILayout.Window(0, _windowRect, DrawWindow, "Test Panel"); }
// Update DrawWindow() title bar GUILayout.BeginHorizontal(); if (GUILayout.Button( _isMinimized ? "+" : "-", GUILayout.Width(25))) _isMinimized = !_isMinimized; if (GUILayout.Button("X", GUILayout.Width(25))) _showTestPanel = false; GUILayout.EndHorizontal();
// Add at end of DrawWindow() // Resize handle (bottom-right) Rect resizeHandle = new Rect( _windowRect.width - 15, _windowRect.height - 15, 15, 15); GUI.DrawTexture(resizeHandle, Texture2D.whiteTexture); EditorGUIUtility.AddCursorRect( resizeHandle, MouseCursor.ResizeUpLeft); if (Event.current.type == EventType.MouseDown && resizeHandle.Contains( Event.current.mousePosition)) _isResizing = true; if (_isResizing) { _windowRect.width = Mathf.Clamp( Event.current.mousePosition.x, _minSize.x, _maxSize.x); _windowRect.height = Mathf.Clamp( Event.current.mousePosition.y, _minSize.y, _maxSize.y); } if (Event.current.type == EventType.MouseUp) _isResizing = false; GUI.DragWindow();
Keymap Update: Add to DrawKeymapWindow(): V = Shield PowerUp, E = Engine PowerUp, W = Weapon PowerUp, ` = Toggle Panel

๐Ÿ”„ The Complete Flow

Sequence of Events:

  1. Collision Detected: Bike enters pickup trigger
  2. Accept Called: Pickup calls BikeController.Accept(powerup)
  3. Distribution: BikeController forwards to all elements
  4. Visit Shield: powerup.Visit(bikeShield) called
    • Shield health restored if configured
  5. Visit Engine: powerup.Visit(bikeEngine) called
    • Turbo boost increased if configured
  6. Visit Weapon: powerup.Visit(bikeWeapon) called
    • Range and strength boosted if configured
  7. Cleanup: Pickup GameObject destroyed

๐ŸŽจ Designer-Friendly Workflow

Creating New Power-Ups (No Code!)

  1. Right-click in Project window
  2. Create โ†’ PowerUp
  3. Name it (e.g., "SuperBoost")
  4. Select the asset in Inspector
  5. Configure properties:
    • Power-Up Name: "Super Boost"
    • Description: "Massive speed and weapon upgrade"
    • Turbo Boost: 50
    • Weapon Range: 10
    • Weapon Strength: 25
  6. Assign to Pickup prefab
  7. Done! No programming required

๐Ÿ”ง Extending the System

Adding New Visitable Components

๐Ÿ“ File Structure Note - PRODUCTION CODE

New file: Assets/Scripts/Bike/BikeArmor.cs
Update: Assets/Scripts/Patterns/IVisitor.cs โ€” add Visit(BikeArmor) method
Update: Assets/Scripts/Items/PowerUp.cs โ€” implement Visit(BikeArmor)
โš ๏ธ This code goes into your Unity project for Blade Racer.

// 1. Create new component public class BikeArmor : MonoBehaviour, IBikeElement { public float armorRating = 0f; public float maxArmor = 100f; public void Accept(IVisitor visitor) { visitor.Visit(this); } } // 2. Add Visit method to IVisitor interface void Visit(BikeArmor bikeArmor); // 3. Implement in PowerUp class public void Visit(BikeArmor bikeArmor) { bikeArmor.armorRating += armorBoost; if (bikeArmor.armorRating > bikeArmor.maxArmor) bikeArmor.armorRating = bikeArmor.maxArmor; }

โฑ๏ธ Optional: Temporary Effects

Adding Duration to Power-Ups

๐Ÿ“ Modifying PowerUp.cs

Modify existing file: Assets/Scripts/Items/PowerUp.cs
Add a duration field and coroutine logic to revert effects after a set time.
โš ๏ธ This code goes into your Unity project for Blade Racer.

โš ๏ธ ScriptableObject Cannot Run Coroutines
ScriptableObject does not inherit from MonoBehaviour, so it has no StartCoroutine method. We borrow one from the scene using FindObjectOfType<BikeController>(). Also: initialize the dictionary with new, or you'll get a NullReferenceException on first use.
public class PowerUp : ScriptableObject, IVisitor { public float duration = 0f; // 0 = permanent // Must initialize โ€” ScriptableObject has no constructor call private Dictionary<IBikeElement, object> _originalValues = new Dictionary<IBikeElement, object>(); public void Visit(BikeEngine bikeEngine) { if (duration > 0) { // Store original _originalValues[bikeEngine] = bikeEngine.turboBoost; // Apply boost bikeEngine.turboBoost += turboBoost; // ScriptableObject can't StartCoroutine โ€” borrow a MonoBehaviour from the scene FindObjectOfType<BikeController>().StartCoroutine(RevertAfterDuration(bikeEngine)); } } IEnumerator RevertAfterDuration(BikeEngine engine) { yield return new WaitForSeconds(duration); engine.turboBoost = (float)_originalValues[engine]; } }

๐ŸŽญ Pattern Variations

Note on Implementation

Our implementation modifies visited objects' properties. This technically breaks a "purist" rule of the Visitor pattern.

Classic Visitor: Performs operations on elements without changing them (e.g., calculating totals, generating reports)

Our Approach: Uses Visitor structure to traverse and modify elements

Why this is okay:

  • Game development requires practical solutions
  • The traversal structure is what matters most
  • We maintain separation of concerns
  • System remains extensible and maintainable

๐Ÿค” When to Use Visitor Pattern

Good Use Cases:

Avoid When:

โš–๏ธ Visitor vs Other Patterns

Pattern Use Case Key Difference
Strategy Swap algorithms at runtime One object, many behaviors
Command Encapsulate requests as objects Focuses on actions, not traversal
Visitor Operate on object structure Traverses composite structures
Visitor shines when: You have a stable structure (bike components) but want to add many different operations (various power-up types).

Gaming History Moment ๐Ÿ•น๏ธ

Weird Consoles: Coleco Telstar Arcade (1977)

Before standardized controllers, console makers experimented wildly. The Coleco Telstar Arcade (1977) was a triangular console with three completely different detachable controllers: a steering wheel for racing games, a light gun for shooting games, and paddle controllers for Pong-style games. Each controller was a separate physical device that plugged into the same console.

The console had to handle input from radically different controller types - rotational input from the wheel, analog paddles, and point-and-click from the light gun. The system needed to "visit" each controller type and extract input in completely different ways, then translate that to game actions. This modularity allowed one console to support multiple game genres with specialized controls.

Connection to Visitor Pattern

The Telstar Arcade's controller system is like the Visitor Pattern! The console (visitor) needs to interact with different controller types (visitable elements) - steering wheel, light gun, paddles - each requiring different input handling. Just as our PowerUpVisitor visits different bike components (Engine, Shield, Weapon) with type-specific logic, the Telstar "visited" different controllers with controller-specific input parsing. The Visitor Pattern lets you add new operations (games) without modifying the controllers themselves!

๐ŸŒ Real-World Applications

Beyond Power-Ups:

โšก Performance Considerations

Optimization Tips:

// Example: Conditional visiting if (powerup.turboBoost > 0) element.Accept(powerup); // Only visit if needed

๐Ÿšซ Common Mistakes to Avoid

  1. Forgetting to Implement All Visit Methods:
    • IVisitor requires one method per visitable type
    • Compiler will catch this, but plan ahead
  2. Not Checking Maximum Values:
    • Always clamp boosted values to maximums
    • Prevents game-breaking stat inflation
  3. Tight Coupling in Visit Methods:
    • Don't access deep nested properties
    • Use public interface of visited objects
  4. Overusing the Pattern:
    • Not every system needs Visitor
    • Simple direct calls may be clearer

๐Ÿ› Debugging Tips

Add Logging to Track Visits:

๐Ÿ“ Debugging Additions

Add Debug.Log statements to existing files:
Assets/Scripts/Items/PowerUp.cs โ€” inside each Visit() method
BikeShield.cs, BikeWeapon.cs, BikeEngine.cs โ€” inside each Accept() method
โš ๏ธ Remove debug logging before final submission.

public void Visit(BikeWeapon bikeWeapon) { Debug.Log($"[PowerUp] Visiting weapon: " + $"Range {bikeWeapon.range} โ†’ {bikeWeapon.range + weaponRange}"); // Apply boost... } public void Accept(IVisitor visitor) { Debug.Log($"[{GetType().Name}] Accepting visitor: {visitor}"); visitor.Visit(this); }

Visual Debugging in Unity:

๐Ÿ“ Summary

What We Learned:

Key Takeaways:

๐Ÿ’ช Practice Exercise

Implement a "Debuff" System

Using the Visitor pattern, create a debuff system that weakens the bike:

Requirements:

  1. Create a Debuff class that implements IVisitor
  2. Debuff should reduce properties instead of increasing them:
    • Reduce turbo boost by percentage
    • Damage shield by fixed amount
    • Decrease weapon range
  3. Make it a ScriptableObject for easy configuration
  4. Create a "DebuffZone" trigger that applies debuff when entered
  5. Add visual feedback (red particle effect?)
Bonus: Make debuffs temporary using coroutines

๐Ÿ“š Additional Resources

Further Reading:

Code Examples:

Next Lecture:

Strategy Pattern: Implementing AI behaviors for enemy drones

๐Ÿ“ Homework Assignment

Submit a video which shows the following:

Recording Checklist (3-5 minutes)

Show Code: IVisitor, IBikeElement, PowerUp with all Visit methods, BikeController

Show Hierarchy: Bike GameObject with components, Pickup prefab

Demonstrate Power-Ups:

  • Apply Shield PowerUp (show health restore)
  • Apply Engine PowerUp (show turbo boost increase)
  • Apply Weapon PowerUp (show range/strength boost)

Show GUI: Shield health, turbo boost, weapon stats

Show Inspector: At least 3 configured PowerUp ScriptableObjects

Explain: How Visitor Pattern separates power-up logic from bike components

Point Breakdown

  • 20 pts: IVisitor interface with Visit() for all 3 component types
  • 15 pts: IBikeElement interface with Accept() method
  • 20 pts: PowerUp ScriptableObject implements all Visit() methods correctly
  • 15 pts: BikeShield, BikeWeapon, BikeEngine implement Accept() with double dispatch
  • 10 pts: BikeController coordinates visiting across all components
  • 10 pts: GUI displays current stats for all components
  • 10 pts: Video demonstrates all features with clear explanation

โ“ Questions?

Common Questions:

  • Q: Can a power-up visit only specific components?
    • A: Yes! Just check conditions before calling Visit() methods
  • Q: How do I make effects stackable or unique?
    • A: Track applied power-ups in a list, check before applying
  • Q: Can visitors access private fields?
    • A: Only through public interface - may need to expose properties
  • Q: Is this pattern overkill for simple power-ups?
    • A: Possibly! Use direct modification for 1-2 simple cases

Thank you! ๐ŸŽฎ