Combine Visitor pattern with Unity ScriptableObjects
Create designer-friendly power-up configuration
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
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.
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:
PowerUp visits BikeController
BikeController dispatches visitor to components
Each component calls correct Visit() method
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.
publicinterfaceIVisitor
{
// One Visit method per visitable elementvoidVisit(BikeShield bikeShield);
voidVisit(BikeEngine bikeEngine);
voidVisit(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.
publicinterfaceIBikeElement
{
// Entry point for visitorvoidAccept(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")]
publicclass PowerUp : ScriptableObject, IVisitor
{
publicstring powerupName;
public GameObject powerupPrefab;
publicstring powerupDescription;
[Tooltip("Fully heal shield")]
publicbool healShield;
[Range(0.0f, 50f)]
[Tooltip("Boost turbo settings up to 50 mph")]
publicfloat turboBoost;
[Range(0.0f, 25)]
[Tooltip("Boost weapon range up to 25 units")]
publicint weaponRange;
๐ป PowerUp ScriptableObject (2/2)
[Range(0.0f, 50f)]
[Tooltip("Boost weapon strength up to 50%")]
publicfloat 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.
publicvoidVisit(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).
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.
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.
[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.
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;
publicclass BikeController : MonoBehaviour, IBikeElement
{
privateList<IBikeElement> _bikeElements =
new List<IBikeElement>();
voidStart()
{
_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
publicvoidAccept(IVisitor visitor)
{
// Forward visitor to all bike elementsforeach (IBikeElement element in _bikeElements)
{
element.Accept(visitor);
}
}
}
The Flow:
Bike collides with power-up
PowerUp calls BikeController.Accept()
BikeController forwards visitor to all components
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.
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 fieldsprivate bool _visitorExpanded = true;
public PowerUp shieldPowerUp;
public PowerUp enginePowerUp;
public PowerUp weaponPowerUp;
// Add to Update() - Visitor shortcutsif (_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()voidDrawVisitorSection()
{
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 fieldsprivate bool _showTestPanel = true;
private bool _isResizing;
private Vector2 _minSize = new Vector2(200, 100);
private Vector2 _maxSize = new Vector2(400, 800);
// Add to Update() - Toggle panelif (Input.GetKeyDown(KeyCode.BackQuote))
_showTestPanel = !_showTestPanel;
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.
publicclass PowerUp : ScriptableObject, IVisitor
{
publicfloat duration = 0f; // 0 = permanent// Must initialize โ ScriptableObject has no constructor callprivate Dictionary<IBikeElement, object> _originalValues
= new Dictionary<IBikeElement, object>();
publicvoidVisit(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 returnnewWaitForSeconds(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).
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:
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 visitingif (powerup.turboBoost > 0)
element.Accept(powerup); // Only visit if needed
๐ซ Common Mistakes to Avoid
Forgetting to Implement All Visit Methods:
IVisitor requires one method per visitable type
Compiler will catch this, but plan ahead
Not Checking Maximum Values:
Always clamp boosted values to maximums
Prevents game-breaking stat inflation
Tight Coupling in Visit Methods:
Don't access deep nested properties
Use public interface of visited objects
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.