Lecture 9: Visitor Pattern - Power-Ups

๐ŸŽฎ Visitor Pattern

Implementing Power-Ups

Game Programming - CSCI 3213

Spring 2026 - Lecture 9

Oklahoma City University

๐Ÿ“š Learning Objectives

  • Understand the Visitor design pattern
  • Learn when and why to use the Visitor pattern
  • Implement a power-up system using Visitor pattern
  • Combine Visitor pattern with Unity ScriptableObjects
  • Create designer-friendly power-up configuration

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

Classic Examples

  • Pac-Man (1980): Power Pellets - temporary invincibility
  • Super Mario Bros: Mushrooms - size and durability boost
  • Sonic: Shield, Speed Shoes, Invincibility
  • Modern Games: Complex multi-attribute boosts
Key Difference: Power-ups activate immediately on contact, while items are collected and activated later by player choice.

๐ŸŽฏ Design Requirements

Power-Up Specifications

  • Granularity: Boost multiple properties simultaneously
    • Example: "Protector" - Shield durability + Weapon strength
  • Permanence: Effects stack until maximum values reached
    • No temporal expiration
    • Benefits accumulate across pickups
  • Designer-Friendly: No code required to create new power-ups
  • Scalable: Easy to add new boost types

๐Ÿ” 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:

  • Power-up enters the bike's structure
  • Visits each component (engine, shield, weapon)
  • Enhances visited components
  • Original bike code remains unchanged

๐Ÿ—๏ธ 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

  • Open/Closed Principle:
    • Open for extension (new visitors)
    • Closed for modification (existing elements)
  • Single Responsibility:
    • Visitable objects hold data
    • Visitor objects define behaviors
  • Centralized Logic:
    • All power-up logic in one place
    • Easy to maintain and modify

โš ๏ธ Potential Drawbacks

  • Accessibility Issues:
    • Visitors may need access to private fields
    • Requires exposing more public properties
  • Structural Complexity:
    • More complex than simpler patterns (Singleton, State)
    • Requires understanding of Double Dispatch concept
  • Learning Curve:
    • Considered one of the harder patterns to grasp
    • Team members need pattern familiarity

๐Ÿ”„ 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

namespace Pattern.Visitor
{
    public interface IVisitor
    {
        // One Visit method per visitable element
        void Visit(BikeShield bikeShield);
        void Visit(BikeEngine bikeEngine);
        void Visit(BikeWeapon bikeWeapon);
    }
}
Pattern Rule: You need one Visit() method for each type of visitable element in your system.

๐Ÿ’ป Implementation: Visitable Interface

namespace Pattern.Visitor
{
    public interface IBikeElement
    {
        // Entry point for visitor
        void Accept(IVisitor visitor);
    }
}
Key Point: The Accept() method is the gateway that allows visitors to access the object's internal structure.

๐Ÿ’ป PowerUp ScriptableObject (1/2)

using UnityEngine;

namespace Pattern.Visitor
{
    [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

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

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

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.

๐Ÿ’ป BikeController - The Coordinator

using UnityEngine;
using System.Collections.Generic;

namespace Pattern.Visitor
{
    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>());
        }

๐Ÿ’ป 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

๐Ÿ’ป BikeShield - Visitable Component

using UnityEngine;

namespace Pattern.Visitor
{
    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!
        }

        // OnGUI for debug display...
    }
}

๐Ÿ’ป BikeWeapon - Visitable Component

using UnityEngine;

namespace Pattern.Visitor
{
    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);
        }
    }
}

๐Ÿ’ป BikeEngine - Visitable Component

using UnityEngine;

namespace Pattern.Visitor
{
    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);
        }
    }
}

๐Ÿ’ป Making Pickups Collectable

using UnityEngine;

public class Pickup : MonoBehaviour
{
    public PowerUp powerup;

    private void OnTriggerEnter(Collider other)
    {
        // Check if player bike entered trigger
        if (other.GetComponent<BikeController>())
        {
            // Apply power-up
            other.GetComponent<BikeController>().Accept(powerup);

            // Destroy pickup
            Destroy(gameObject);
        }
    }
}
Setup: Attach to GameObject with Collider (IsTrigger = true)

๐Ÿงช Testing the System

