If you've read my previous article on ScriptableObjects, you'll know they're far more than glorified data containers. Of course they're fantastic for storing weapon stats, enemy configurations, and game settings, but that's only the beginning.
One of the biggest "wait a minute sister" moments in my personal Unity journey was realizing that ScriptableObjects can also help shape the architecture of an entire project. They aren't just assets sitting quietly in your Project window; they can become communication hubs, registries, and reusable building blocks that make your code cleaner and easier to maintain.
In this mini-series, we're going to explore ten practical ScriptableObject patterns that I sometimes use in my own Unity projects. These aren't theoretical examples you'll only find in programming textbooks. They're patterns that solve real-world problems you'll encounter as your projects grows.
Learning all these patterns is important in my opinion. Even if you don't plan to use them now, you at least know which options are available - and perhaps you may find one useful in the future. All extra useful knowledge you can add to your programmer's toolbox.
In this first part, I'll cover:
Data Assets
Runtime Sets
Event Channels
Let's dive in.
1. Data Assets (The Foundation)
Let's start with the pattern every Unity developer should know.
Data Assets are the simplest and most common use of ScriptableObjects. Their sole purpose is to store shared configuration data that multiple objects can reference.
Imagine you're making an RPG with several different weapons.
Without ScriptableObjects, every weapon prefab might contain something like this:
public string weaponName;
public int damage;
public float attackSpeed;
public int value;
Now picture you have thirty different weapons. Every prefab contains its own copy of that data. Need to rebalance your swords? Time to open thirty prefabs, or spend an afternoon questioning your life choices.
Instead, create a ScriptableObject.
using UnityEngine;
[CreateAssetMenu(menuName = "Items/Weapon")]
public class WeaponData : ScriptableObject
{
public string weaponName;
public int damage;
public float attackSpeed;
public int value;
}
Now your weapon MonoBehaviour simply references the asset.
using UnityEngine;
public class Weapon : MonoBehaviour
{
[SerializeField] private WeaponData weaponData;
public void Attack()
{
Debug.Log($"Attacked for {weaponData.damage} damage.");
}
}
Suddenly your prefab becomes much simpler. It doesn't care what weapon it is. It only knows how to attack. The ScriptableObject decides what it attacks with.
This separation of behavior from configuration is one of the biggest architectural improvements you can make in Unity.
Why This Pattern Is So Useful
Instead of storing duplicate data hundreds of times, every object simply references the same asset.
Goblin
↓
GoblinData.asset
Goblin
↓
GoblinData.asset
Goblin
↓
GoblinData.asset
That means you only have to change the Goblin's health once and every Goblin updates automatically. So, no prefab hunting, no forgotten values and no accidentally creating an invincible Goblin because one prefab escaped your balancing pass. In my upcoming horror survival game Anomaly Hours: Forest Cabin I use scriptableObjects to store time-of-day information. For instance, the brightness, position and color of the sun.
Best Use Cases
Data Assets are perfect for things like:
Weapons
Armor
Enemies
NPC definitions
Abilities
Audio definitions
Loot items
Difficulty settings
If the information is mostly read rather than constantly modified during gameplay, it probably belongs in a Data Asset.
2. Runtime Sets
This is where ScriptableObjects start becoming genuinely interesting. Suppose your game contains fifty enemies. At some point, another system needs to know which enemies are currently alive.
Many developers write something like this:
Enemy[] enemies = FindObjectsOfType<Enemy>();
It works. Until you call it repeatedly. Searching an entire scene isn't free, especially as your project grows, and it's also unnecessary. The enemies already know when they're created and destroyed. So why not let them keep track of themselves?
That's exactly what a Runtime Set does.
Creating the Runtime Set
First, create a ScriptableObject that stores a list of active enemies.
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Runtime Sets/Enemy Set")]
public class EnemyRuntimeSet : ScriptableObject
{
public List<Enemy> Enemies = new();
}
Nothing fancy, just a shared list.
Registering Enemies
Whenever an enemy spawns, it registers itself.
public class Enemy : MonoBehaviour
{
[SerializeField] private EnemyRuntimeSet runtimeSet;
private void OnEnable()
{
runtimeSet.Enemies.Add(this);
}
private void OnDisable()
{
runtimeSet.Enemies.Remove(this);
}
}
Now every active enemy automatically appears inside the Runtime Set.
Accessing Active Enemies
Need to know how many enemies remain?
int enemyCount = runtimeSet.Enemies.Count;
Need the closest enemy? Loop through the list. Need to alert every enemy? You already have them. No scene searches. No expensive lookups, and no detective work. Simple.
Why This Pattern Scales So Well
As projects become larger, scene searches become increasingly difficult to manage. A Runtime Set turns your scene into a self-maintaining registry.
Objects join.
Objects leave.
Everyone always knows who's currently at the party. Even if one Goblin wasn't invited.
Other Great Runtime Set Ideas
The pattern works for almost anything.
Active enemies
NPCs
Collectibles
Checkpoints
Spawn points
Players
Cameras
Interactive objects
If something needs to know "everything currently alive," a Runtime Set is often the cleanest solution.
3. Event Channels
If Runtime Sets help objects discover one another then Event Channels can do the exact opposite and help them communicate without knowing each other exists! I implemented this in a project once and it seems to do the job. One big benefit of this ScriptableObject pattern is decoupling code.
Let's say your player dies.
Several things may need to happen like:
Show the Game Over screen.
Stop the music.
Play a sound.
Update statistics.
Disable player controls.
Save progress.
A beginner solution might look like this:
Player
↓
GameManager
↓
UIManager
↓
AudioManager
↓
SaveManager
Everything depends on everything else. The Player suddenly knows far too much about the rest of your game. It's basically become the office gossip.
Event Channels Solve This
Instead, create a ScriptableObject that represents an event.
using System;
using UnityEngine;
[CreateAssetMenu(menuName = "Events/Void Event")]
public class VoidEventChannel : ScriptableObject
{
public event Action OnRaised;
public void Raise()
{
OnRaised?.Invoke();
}
}
This asset now acts as a communication hub.
Raising the Event
When the player dies:
[SerializeField]
private VoidEventChannel playerDied;
private void Die()
{
playerDied.Raise();
}
That's it. The Player doesn't know who cares.
It simply announces:
"I'm dead."
A little dramatic perhaps but accurate.
Listening for Events
Now the UI simply subscribes.
[SerializeField] private VoidEventChannel playerDied;
private void OnEnable()
{
playerDied.OnRaised += ShowGameOver;
}
private void OnDisable()
{
playerDied.OnRaised -= ShowGameOver;
}
The Audio Manager can also subscribe. So can the Save Manager or the Achievement System. Nothing references each other directly. Everything stays nicely decoupled.
Why This Is Better
Imagine six systems listening for the same event.
With direct references, your Player would need six different dependencies. With an Event Channel the Player needs exactly one. Adding another listener requires changing...
Nothing.
Your architecture grows naturally instead of becoming an ever-expanding web of references.
Event Channels Aren't Magic (Beware! Read 👇🏻)
Like every pattern, they can be overused!
If every interaction in your game goes through Event Channels, debugging can become difficult because it's no longer obvious who is responding to which event. I had a project once where I implemented Event Channels for pretty much EVERYTHING! My thought was that decoupled code was the best code, until I forgot which event was called by what. Pretty frustrating if a little sound effect played and you have no idea when it came from. I eventually had to hardcode a debugger that would show on the console which event was raised and what was listening to it. I found it, but it took way too much time.
So, use them only for genuine broadcasts:
Player died.
Level completed.
Item collected.
Game paused.
Achievement unlocked.
For simple one-to-one communication, a direct method call is often still the clearest solution.
To Wrap Things Up
These three patterns form the foundation of many scalable Unity projects. We discussed:
Data Assets keep shared configuration in one place, reducing duplication and making balancing far easier.
Runtime Sets eliminate unnecessary scene searches by allowing objects to maintain their own shared registry automatically.
Event Channels decouple systems so they can communicate without becoming tightly connected to one another.
Individually, each pattern is useful.
Combined, they encourage a cleaner architecture that's easier to understand, easier to expand, and far less likely to turn into the infamous "spaghetti code" every developer hopes to avoid.
In the next article (Part 2), we'll continue exploring more advanced ScriptableObject patterns, including Variable Assets, Ability Definitions, and Item Databases (three techniques that can dramatically simplify gameplay systems while keeping your code flexible and maintainable).
In the meantime, ~happy coding!

No comments:
Post a Comment