Lecture 11: Decorator Pattern - Weapon Systems

๐Ÿ”ซ Decorator Pattern

Implementing a Weapon System

Game Programming - CSCI 3213

Spring 2026 - Lecture 11

๐Ÿ“š Learning Objectives

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.

๐ŸŽฏ Weapon System Design

Core Concept

Every bike comes with a front-mounted laser weapon. Players can purchase attachments to boost weapon stats.

Requirements:

Design Goal: Build weapon variations without modifying base weapon code

๐Ÿ”ง Weapon Attachments

Three Attachment Categories:

๐Ÿ” Understanding Decorator Pattern

Core Definition

Add new functionality to an existing object without altering it, by creating a decorator class that wraps the original.

Key Concept:

Decorator wraps an object and enhances its behavior without changing its structure.

Real-World Analogy:

Think of a smartphone:

๐Ÿ—๏ธ Decorator Pattern Structure

Key Participants:

Pattern Family: Structural - concerned with object composition

โš–๏ธ Decorator vs Inheritance

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

With Inheritance (Static):

Weapon โ†’ StrongWeapon โ†’ StrongFastWeapon โ†’ StrongFastLongRangeWeapon

Need a new class for every combination!

With Decorator (Dynamic):

Weapon + Injector + Cooler + Stabilizer (runtime composition)

โœ… Benefits of Decorator Pattern

โš ๏ธ Potential Drawbacks

Our Mitigation: Use ScriptableObjects as decorators - reduces complexity by making them configurable assets instead of code classes.

๐Ÿค” When to Use Decorator Pattern

Use Decorator When:

Other Game Use Cases:

Pattern Family: Other structural patterns include Adapter, Bridge, Faรงade, and Flyweight.

๐ŸŽฎ Unity-Specific Challenge

The Constructor Problem

Traditional Decorator: Relies heavily on constructors

// Standard C# approach
IWeapon weapon = new Weapon();
weapon = new InjectorDecorator(weapon);
weapon = new CoolerDecorator(weapon);
Problem: MonoBehaviour and ScriptableObject don't use constructors the same way!
  • Unity manages object lifecycle
  • Initialization in Awake()/Start() callbacks
  • Can't new up MonoBehaviours directly

Our Solution:

Adapt the pattern to use Unity's ScriptableObject system while preserving Decorator benefits!

๐Ÿ’ป Implementation: IWeapon Interface

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Patterns/IWeapon.cs
This interface defines the contract for all weapons and decorators.
โš ๏ธ This code goes into your Unity project for Blade Racer.

public interface IWeapon { float Range { get; } float Rate { get; } // Fire rate float Strength { get; } // Damage float Cooldown { get; } // Cool-down duration }
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   IWeapon   โ”‚
โ”‚  (interface)โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚ implements
  โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
  โ–ผ         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚Weaponโ”‚ โ”‚WeaponDecorator โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

IWeapon - The common contract. Both the base weapon AND all decorators must implement this interface, making them interchangeable.

float Range { get; } - Read-only property. Decorators can return enhanced values without modifying the original.

Why an Interface? - Common contract between weapon and decorators. Ensures consistent property signatures. Allows decorators to wrap weapons seamlessly. Both Weapon and WeaponDecorator implement it.

๐Ÿ’ป WeaponConfig - Base Configuration

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Configs/WeaponConfig.cs
This ScriptableObject stores base weapon configurations.
โš ๏ธ This code goes into your Unity project for Blade Racer.

using UnityEngine; [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;

ScriptableObject - Unity asset type. Configure once in Inspector, reuse across scenes without code changes.

[CreateAssetMenu] - Adds a right-click menu in Unity Project panel to create this asset type. fileName = default name, menuName = menu path.

[Range(0, 60)] - Constrains the Inspector slider range.

[Tooltip("...")] - Shows a tooltip in the Inspector when hovering.

[SerializeField] - Makes private field visible and editable in the Inspector.

These attributes are for Unity's Inspector only โ€” they don't affect runtime behavior.

๐Ÿ’ป WeaponConfig - Properties & Metadata

๐Ÿ“ Continuing WeaponConfig.cs

Add these properties and IWeapon implementation to WeaponConfig from Slide 14.

[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; } } }

