Documentation
Features
🎨 Decorators

🎨 Decorators

What are Decorators

Use Decorators to set or change visuals or behavior of any GameObjects in a clean, decoupled, single responsibility and reusable way.

This sounds like very broad description, and thats because it can be used in many different scenarios.

Any hierarchy of GameObjects which need to show/interact with different elements of the same set of data is a perfect contender to use Decorators.

Good scenarios to be using decorators in:

  • Skinning a spawned prefab
  • In-game character dress up
  • Spawning procedural level assets
  • UI screens
  • World map
  • Reward chest contents
  • Card stats and graphics
  • Inventory amount and graphic

How to create a Decorator

Implement IDecorator<T> where T is the type of data you want to process.

Then send the data object through Decorators.Distribute(GameObject, T) and the GameObject and all its children will receive the data object.

Lets look at some pseudo code to understand the bigger picture first:

// This is pseudo code, do not use directly.
 
// This should be at the root GameObject in the hierarchy
// where all the weapons should be spawned, such as a ScrollView.
class WeaponSpawner : SerializedMonoBehaviour
{
    // Set the base prefab which should be spawned and dressed
    // using the decorators.
    [SerializeField, Resource]
    private Guid weaponPrefab;
 
    protected void Start()
	{
        // Get this weapon list from inventory or libraries.
        List<WeaponAsset> weapons = ...;
        
        for (int i = 0; i < weapons.Count; i++)
        {
            // Spawn the base weapon prefab.
            GameObject weaponInstance = GameResources.Instantiate(weaponPrefab);
            // All decorators will be notified of the WeaponAsset.
            Decorators.Distribute(weaponInstance, weapons[i]);
        }
    }
}
 
// Then add these specific decorators inside of the base weapon prefab 
// on the GameObjects that should visualize the data.
class WeaponGraphicDecorator : MonoBehaviour, IDecorator<WeaponAsset>
{
    public void Decorate(WeaponAsset weapon)
	{
        // Spawn Graphic.
    }
}
 
class WeaponNameDecorator : MonoBehaviour, IDecorator<WeaponAsset>
{
    public void Decorate(WeaponAsset weapon)
	{
        // Show Name.
    }
}
 
class WeaponDpsDecorator : MonoBehaviour, IDecorator<WeaponAsset>
{
    public void Decorate(WeaponAsset weapon)
	{
        // Show Damage per second.
    }
}

As you can see, the decorators are tiny, isolated pieces of code that do 1 thing only, all in line with the single responsibility principle.

No more facade script at the root that tries to do everything and has 10s of inspector properties with Transforms, Images and texts that need to be linked, and when 1 component is missing, the script breaks.

In games there are multiple places throughout the flow which show the same data, but just a little differently. For example a weapon could be showed in these locations:

  • Shop
  • Inventory
  • Weapon catalog
  • Chest drops
  • Hud
  • Quest rewards

However, each time its shown, the content is just a little different:

  • One should not show the name of the weapon
  • Another should only show the Dps
  • Another only the rarity, and the weapon graphic should be a question mark

There are many use cases where the data should not always be visualized the same way.

Using Decorators allows any developers to only show the data needed without having the overhead of everything that could possible be shows through a single script.

Example 1

Let's say we have a screen which show all cards in the player's inventory.

InventoryAsset

Of course, in the DoD fashion, we start with defining the data:

using GameSuite;
 
namespace Game
{
    public class CardInventoryAsset : InventoryAsset
    {
        [TermsPopup]
        public string Title;
        [TermsPopup]
        public string Description;
        [Resource]
        public Guid Graphic;
        [Rarity]
        public Guid Rarity;
	}

Distributor

Let's spawn the prefabs of every card in the player's inventory and call the decorators.

This component can be on a ScrollView for example.

using GameSuite;
 
namespace Game
{
    public class CardSpawner : SerializedMonoBehaviour
    {
        // Set the base prefab which should be spawned and dressed
        // using the decorators.
        [SerializeField, Resource]
        private Guid cardPrefab;
        
        protected void Start()
	    {
            // Get all cards in the current players inventory.
            List<CardInventoryData> cards = 
                SaveData.Get<InventorySaveData>().GetAll<CardInventoryData>();
            InventoryLibrary inventoryLibrary = Libraries.Get<InventoryLibrary>();
            
            for (int i = 0; i < cards.Count; i++)
            {
                if (inventoryLibrary.TryGetByType(cards[i].Item, 
                        out CardInventoryAsset cardInventoryAsset))
                {
                    // Spawn the base card prefab.
                    GameObject cardInstance = GameResources.Instantiate(cardPrefab);
                    // All decorators will be notified of the CardInventoryAsset.
                    Decorators.Distribute(cardInstance, cardInventoryAsset);
                }
            }
        }
    }

Inject SaveData directly
You can also implement an IDecorator<SaveDataObject> on the CardSpawner and get the SaveData from any other player or NPC data passed through.

Image Decorator

Then we will use IDecorator<CardInventoryAsset> to spawn the card graphic:

using GameSuite;
 
namespace Game
{
    public class CardImageDecorator : SerializedMonoBehaviour, IDecorator<CardInventoryAsset>
    {
        private GameObject cardInstance;
 
