Design Patterns

It’s harder to read code than to write it. - Joel Spolsky

Development can resemble déjà vu, solving recurring or similar problems. Design patterns help tap into the collective knowledge of software engineers who’ve already solved them.

Recommended Reads

  1. Singleton
  2. Flyweight
  3. Serialization
  4. Scriptable Objects
  5. Finite State Machine
  6. Broker Chain
  7. Decorator
  8. Command
  9. Entity-Component System
  10. MVC

Singleton

- Ensure a class has one instance, and 
- provide a global point of access to it.

Benefits

  1. It doesn’t create the instance if no one uses it
  2. It’s initialized at runtime.
  3. You can subclass the singleton

Avoid Singleton unless unavoidable

Poorly designed singletons are often “helpers” that add functionality to another class. If you can, just move all of that behavior into the Manager class it helps.

Alternatives

Single thread implementation

public class SimpleSingleton
{
    private static SimpleSingleton instance;
    
    //private constructor to not allow external initialization
    private SimpleSingleton() { }

    //static method to intialize class without creating class instance
    public static SimpleSingleton Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new SimpleSingleton();
            }
            return instance;
        }
    }
}

Flyweight Pattern

When you have tons of objects, memory can get eaten up fast, and performance can take a hit. The Flyweight Pattern helps by sharing objects to save memory. It splits the state into intrinsic (shared) and extrinsic (unique) parts, so shared objects can be reused in different situations at the same time.

Use Case

Participants

Code


// Flyweight interface
interface Shape {
    void draw(int x, int y);
}
// Concrete Flyweight
class Circle implements Shape {
    private String color; // Intrinsic state

    public Circle(String color) {
        this.color = color;
    }

    @Override
    public void draw(int x, int y) {
        System.out.println("Drawing " + color + " circle at (" + x + "," + y + ")");
    }
}

Circle class implemets Shape interface with intrinstic state color that is shared. The draw method uses this intrinsic state to darw the circle.

// Flyweight Factory
class ShapeFactory {
    private static final Map<String, Shape> circleMap = new HashMap<>();

    public static Shape getCircle(String color) {
        Circle circle = (Circle) circleMap.get(color);

        if (circle == null) {
            circle = new Circle(color);
            circleMap.put(color, circle);
            System.out.println("Created " + color + " circle.");
        }
        return circle;
    }
}

// Client
public class FlyweightPatternDemo {
    public static void main(String[] args) {
        Shape redCircle1 = ShapeFactory.getCircle("Red");
        redCircle1.draw(10, 10);

        Shape redCircle2 = ShapeFactory.getCircle("Red");
        redCircle2.draw(20, 20);

        Shape blueCircle = ShapeFactory.getCircle("Blue");
        blueCircle.draw(30, 30);
    }
}

Cons

One downside of the Flyweight Pattern is that the client (like our FlyweightPatternDemo class) has to handle the extrinsic state. This means the client needs to keep track of things like the coordinates where the circles are drawn. If you mess up managing this state, it can lead to bugs.

Serialization

Serialization is the process of converting an object into a stream of bytes to store the object or transmit it to memory, a database, or a file.

Serialization (Object -> Bytes) -> File, Database or Memory -> (Bytes -> Object) Deserialisation

Scriptable Objects

Uses flyweight pattern

First-class objects:

[CreateAssetMenu(fileName="NPCConfig")]
public class NPCConfigSO : ScriptableObject
{
    [Range(10, 100)]
    public int maxHealth;

    public NPCAIStateEnum goodHealthAi;
    public NPCAIStateEnum lowHealthAi;
}
public class NPCHealth: MonoBehaviour
{
    [SerializeField] NPCConfigSO config;
    [SerializeField] int currentHealth;
}

Support a limited number of event functions, including Awake, OnEnable, OnDestroy, and OnDisable at runtime. The Editor also calls OnValidate and Reset from the Inspector.

The most common use for ScriptableObjects is as data containers for shared data, particularly for static game configuration data that doesn’t change at runtime. Think of ScriptableObject data as “read-only” relative to persistent data Typical use cases of ScriptableObjects may include:

data containers : data that does not need to change at runtime

using System.IO;
public class LevelManager : MonoBehaviour
{
    public ScriptableObject levelLayout;
    public void LoadLevelFromJson(string jsonFile)
    {
        if (levelLayout == null)
        {
         levelLayout = ScriptableObject.
         CreateInstance<LevelLayout>();
        }
        var importedFile = File.ReadAllText(jsonFile);
        JsonUtility.FromJsonOverwrite(importedFile,
        levelLayout);
    }
}

But now bad actors can potentially tamper application. To avoid it, one has to use combination of all below three methods:

Empty SO can also be used as Enum like comparisions. However, unlike enums, ScriptableObjects are easy to extend.

Using UnityEngine;
[CreateAssetMenu(fileName=”GameItem”)]
public class GameItem : ScriptableObject
{
    // Every GameItem can now be used as Enum GameItemType

    //Lets extend it with some business logic
    public GameItem weakness;
    public bool IsWinner(GameItem other)
    {
        return other.weakness == this;
    }
}

Finite State Machine

A finite state machine can only be in one state at any moment in time.

Idea is to decompose an object’s behavior into easily manageable “chunks” or states.

A naive approach is to use a series of if-else statements . A better mechanism for organizing states and affecting state transitions is a state transition table (A table of conditions and the states those conditions lead to.)

A Bowler in a cricket simulation game can be implemented as state machines. Considering some simple states for a bowler :-

Interestingly, whole team can also be implemented as FSMs, and can be in state of Batting or Fielding.