Metadata fields - weaponName, weaponPrefab, weaponDescription are public so designers can label and associate a 3D model.

public float Rate { get { return rate; } } - Property getter. The private rate field is set in Inspector; this property exposes it as the IWeapon interface requires.

IWeapon implementation - WeaponConfig acts as the source of truth for base stats. Weapon class delegates to these values.

ScriptableObject Benefit - Create weapon configs as assets, configure in Inspector without code!

๐Ÿ’ป Weapon - The Component

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Patterns/Weapon.cs
This is the concrete component that will be decorated.
โš ๏ธ This code goes into your Unity project for Blade Racer.

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; } } }

Concrete Component - In the Decorator pattern, this is the "real" object being decorated. Not a MonoBehaviour!

private readonly WeaponConfig _config - readonly means the reference can only be set in the constructor, never changed afterward.

Weapon(WeaponConfig weaponConfig) - Plain C# constructor. Since Weapon is not a MonoBehaviour, we can use normal constructors.

Pass-through properties - Each IWeapon property simply reads from the config. The decorator will add on top of these values.

๐Ÿ’ป WeaponDecorator - The Wrapper

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Patterns/WeaponDecorator.cs
This is the decorator that wraps and enhances weapons.
โš ๏ธ This code goes into your Unity project for Blade Racer.

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; } }

Decorator Base - Wraps any IWeapon (could be a Weapon OR another WeaponDecorator for chaining!).

IWeapon _decoratedWeapon - Holds reference to the thing being wrapped. Type is IWeapon, not Weapon โ€” this enables chaining.

Constructor - Takes IWeapon weapon (not Weapon) so you can pass in another decorator to chain.

Enhanced properties - _decoratedWeapon.Rate + _attachment.Rate adds attachment bonus on top of whatever the wrapped object returns (which could itself be decorated).

The key insight: the constructor accepts IWeapon, not Weapon. This single decision enables unlimited chaining.

๐Ÿ’ป WeaponDecorator - Complete

๐Ÿ“ Continuing WeaponDecorator.cs

Add these remaining property implementations to WeaponDecorator from Slide 17.

public float Strength { get { return _decoratedWeapon.Strength + _attachment.Strength; } } public float Cooldown { get { return _decoratedWeapon.Cooldown + _attachment.Cooldown; } } }

Same pattern - Strength and Cooldown follow the identical add-on-top formula.

Closing brace - The full WeaponDecorator class is complete. It has a constructor and 4 property implementations โ€” that's all it needs.

The Magic - Wraps an IWeapon (could be Weapon or another decorator!). Doesn't modify wrapped object directly. Returns enhanced values by adding attachment bonuses. Can chain decorators: Weapon โ†’ Injector โ†’ Cooler.

๐Ÿ’ป WeaponAttachment - Designer Assets

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Configs/WeaponAttachment.cs
ScriptableObject that defines weapon attachment properties.
โš ๏ธ This code goes into your Unity project for Blade Racer.

using UnityEngine; [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;

ScriptableObject Decorator Data - Instead of code classes (InjectorDecorator, CoolerDecorator), we use data assets. One WeaponDecorator class handles ALL attachment types!

[CreateAssetMenu] - Creates the "Weapon/Attachment" menu item so designers can spawn new attachments in Unity's Project window.

implements IWeapon - WeaponAttachment acts as a stat delta. Its values represent the BONUS added by the attachment, not absolute stats.

Unity advantage - Designers can create a "TurboCharger" attachment by filling in fields, with zero code changes.

๐Ÿ’ป WeaponAttachment - Properties

๐Ÿ“ Continuing WeaponAttachment.cs

Add these properties and IWeapon implementation to WeaponAttachment from Slide 19.

[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; } } }

Metadata - attachmentName, attachmentPrefab, attachmentDescription let designers name, describe, and link a 3D visual for the attachment.

