Game Programming - CSCI 3213
Spring 2026 - Lecture 11
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.
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!
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.
โโโโโโโโโโโโโโโ
โ 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.
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.
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.
Add these properties and IWeapon implementation to WeaponConfig from Slide 14.
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!
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.
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.
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.
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.
Add these remaining property implementations to WeaponDecorator from Slide 17.
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.
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.
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.
Add these properties and IWeapon implementation to WeaponAttachment from Slide 19.
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.
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.
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.
Add this Decorate() method to the BikeWeapon class from Slide 21.
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.
Add these Reset() and ToggleFire() methods to BikeWeapon.
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.
Add this OnGUI() method to BikeWeapon for debugging weapon stats.
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.
Weapon with config_weapon = new WeaponDecorator(_weapon, Injector)_weapon = new WeaponDecorator( new WeaponDecorator(_weapon, Injector), Cooler)Add Decorator Pattern keyboard shortcuts and section to your TestPanel.cs.
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.
DrawKeymapWindow():M = Decorate (Modify), N = Reset (uNdo), B = Toggle Fire (Bang)
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.
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!
| 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 |
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.
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!
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).
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 |
Caching the decorated value after each Decorate() call avoids re-traversing the chain every frame. Compute once when the chain changes, read cheaply in Update() or OnGUI().
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.
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.
[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.
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.
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!
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! ๐ฎ