Quick Test Steps:

  1. Create a new Unity scene
  2. Add an empty GameObject
  3. Attach the ClientVisitor test script (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 ClientVisitor component
  7. Press Play and test buttons

๐Ÿ’ป ClientVisitor Test Script

using UnityEngine;

namespace Pattern.Visitor
{
    public class ClientVisitor : MonoBehaviour
    {
        public PowerUp enginePowerUp;
        public PowerUp shieldPowerUp;
        public PowerUp weaponPowerUp;

        private BikeController _bikeController;

        void Start()
        {
            _bikeController =
                gameObject.AddComponent<BikeController>();
        }

        void OnGUI()
        {
            if (GUILayout.Button("PowerUp Shield"))
                _bikeController.Accept(shieldPowerUp);
            if (GUILayout.Button("PowerUp Engine"))
                _bikeController.Accept(enginePowerUp);
            if (GUILayout.Button("PowerUp Weapon"))
                _bikeController.Accept(weaponPowerUp);
        }
    }
}

๐Ÿ”„ 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

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

public class PowerUp : ScriptableObject, IVisitor
{
    public float duration = 0f; // 0 = permanent

    // Store original values
    private Dictionary<IBikeElement, object> _originalValues;

    public void Visit(BikeEngine bikeEngine)
    {
        if (duration > 0)
        {
            // Store original
            _originalValues[bikeEngine] = bikeEngine.turboBoost;

            // Apply boost
            bikeEngine.turboBoost += turboBoost;

            // Schedule revert
            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:

  • Operating on complex object structures
  • Need to add operations without modifying classes
  • Operations are related but conceptually separate
  • Object structure rarely changes, but operations do

Avoid When:

  • Object structure changes frequently (adding new types)
  • Simple operations on simple structures
  • Team unfamiliar with pattern complexity
  • Simpler patterns (Strategy, Command) would suffice

โš–๏ธ 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).

๐ŸŒ Real-World Applications

Beyond Power-Ups:

  • Damage Systems:
    • Different damage types visit different armor components
    • Fire damage, ice damage, physical damage
  • Status Effects:
    • Poison, slow, stun effects visit character stats
    • Each effect has unique impact on different attributes
  • Buff/Debuff Systems:
    • Temporary or permanent stat modifications
    • Stackable or unique effects
  • Rendering Pipelines:
    • Shader visitors operate on mesh components
    • Post-processing effects visit render targets

โšก Performance Considerations

Optimization Tips:

  • Cache Component References:
    • Store _bikeElements list once in Start()
    • Avoid repeated GetComponent() calls
  • Object Pooling for Pickups:
    • Don't Destroy() - recycle instead
    • Reduces garbage collection overhead
  • Conditional Visiting:
    • Skip components if powerup doesn't affect them
    • Check flags before calling Visit()
// 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

๐Ÿงช Testing Strategy

Unit Test Scenarios:

[Test]
public void PowerUp_BoostsWeaponRange()
{
    // Arrange
    var weapon = new BikeWeapon { range = 5, maxRange = 25 };
    var powerup = CreatePowerUp(weaponRange: 10);

    // Act
    powerup.Visit(weapon);

    // Assert
    Assert.AreEqual(15, weapon.range);
}

[Test]
public void PowerUp_RespectsMaximumValues()
{
    // Arrange
    var engine = new BikeEngine { turboBoost = 180, maxTurboBoost = 200 };
    var powerup = CreatePowerUp(turboBoost: 50);

    // Act
    powerup.Visit(engine);

    // Assert - Should cap at max, not exceed
    Assert.AreEqual(200, engine.turboBoost);
}

๐Ÿ› Debugging Tips

Add Logging to Track Visits:

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:

  • Use OnGUI() to display current stat values
  • Add particle effects when power-up applied
  • Color-code UI elements based on boost level

๐Ÿ“ Summary

What We Learned:

  • โœ… Visitor pattern allows operations on object structures without modifying them
  • โœ… Perfect for power-up systems with multiple component types
  • โœ… Combines well with ScriptableObjects for designer-friendly workflows
  • โœ… Separates data (visitable components) from behavior (visitors)
  • โœ… Follows Open/Closed and Single Responsibility principles

Key Takeaways:

  • One Visit() method per visitable type
  • Accept() method is the entry point
  • BikeController coordinates the visiting process
  • ScriptableObjects enable no-code power-up creation

๐Ÿ’ช 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:

  • Design Patterns (Gang of Four): Original Visitor pattern description
  • Unity ScriptableObjects Documentation: Deep dive into asset-based design
  • Game Programming Patterns: Game-specific pattern applications

Code Examples:

  • Full implementation in course GitHub repo
  • FPP project folder has complete racing game example
  • Includes working pickups and visual effects

Next Lecture:

Strategy Pattern: Implementing AI behaviors for enemy drones

โ“ 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! ๐ŸŽฎ