[Range(0, -5)] - Negative range for cooldown reduction. A negative value means the attachment REDUCES cooldown.

IWeapon implementation - These properties expose the private serialized fields. WeaponDecorator reads these when computing enhanced stats.

Key Innovation - Instead of code classes for each decorator, we create ScriptableObject assets! Designers can create unlimited attachment types.

๐Ÿ’ป BikeWeapon - MonoBehaviour Client

๐Ÿ“ File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Bike/BikeWeapon.cs
MonoBehaviour that uses the decorated weapon system.
โš ๏ธ This code goes into your Unity project for Blade Racer.

using UnityEngine; using System.Collections; 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); }

Client (MonoBehaviour) - BikeWeapon is the "client" in the Decorator pattern. It uses the IWeapon interface without knowing if it's base or decorated.

public WeaponConfig weaponConfig - Assigned in Inspector. Defines the weapon's base stats.

public WeaponAttachment mainAttachment / secondaryAttachment - Optional attachments assigned in Inspector. Can be null.

private IWeapon _weapon - Holds the current weapon (base Weapon or decorated chain). Type is IWeapon for flexibility.

Start() - Creates the initial plain Weapon wrapping the config. No decorators yet.

๐Ÿ’ป BikeWeapon - The Magic Method

๐Ÿ“ Continuing BikeWeapon.cs

Add this Decorate() method to the BikeWeapon class from Slide 21.

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; }

Decorate() - The method that applies decorators at runtime. Called when player equips an attachment.

_weapon = new WeaponDecorator(_weapon, mainAttachment) - Wraps the current _weapon in a new decorator. The original Weapon is untouched.

Decorator Chaining - When both attachments are present: inner WeaponDecorator wraps Weapon+Injector, outer wraps that result with Cooler. Each adds on top of the previous. Notice how we wrap a decorator with another decorator! This is the power of the pattern.

๐Ÿ’ป BikeWeapon - Reset Functionality

๐Ÿ“ Continuing BikeWeapon.cs

Add these Reset() and ToggleFire() methods to BikeWeapon.

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); } } }

Reset() - Removes all decorators by creating a brand new Weapon. Previous decorators are discarded.

IEnumerator FireWeapon() - A coroutine: pauses and resumes execution over time without blocking. Started with StartCoroutine().

yield return new WaitForSeconds(firingRate) - Pauses the coroutine for firingRate seconds, then continues the while loop.

1.0f / _weapon.Rate - Converts "fires per second" rate to a time interval. Rate=5 โ†’ interval=0.2s between shots.

๐Ÿ’ป BikeWeapon - Debug GUI

๐Ÿ“ Continuing BikeWeapon.cs

Add this OnGUI() method to BikeWeapon for debugging weapon stats.

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); } } }

OnGUI() - Unity's immediate-mode GUI, called every frame. Good for debug displays without needing UI components.

GUI.Label(new Rect(x, y, w, h), text) - Draws text at screen position. Rect parameters: x, y, width, height in pixels.

Reads from _weapon - These labels reflect the current state: if decorated, shows enhanced values; if reset, shows base values. The UI doesn't need to know which!

Conditional attachment labels - Only shows attachment names when _isDecorated is true, so the display stays clean.

๐Ÿ”„ The Complete Flow

Initialization:

  1. BikeWeapon.Start() creates base Weapon with config
  2. Weapon wraps WeaponConfig ScriptableObject
  3. Base stats: Range=10, Strength=25, Rate=5, Cooldown=2

Decoration (One Attachment):

  1. User clicks "Decorate Weapon" button
  2. BikeWeapon.Decorate() called
  3. _weapon = new WeaponDecorator(_weapon, Injector)
  4. Now accessing _weapon.Strength returns: 25 (base) + 15 (injector) = 40

Decoration (Two Attachments - Chained!):

  1. _weapon = new WeaponDecorator(
  2.   new WeaponDecorator(_weapon, Injector),
  3.   Cooler)
  4. Inner decorator adds Injector stats
  5. Outer decorator adds Cooler stats on top
  6. Final: Strength = 25 + 15 + 0 = 40, Rate = 5 + 0 + 10 = 15