        public void Decorate(CardInventoryAsset card)
		{
            if (cardInstance != null)
            {
                // If a previous graphic has been spawned, destroy the old one first.
                Destroy(cardInstance);
            }
            
            cardInstance = GameResources.Instantiate(card.Graphic, transform);
        }
	}
}

Title Decorator

Lets also set the title of the card using the same IDecorator<CardInventoryAsset>:

using GameSuite;
 
namespace Game
{
    public class CardTitleDecorator : SerializedMonoBehaviour, IDecorator<CardInventoryAsset>
    {
        [SerializeField]
        private UnityStringEvent onSetTitle;
        
        public void Decorate(CardInventoryAsset card)
		{
            onSetTitle.Invoke(card.Title);
        }
	}
}

Description Decorator

Now the description as well to really start to understand how easy it is to implement, again using the same IDecorator<CardInventoryAsset>:

using GameSuite;
 
namespace Game
{
    public class CardDescriptionDecorator : SerializedMonoBehaviour, IDecorator<CardInventoryAsset>
    {
        [SerializeField]
        private UnityStringEvent onSetDescription;
        
        public void Decorate(CardInventoryAsset card)
		{
            onSetDescription.Invoke(card.Description);
        }
	}
}

Rarity Decorator

Instantiate the rarity of the card, which could for example be the card outline in different colors and styles:

using GameSuite;
 
namespace Game
{
    public class CardRarityGraphicDecorator : SerializedMonoBehaviour, IDecorator<CardInventoryAsset>
    {
        private GameObject rarityInstance;
        
        public void Decorate(CardInventoryAsset card)
		{
            if (rarityInstance != null)
            {
                // If a previous graphic has been spawned, destroy the old one first.
                Destroy(rarityInstance);
            }
            
            if (!Libraries.Get<RarityLibrary>().TryGet(card.Rarity, out RarityAsset rarity))
            {
                return;
            }
            
            rarityInstance = GameResources.Instantiate(rarity.Graphic, transform);
        }
	}
}

Example 2

Let's take another example where the UI needs to display the total amount of an item or currency a player has in its inventory.

using GameSuite;
 
namespace Game
{
    public class InventoryDecorator : SerializedMonoBehaviour, IDecorator<SaveDataObject>
    {
        [Inventory]
        public Guid Item;
        private UnityIntEvent OnSetAmount;
        
        public void Decorate(SaveDataObject saveData)
		{
            if (saveData.TryGet(out InventorySaveData inventorySaveData))
            {
                OnSetAmount.Invoke(inventorySaveData.GetAmount(Item));
            }
        }
	}
}

The UnityEvent is called with the integer of how much of the specified inventory item this SaveDataObject has.

Note that this Decorator example accepts the entire SaveDataObject, not just the InventorySaveData. This allows for example showing a popup with any player profile data. Pass in the active player, a dummy character such as an NPC, or show any other online player data.

This popup could have:

  • Player name
  • Character model
  • Outfit
  • Inventory
  • Health
  • Battle history

The SaveDataObject which should be shown could be stored in the Blackboard, or for example by adding this script to the root to pass down the player SaveDataObject:

using GameSuite;
 
namespace Game
{
    public class PlayerSaveDataDistributor : SerializedMonoBehaviour
    {
        public void Start()
		{
            SaveDataService saveDataService = Services.Get<SaveDataService>();
            SaveDataObject saveData = saveDataService.PlayerSaveData;
            Decorators.Distribute(gameObject, saveData);
		}
	}
}

Decorator hierarchy
You can make many complex layers of decorators which distribute, which decorate, which distribute etc.

Multiple parameters

IDecorator supports up to 5 values.

This allow for very specific decorators implementations.

Example:

IDecorator<SaveDataObject, List<WeaponInventoryAsset>, IAction, int, string>

Avoid value types
Distributing basic data types such as: IDecorator<int>, IDecorator<string> etc. can be caught by different decorators and can mean completely different things.
Be mindful what you decorate, as it is originally designed to send specific data objects, like LibraryAssets, ConfigAssets, classes or structs.
We suggest creating a struct, add the specific value and send it to decorate like so:

public struct HealthData
{
    public int Health;
}
⚠️

Avoid parameter complexity
Using 5 parameters is not recommended, as this becomes way too specific in most cases.
Don't use Decorators just to call a specific method on a single component, then you might as well get the component directly.
Use it, if there are, or in the near future will be, multiple items which should decorate the data.

LibraryAsset decorator

In a lot of use cases you just want to distribute an existing LibraryAsset to all its child decorators, such as the top bar containing multiple currency prefabs. GameSuite includes a LibraryAssetDistributor just for this scenario. Select any LibraryAsset or Config to distribute using the dropdown.


Config inspector

Naming conventions

As you might get a lot of decorators, its easiest to start with the large denominator and work down from there, ending with Decorator like so:

  • InventoryAmountDecorator
  • InventoryGraphicDecorator
  • LevelNumberDecorator
  • LevelEnvironmentDecorator
  • LevelDifficultyDecorator