Game Programming - CSCI 3213
Spring 2026 - Lecture 9
Oklahoma City University
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.
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.
A Visitable object permits a Visitor to operate on specific elements of its structure.
Key Benefit: Add new functionality to objects without modifying them
Imagine a power-up flowing through a bike like an electric current:
// 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);
}
A mechanism that delegates method calls to different concrete methods based on the runtime types of two objects.
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);
}
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);
}
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;
[Range(0.0f, 50f)]
[Tooltip("Boost weapon strength up to 50%")]
public float weaponStrength;
// Visitor methods on next slides...
}
Add this Visit method to the PowerUp class from Slide 12.
public void Visit(BikeShield bikeShield)
{
if (healShield)
bikeShield.health = 100.0f;
}
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;
}
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;
}
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>());
}
public void Accept(IVisitor visitor)
{
// Forward visitor to all bike elements
foreach (IBikeElement element in _bikeElements)
{
element.Accept(visitor);
}
}
}
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!
}
// OnGUI for debug display...
}
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);
}
}
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);
}
}
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)
{
// Check if player bike entered trigger
if (other.GetComponent<BikeController>())
{
// Apply power-up
other.GetComponent<BikeController>().Accept(powerup);
// Destroy pickup
Destroy(gameObject);
}
}
}
Create a new file: Assets/Scripts/Testing/ClientVisitor.cs
Test client that applies power-ups via GUI buttons.
โ ๏ธ This is temporary testing code - you can remove it after testing your implementation.
using UnityEngine;
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);
}
}
TestPanel.cs to combine all test controls into one draggable window.
See the Event Bus lecture for the unified TestPanel implementation.
Add Visitor Pattern keyboard shortcuts and section to your TestPanel.cs.
DrawKeymapWindow():V = Shield PowerUp, E = Engine PowerUp, W = Weapon PowerUp
// 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;
}
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];
}
}
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
| 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 |
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.
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!
// Example: Conditional visiting
if (powerup.turboBoost > 0)
element.Accept(powerup); // Only visit if needed
[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);
}
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);
}
Using the Visitor pattern, create a debuff system that weakens the bike:
Debuff class that implements IVisitorStrategy Pattern: Implementing AI behaviors for enemy drones
Thank you! ๐ฎ