๐Ÿงช Testing the System

Setup Steps:

  1. Create new Unity scene
  2. Create empty GameObject, add BikeWeapon component
  3. Create WeaponConfig asset:
    • Assets โ†’ Create โ†’ Weapon โ†’ Config
    • Name: "BaseLaser"
    • Set: Rate=5, Range=10, Strength=25, Cooldown=2
  4. Create Attachment assets:
    • Assets โ†’ Create โ†’ Weapon โ†’ Attachment
    • "Injector": Strength +15
    • "Cooler": Rate +10, Cooldown -1
    • "Stabilizer": Range +20
  5. Assign assets to BikeWeapon in Inspector
  6. Add Decorator section to TestPanel (next slide)

๐ŸŽฎ Updating TestPanel for Decorator Pattern

๐Ÿ“ Evolving TestPanel.cs

Add Decorator Pattern keyboard shortcuts and section to your TestPanel.cs.

// Add to TestPanel fields private bool _decoratorExpanded = true; private BikeWeapon _bikeWeapon; private bool _isDecorated; void Start() { // ... existing code ... _bikeWeapon = FindFirstObjectByType<BikeWeapon>(); } // Add to Update() - Decorator Pattern shortcuts if (_bikeWeapon != null) { if (Input.GetKeyDown(KeyCode.M)) // Modify { _bikeWeapon.Decorate(); _isDecorated = true; } if (Input.GetKeyDown(KeyCode.N)) // uNdo { _bikeWeapon.Reset(); _isDecorated = false; } if (Input.GetKeyDown(KeyCode.B)) // Bang! _bikeWeapon.ToggleFire(); }
// Add DrawDecoratorSection() method void DrawDecoratorSection() { if (_bikeWeapon == null) return; GUI.backgroundColor = new Color(0.5f, 0f, 0.5f); // Purple _decoratorExpanded = GUILayout.Toggle( _decoratorExpanded, "โ–ผ Decorator Pattern", "button"); GUI.backgroundColor = Color.white; if (_decoratorExpanded) { GUILayout.BeginVertical("box"); if (!_isDecorated) { if (GUILayout.Button("Decorate (M)")) { _bikeWeapon.Decorate(); _isDecorated = true; } } else { if (GUILayout.Button("Reset (N)")) { _bikeWeapon.Reset(); _isDecorated = false; } } if (GUILayout.Button("Toggle Fire (B)")) _bikeWeapon.ToggleFire(); GUILayout.EndVertical(); } }

Fields & Start() - Adds three new fields: _decoratorExpanded (collapsible toggle), _bikeWeapon (reference to the scene component), and _isDecorated (tracks state). FindFirstObjectByType locates the BikeWeapon in the scene at startup.

Update() shortcuts - M key calls Decorate() to apply attachments, N key calls Reset() to strip them, B key toggles firing on/off. All guarded by a null check so they only run when a BikeWeapon exists.

DrawDecoratorSection() - Draws the purple collapsible GUI panel. The toggle button shows/hides the inner controls. Buttons are context-sensitive: shows "Decorate" when undecorated and "Reset" when decorated, ensuring you can always undo.

GUI.backgroundColor = new Color(0.5f, 0f, 0.5f) - Tints the toggle button purple to visually distinguish the Decorator section from other sections in TestPanel.

Keymap Update: Add to DrawKeymapWindow():
M = Decorate (Modify), N = Reset (uNdo), B = Toggle Fire (Bang)

๐Ÿ“Š Expected Behavior

Before Decoration:

Range: 10
Strength: 25
Cooldown: 2
Firing Rate: 5
Weapon Firing: False

These are the raw WeaponConfig values โ€” no decorators applied yet. Range: 10 = base laser reach. Strength: 25 = base damage output. Cooldown: 2 = seconds before weapon can fire again. Firing Rate: 5 = fires 5 times per second. Weapon Firing: False = fire coroutine is not running.

After Decoration (Injector + Cooler):

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

