Game Programming - CSCI 3213
Spring 2026 - Lecture 9
Oklahoma City University
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.
namespace Pattern.Visitor
{
public interface IVisitor
{
// One Visit method per visitable element
void Visit(BikeShield bikeShield);
void Visit(BikeEngine bikeEngine);
void Visit(BikeWeapon bikeWeapon);
}
}
namespace Pattern.Visitor
{
public interface IBikeElement
{
// Entry point for visitor
void Accept(IVisitor visitor);
}
}
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;
[Range(0.0f, 50f)]
[Tooltip("Boost weapon strength up to 50%")]
public float weaponStrength;
// Visitor methods on next slides...
}
}
public void Visit(BikeShield bikeShield)
{
if (healShield)
bikeShield.health = 100.0f;
}
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;
}
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;
}
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>());
}
public void Accept(IVisitor visitor)
{
// Forward visitor to all bike elements
foreach (IBikeElement element in _bikeElements)
{
element.Accept(visitor);
}
}
}
}
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...
}
}
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);
}
}
}
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);
}
}
}
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);
}
}
}
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);
}
}
}
// 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 |
// 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! ๐ฎ