class Bowler
{
	enum StateType(Idle, Runup, Deliver, Field);

	Update()
	{
		state->execute();
	}
	UpdateState(StateType newState)
	{
		//delete currentState
		//state = newState
	}
	
}
class DeliverBowl: State
{
	Execute()
	{
		//apply physics to ball with some force and spin
		//throw event that ball has been bowled
		//or Call batsman object with details of ball bowled
	}
}

Broker Chain Pattern

bcp

// data class
class BaseStats { 
    int damage;
    int heal;
}
enum StatType {Damage, Heal}
class Stats {
    readonly BaseStats baseStats;
    int Damage { get {}} //modifier
    int Heal { get {} } //modifier
}
class StatsMediator {
    // Keep in memory because it is meant to access frequently
    readonly LinkedList<StatModifier> modifiers = new ();

    event EventHandler<Query> Queries;
    void PerformQuery(object sender, Query query) => Queries?.Invoke(sender, query);

    public void AddModifier(StatModifier modifier) {
        modifiers.AddLast(modifier);
    }
}
class Query {
    readonly StatType StatType;
    int Value;
    Query(StatType statType, int value) {
        StatType = statType;
        Valye = value;
    }
}
abstract class StatModifier {}

Decorator Pattern

Decorator or Wrapper pattern allows behaviour to be added to individual objects without affecting the behaviour of other objects of same class.

Use Case

  1. A game has multiple AI Bot players.
  2. Each AI Bot Player can cast spells using one of predefined abilities.
  3. It can also choose to merge its ability with any of other abilities, to make a single cast stronger.

Structure

+-----------+   +--------------+   
|Interface  |-->|Magic Ability|   
+-----+-----+   +------+-------+   
      |                           
      |                                   
      |          +-----+------+        +-------------+
      +--------->+ Ability     +------>|Fire Damage |
                 | Decorator   |       |Deocartor   |
                 +------------+        +-------+----+
                       |
                       |           +---------------+
                       +---------->|Heal Decorator |
                                   +-------+-------+


+-----------+     +----------------+   
|Controller  |--->|Ability Definition|   
+-----+-----+     +--------+-------+  
    |
    |          +--------------+   
    +--------->|Ability Factory|   
               +------+-------+  

Implementation


publc interface IAbility {
    public int CastSpell();
}

public enum AbilityType {
    Magic,
    FireDamage,
    Heal
}

public class AbilityDefinition { // Immutable Data Class
    public int value;// = 10;
    public AbilityType abilityType;// = AbilityType.Magic;
}   
public static class AbilityFactory {
    public static IAbility Create(AbilityDefinition definition) {
	return definition.type switch {
	    _ => new MagicAbility(definition.value),
		// Magic, Heal and Fire
	    };
    }
}

public class MagicAbility: IAbility {
    private readonly int value;
    public MagicAbility(int value){
        this.value = value;
    }
    public int CastSpell() {
        return vaule;
    }
}
public abstract class AbilityDecorator : IAbility {
    //ability decorator can Wrap another ability inside it
    protected IAbility ability;

    public void Decorate(IAbility ability) {
        if(ReferenceEquals(this, ability) {
            //Dont decoarte with itself
        }
        if(this.ability is AbilityDecorator decorator){
            //Wrap this ability with another
            decorator.Decorate(ability);
        } else {
            this.ability = ability;
        }
    }
    
    protected readonly int value;
    protected AbilityDecorator(int value) {
	    this.value = value;
    }
    
    public virtual int CastSpell() {
	    //Deocator adds value spell effect over 
        // original ability spell effect. If it exists. 
	    return ability?.CastSpell() + value ?? value;
    }
}
public class FireDamageDecorator: AbilityDecorator {
    public FireDamageDecorator(int value) : base(value) {}
}

public class HealDecorator: AbilityDecorator {
    public  HealDecorator(int value): base(value) {}

    public override int CastSpell() {
        HealPlayer();
        return ability?.CastSpell() ?? 0;
    }
    void HealPlayer(){
		//Special ability to heal self when damaging enemy
    }
}

public class AbilityController {
    //psuedocode
    AbilityDefinition definition;
    public IAbility Ability {get; private set;}
    //Step 1
    Ability = AbilityFactory.Create(definition);
    //Step 2
    if(AbilityManager.Instance.selectedAbility == null){
	AbilityManager.Instance.selectedAbility = this;
    } else {
	AbilityManager.Instance.Decorate(this);
    	AbilityManager.Instance.selectedAbility = null;
    }
    //Step 3
    Ability.CastSpell();
}

public class AbilityManager : Singleton {
    //Instance for Singleton
    public AbilityController selectedAbility;
    public void Decorate(AbilityController abilityChosen) {
        if (selectedAbility == abilityChosen) return;
        if(selectedAbility.Ability is AbilityController decorator) {
		    //Wrapping two abilities
	        decorator.Decorate(abilityChosen.Ability);
	        abilityChosen.Ability = decorator;
        }
    }
}

Command Pattern

Command/Action : Converts an method call into a stand-alone object.

A method call usually responds to a specific action ( or command ). Everything needed to execute that command is available to the class.

It needs 4 components:-

  1. Command Interface
  2. Concrete Command
  3. Invoker
  4. Receiver

Invoker is needed to set command

Implement Command in concrete classes

Reciever is not aware which key is pressed and how to handle it

insert_text_key = Key() // .SetCommand(InsertTextCommand) bold_key = Key() // .SetCommand(BoldTextCommand)

bold_key.press()

ECS

Entity-Component System : Components are pure classes, almost entirely for data.

MVC