Range: 10 - Unchanged; neither Injector nor Cooler boosts range. Strength: 40 - Base 25 + Injector's +15 bonus. Cooldown: 1 - Base 2 + Cooler's โˆ’1 (negative value reduces cooldown). Firing Rate: 15 - Base 5 + Cooler's +10. Main/Secondary labels - Only appear when _isDecorated is true, confirming both decorators are active.

The BikeWeapon code never changed โ€” only the decorator chain changed what _weapon returns.

Click "Toggle Fire" to see firing rate in action!

๐ŸŽจ Designer-Friendly Workflow

Creating New Attachments (No Code!):

  1. Right-click in Project: Create โ†’ Weapon โ†’ Attachment
  2. Name it: "TurboCharger"
  3. Configure in Inspector:
    • Rate: +15 (fires faster)
    • Range: 0 (no change)
    • Strength: -5 (trade-off for speed)
    • Cooldown: -2 (cools faster)
    • Attachment Name: "Turbo Charger"
    • Description: "High speed, low power"
  4. Assign prefab: Drag 3D model
  5. Done! No programming required
Power of ScriptableObjects: Designers become content creators without touching code!

๐Ÿ”€ Combination Possibilities

With 3 Attachments:

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
With 10 attachments: 1 + 10 + 45 = 56 combinations! (With 2 slots)

๐Ÿ”ง Extending Beyond 2 Slots

Current Limitation: Hardcoded 2 Slots

// Fixed in BikeWeapon public WeaponAttachment mainAttachment; public WeaponAttachment secondaryAttachment;

Hardcoded public fields mean you must add new code every time you want a new attachment slot. Adding a third slot requires modifying BikeWeapon and updating Decorate() โ€” a code change just to change a number.

Better Approach: Dynamic Slots

public class BikeWeapon : MonoBehaviour { public WeaponConfig weaponConfig; public List 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); } } }

List<WeaponAttachment> - Dynamic list, any number of attachments. Assign them in Inspector without touching code.

.Take(maxSlots) - LINQ method that limits iteration to the configured max slots, ignoring any extras in the list.

maxSlots is a public field, so designers can configure the limit in Inspector without any code change.

This approach makes the slot count a data decision, not a code decision.

Now easily configurable to 3, 4, or more slots!

๐Ÿ”„ Alternative: Without Decorator

Simple Iteration Approach:

public class SimpleWeaponSystem : MonoBehaviour { public WeaponConfig baseWeapon; public List 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; } } }

SimpleWeaponSystem - Direct approach: stores totals and loops through attachments to sum them up. Straightforward to read, but loses the IWeapon interface chain โ€” can't pass this as an IWeapon or nest it inside another wrapper.

Why Use Decorator Then? - Decorator provides a structured, repeatable pattern. Chaining capability (can wrap decorators). Maintains interface consistency โ€” every weapon, decorated or not, is usable as IWeapon. Follows established design principles (Open/Closed).

๐ŸŽญ Pattern Experimentation

Important Lesson: Design patterns are guidelines, not commandments!

Our Implementation is "Unorthodox":

Why This is OK:

Lesson: Experiment with patterns! Find what works for your engine, your team, and your game.

โš–๏ธ Decorator vs Simple: Trade-offs

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
Decision Point: Use Decorator when system will grow complex. Use simple approach for small, stable systems.

๐ŸŒ Beyond Weapons

Other Decorator Use Cases:

โšก Performance Tips

Optimization Strategies:

๐Ÿšซ Common Mistakes to Avoid

  1. Modifying Wrapped Object Directly:
    // 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 }

    Mutating the wrapped object's state permanently breaks the "wrap without modifying" guarantee. If you set weapon.Strength = 100 in the constructor, the original object is permanently changed โ€” other code reading it will see the corrupted value. The correct approach returns a new computed value in the property getter, leaving the original untouched.

  2. Forgetting Interface Consistency:
    • Decorator must implement same interface as component
    • Otherwise can't wrap/chain properly
  3. Not Handling null Attachments:
    // Add null checks! if (mainAttachment && secondaryAttachment) { ... }

    Always check for null before constructing decorators. An unassigned WeaponAttachment ScriptableObject reference is null in Unity. Passing null into WeaponDecorator will throw a NullReferenceException at runtime.

  4. Overcomplicating Simple Systems:
    • If you only have 1-2 attachments total, pattern may be overkill

