Game Programming - CSCI 3213
Spring 2026 - Lecture 11
Oklahoma City University
Every bike comes with a front-mounted laser weapon. Players can purchase attachments to boost weapon stats.
Add new functionality to an existing object without altering it, by creating a decorator class that wraps the original.
Decorator wraps an object and enhances its behavior without changing its structure.
Think of a smartphone:
IWeaponWeaponWeaponDecoratorWeaponAttachment instances| Aspect | Inheritance | Decorator Pattern |
|---|---|---|
| Timing | Compile-time (static) | Runtime (dynamic) |
| Flexibility | Fixed class hierarchy | Mix and match behaviors |
| Combination | Limited by class design | Unlimited combinations |
| Reversibility | Can't "unextend" a class | Can remove decorators |
Weapon โ StrongWeapon โ StrongFastWeapon โ StrongFastLongRangeWeapon
Need a new class for every combination!
Weapon + Injector + Cooler + Stabilizer (runtime composition)
Traditional Decorator: Relies heavily on constructors
// Standard C# approach
IWeapon weapon = new Weapon();
weapon = new InjectorDecorator(weapon);
weapon = new CoolerDecorator(weapon);
Adapt the pattern to use Unity's ScriptableObject system while preserving Decorator benefits!
namespace Chapter.Decorator
{
public interface IWeapon
{
float Range { get; }
float Rate { get; } // Fire rate
float Strength { get; } // Damage
float Cooldown { get; } // Cool-down duration
}
}
Weapon and WeaponDecorator implement itusing UnityEngine;
namespace Chapter.Decorator
{
[CreateAssetMenu(fileName = "NewWeaponConfig",
menuName = "Weapon/Config", order = 1)]
public class WeaponConfig : ScriptableObject, IWeapon
{
[Range(0, 60)]
[Tooltip("Rate of firing per second")]
[SerializeField] private float rate;
[Range(0, 50)]
[Tooltip("Weapon range")]
[SerializeField] private float range;
[Range(0, 100)]
[Tooltip("Weapon strength")]
[SerializeField] private float strength;
[Range(0, 5)]
[Tooltip("Cooldown duration")]
[SerializeField] private float cooldown;
public string weaponName;
public GameObject weaponPrefab;
public string weaponDescription;
// IWeapon implementation
public float Rate { get { return rate; } }
public float Range { get { return range; } }
public float Strength { get { return strength; } }
public float Cooldown { get { return cooldown; } }
}
}
namespace Chapter.Decorator
{
public class Weapon : IWeapon
{
private readonly WeaponConfig _config;
// Constructor - initializes with config
public Weapon(WeaponConfig weaponConfig)
{
_config = weaponConfig;
}
// IWeapon implementation - pass through to config
public float Range { get { return _config.Range; } }
public float Rate { get { return _config.Rate; } }
public float Strength { get { return _config.Strength; } }
public float Cooldown { get { return _config.Cooldown; } }
}
}
namespace Chapter.Decorator
{
public class WeaponDecorator : IWeapon
{
private readonly IWeapon _decoratedWeapon;
private readonly WeaponAttachment _attachment;
public WeaponDecorator(
IWeapon weapon, WeaponAttachment attachment)
{
_attachment = attachment;
_decoratedWeapon = weapon;
}
// Enhance properties by adding attachment values
public float Rate
{
get { return _decoratedWeapon.Rate + _attachment.Rate; }
}
public float Range
{
get { return _decoratedWeapon.Range + _attachment.Range; }
}
public float Strength
{
get { return _decoratedWeapon.Strength + _attachment.Strength; }
}
public float Cooldown
{
get { return _decoratedWeapon.Cooldown + _attachment.Cooldown; }
}
}
}
IWeapon (could be Weapon or another decorator!)Weapon โ Injector โ Coolerusing UnityEngine;
namespace Chapter.Decorator
{
[CreateAssetMenu(fileName = "NewWeaponAttachment",
menuName = "Weapon/Attachment", order = 1)]
public class WeaponAttachment : ScriptableObject, IWeapon
{
[Range(0, 50)]
[Tooltip("Increase rate of firing per second")]
[SerializeField] private float rate;
[Range(0, 50)]
[Tooltip("Increase weapon range")]
[SerializeField] private float range;
[Range(0, 100)]
[Tooltip("Increase weapon strength")]
[SerializeField] private float strength;
[Range(0, -5)] // Negative values reduce cooldown!
[Tooltip("Reduce cooldown duration")]
[SerializeField] private float cooldown;
public string attachmentName;
public GameObject attachmentPrefab;
public string attachmentDescription;
// IWeapon implementation
public float Rate { get { return rate; } }
public float Range { get { return range; } }
public float Strength { get { return strength; } }
public float Cooldown { get { return cooldown; } }
}
}
using UnityEngine;
using System.Collections;
namespace Chapter.Decorator
{
public class BikeWeapon : MonoBehaviour
{
public WeaponConfig weaponConfig;
public WeaponAttachment mainAttachment;
public WeaponAttachment secondaryAttachment;
private bool _isFiring;
private IWeapon _weapon;
private bool _isDecorated;
void Start()
{
// Initialize base weapon
_weapon = new Weapon(weaponConfig);
}
public void Decorate()
{
// One attachment
if (mainAttachment && !secondaryAttachment)
{
_weapon = new WeaponDecorator(_weapon, mainAttachment);
}
// Two attachments - CHAINING!
if (mainAttachment && secondaryAttachment)
{
_weapon = new WeaponDecorator(
new WeaponDecorator(_weapon, mainAttachment),
secondaryAttachment);
}
_isDecorated = !_isDecorated;
}
Notice how we wrap a decorator with another decorator! This is the power of the pattern.
public void Reset()
{
// Remove all decorators - back to base weapon
_weapon = new Weapon(weaponConfig);
_isDecorated = !_isDecorated;
}
public void ToggleFire()
{
_isFiring = !_isFiring;
if (_isFiring)
StartCoroutine(FireWeapon());
}
IEnumerator FireWeapon()
{
float firingRate = 1.0f / _weapon.Rate;
while (_isFiring)
{
yield return new WaitForSeconds(firingRate);
Debug.Log("Fire! Strength: " + _weapon.Strength);
}
}
}
void OnGUI()
{
GUI.color = Color.green;
GUI.Label(new Rect(5, 50, 150, 100),
"Range: " + _weapon.Range);
GUI.Label(new Rect(5, 70, 150, 100),
"Strength: " + _weapon.Strength);
GUI.Label(new Rect(5, 90, 150, 100),
"Cooldown: " + _weapon.Cooldown);
GUI.Label(new Rect(5, 110, 150, 100),
"Firing Rate: " + _weapon.Rate);
GUI.Label(new Rect(5, 130, 150, 100),
"Weapon Firing: " + _isFiring);
if (mainAttachment && _isDecorated)
GUI.Label(new Rect(5, 150, 150, 100),
"Main: " + mainAttachment.name);
if (secondaryAttachment && _isDecorated)
GUI.Label(new Rect(5, 170, 200, 100),
"Secondary: " + secondaryAttachment.name);
}
}
}
Weapon with config_weapon = new WeaponDecorator(_weapon, Injector)_weapon = new WeaponDecorator( new WeaponDecorator(_weapon, Injector), Cooler)using UnityEngine;
namespace Chapter.Decorator
{
public class ClientDecorator : MonoBehaviour
{
private BikeWeapon _bikeWeapon;
private bool _isWeaponDecorated;
void Start()
{
_bikeWeapon = (BikeWeapon)
FindObjectOfType(typeof(BikeWeapon));
}
void OnGUI()
{
if (!_isWeaponDecorated)
if (GUILayout.Button("Decorate Weapon"))
{
_bikeWeapon.Decorate();
_isWeaponDecorated = !_isWeaponDecorated;
}
if (_isWeaponDecorated)
if (GUILayout.Button("Reset Weapon"))
{
_bikeWeapon.Reset();
_isWeaponDecorated = !_isWeaponDecorated;
}
if (GUILayout.Button("Toggle Fire"))
_bikeWeapon.ToggleFire();
}
}
}
Range: 10 Strength: 25 Cooldown: 2 Firing Rate: 5 Weapon Firing: False
Range: 10 (no change) Strength: 40 (25 + 15 from Injector) Cooldown: 1 (2 + -1 from Cooler) Firing Rate: 15 (5 + 10 from Cooler) Weapon Firing: False Main: Injector Secondary: Cooler
Click "Toggle Fire" to see firing rate in action!
| Combo | Result | Playstyle |
|---|---|---|
| None | Base weapon | Balanced |
| Injector | +Strength | Power |
| Cooler | +Rate, -Cooldown | Rapid fire |
| Stabilizer | +Range | Sniper |
| Injector + Cooler | +Strength, +Rate | Aggressive |
| Injector + Stabilizer | +Strength, +Range | Long-range power |
| Cooler + Stabilizer | +Rate, +Range | Sustained assault |
// Fixed in BikeWeapon
public WeaponAttachment mainAttachment;
public WeaponAttachment secondaryAttachment;
public class BikeWeapon : MonoBehaviour
{
public WeaponConfig weaponConfig;
public List<WeaponAttachment> attachments;
public int maxSlots = 2;
public void Decorate()
{
_weapon = new Weapon(weaponConfig);
// Chain all attachments
foreach (var attachment in attachments.Take(maxSlots))
{
_weapon = new WeaponDecorator(_weapon, attachment);
}
}
}
Now easily configurable to 3, 4, or more slots!
public class SimpleWeaponSystem : MonoBehaviour
{
public WeaponConfig baseWeapon;
public List<WeaponAttachment> attachments;
private float totalRange, totalStrength, totalRate, totalCooldown;
public void ApplyAttachments()
{
// Start with base
totalRange = baseWeapon.Range;
totalStrength = baseWeapon.Strength;
totalRate = baseWeapon.Rate;
totalCooldown = baseWeapon.Cooldown;
// Add each attachment
foreach (var att in attachments)
{
totalRange += att.Range;
totalStrength += att.Strength;
totalRate += att.Rate;
totalCooldown += att.Cooldown;
}
}
}
Important Lesson: Design patterns are guidelines, not commandments!
Lesson: Experiment with patterns! Find what works for your engine, your team, and your game.
| Aspect | Decorator Pattern | Simple Addition |
|---|---|---|
| Complexity | Higher (wrapper classes) | Lower (just loops) |
| Flexibility | Can chain, wrap recursively | Linear addition only |
| Testability | Test each decorator alone | Test entire system |
| Learning Curve | Requires pattern knowledge | Straightforward |
| Extensibility | Add new decorators easily | Modify existing code |
// Instead of calculating every frame:
float strength = _weapon.Strength; // Traverses decorator chain
// Cache when decorators change:
private float _cachedStrength;
public void Decorate() {
// Apply decorators...
_cachedStrength = _weapon.Strength; // Calculate once
}
// BAD - breaks pattern intent
public class WeaponDecorator : IWeapon {
public WeaponDecorator(IWeapon weapon) {
weapon.Strength = 100; // โ Don't modify!
}
}
// GOOD - return modified values
public float Strength {
get { return _weapon.Strength + 50; } // โ Enhance
}
// Add null checks!
if (mainAttachment && secondaryAttachment) { ... }
[Test]
public void Decorator_EnhancesWeaponStats()
{
// Arrange
var config = ScriptableObject.CreateInstance<WeaponConfig>();
config.strength = 25;
var weapon = new Weapon(config);
var attachment = ScriptableObject.CreateInstance<WeaponAttachment>();
attachment.strength = 15;
// Act
IWeapon decorated = new WeaponDecorator(weapon, attachment);
// Assert
Assert.AreEqual(40, decorated.Strength); // 25 + 15
}
[Test]
public void MultipleDecorators_Stack()
{
// Arrange
var weapon = CreateBaseWeapon(strength: 25);
var injector = CreateAttachment(strength: 15);
var cooler = CreateAttachment(rate: 10);
// Act
IWeapon result = new WeaponDecorator(
new WeaponDecorator(weapon, injector),
cooler);
// Assert
Assert.AreEqual(40, result.Strength); // 25 + 15 + 0
Assert.AreEqual(15, result.Rate); // 5 + 0 + 10
}
IArmor interface with properties:
Armor class and ArmorConfig ScriptableObjectArmorDecorator wrapper classArmorUpgrade ScriptableObject with upgrade types:
PlayerArmor MonoBehaviour with 3 upgrade slotsSpatial Partition Pattern: Optimizing large game worlds with spatial data structures
Thank you! ๐ฎ