๐Ÿงช Unit Testing Decorators

[Test] public void Decorator_EnhancesWeaponStats() { // Arrange var config = ScriptableObject.CreateInstance(); config.strength = 25; var weapon = new Weapon(config); var attachment = ScriptableObject.CreateInstance(); 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 }

[Test] - NUnit attribute marking this as a Unity Test Runner test method.

ScriptableObject.CreateInstance<T>() - Creates a ScriptableObject in-memory for testing, without needing an asset file on disk.

Arrange / Act / Assert - Standard test pattern: set up data, perform action, verify result. The first test checks a single decorator adds the bonus. The second test MultipleDecorators_Stack verifies chaining: two decorators applied in sequence both contribute their bonuses correctly.

Decorators are easy to test in isolation because they're plain C# classes, not MonoBehaviours.

๐Ÿ“ Summary

What We Learned:

Key Takeaways:

Gaming History Moment ๐Ÿ•น๏ธ

Pokรฉmon Mania (1996-2000)

When Pokรฉmon Red and Blue launched in 1998 (North America), they introduced a revolutionary progression system. Your starter Pokรฉmon began basic, but through battles it would level up, learn new moves, hold items, and eventually evolve. A Charmander could become Charmeleon, then Charizard - each form adding capabilities without losing the base stats.

But the real genius was the item system. Give Pikachu a Light Ball - now it has double attack! Add a Focus Band - 10% chance to survive knockout! Equip Leftovers - passive health regeneration! Each held item decorated the Pokรฉmon with new abilities while keeping its core identity. You could swap items anytime, creating millions of possible combinations from 151 Pokรฉmon.

Connection to Decorator Pattern

Pokรฉmon's held item system IS the Decorator Pattern! Each Pokรฉmon is a base weapon (like our BikeWeapon), and held items are decorators that wrap functionality around it. Light Ball = RangeDecorator, Leftovers = RegenDecorator, Focus Band = SurvivalDecorator. The base Pokรฉmon stats stay intact, but decorators add layers of behavior. Just like our weapon attachments, you can equip/unequip items at runtime, stack effects, and create infinite combinations while maintaining the core IPokรฉmon interface!

Learn More: The Pokรฉmon Company: Official History | Satoshi Tajiri: 2004 Interview (Kotaku)

๐Ÿ’ช Practice Exercise

Implement an Armor Decoration System

Requirements:

  1. Create IArmor interface with properties:
    • Defense (int)
    • Weight (float)
    • Durability (float)
  2. Create Armor class and ArmorConfig ScriptableObject
  3. Create ArmorDecorator wrapper class
  4. Create ArmorUpgrade ScriptableObject with upgrade types:
    • Reinforcement: +Defense, +Weight
    • Lightweight Plating: -Weight, -Defense
    • Hardening: +Durability, +Weight
  5. Create PlayerArmor MonoBehaviour with 3 upgrade slots
  6. Test different upgrade combinations
Bonus: Add visual representation - show different armor pieces on character model when upgraded!

โ“ Questions?

Common Questions:

  • Q: Can I remove individual decorators from the middle?
    • A: Not easily with this implementation. Would need to rebuild chain. Better: store attachments in list, rebuild when changed.
  • Q: What if I want decorators to interact with each other?
    • A: Pass additional context in constructor, or use different pattern (Chain of Responsibility)
  • Q: Can decorators have different types of enhancement?
    • A: Yes! Some can multiply, others add, others replace values entirely
  • Q: When should I use Decorator vs Composite pattern?
    • A: Decorator adds behavior, Composite handles tree structures of objects

Next Lecture:

Spatial Partition Pattern: Optimizing large game worlds with spatial data structures

Thank you! ๐ŸŽฎ