Over the last two articles, we've explored seven practical ways to use ScriptableObjects beyond simple data storage.
We've covered everything from Data Assets and Runtime Sets to Item Databases and Spawn Tables. Hopefully by now it's becoming clear that ScriptableObjects aren't just a convenient place to store numbers, colors or sprites, they're powerful architectural tools that can help keep your project organized as it grows.
In this final part, we'll look at three more advanced patterns that are particularly useful in larger projects.
Don't worry if some of these seem a little intimidating at first, you probably won't need all of them on your next game. In fact, you may never need them at all, but knowing about it is a great addition to your programmer's toolbox.
Remember, good architecture isn't about using every design pattern you know. It's about choosing the simplest solution that solves the problem in front of you.
Let's jump in.
8. The Factory Pattern
Let's say you're making a tower defense game. Every enemy spawns slightly differently.
Some enemies:
Walk
Fly
Explode
Split into smaller enemies
Spawn with different statistics
You could create one giant EnemySpawner filled with switch statements.
Or...
You could let each enemy define how it's created. That's where the Factory Pattern comes in.
Creating a Factory
Instead of spawning enemies directly, create a ScriptableObject responsible for creating them.
using UnityEngine;
public abstract class EnemyFactory : ScriptableObject
{
public abstract Enemy Create(Vector3 position);
}
Then create a concrete factory.
using UnityEngine;
[CreateAssetMenu(menuName = "Factories/Goblin Factory")]
public class GoblinFactory : EnemyFactory
{
[SerializeField]
private Enemy goblinPrefab;
public override Enemy Create(Vector3 position)
{
return Instantiate(goblinPrefab, position, Quaternion.identity);
}
}
Your spawner becomes wonderfully simple.
factory.Create(spawnPoint.position);
No switch or endless if statements, and no mysterious EnemyType enum with seventeen different values that nobody wants to touch anymore.
Why Use a Factory?
Factories are particularly useful when object creation becomes complicated.
Perhaps enemies require:
Different initialization
Random statistics
Equipment
AI configuration
Difficulty scaling
Instead of putting all of that logic inside your spawner, every factory handles its own setup so each class has one responsibility.
9. AI Behavior Assets
This is one of my favorite advanced uses when it comes to ScriptableObjects. Let's say every enemy has slightly different behavior;
One enemy patrols.
Another guards an area.
Another flees when injured.
Another charges directly at the player.
A beginner solution might look something like this.
EnemyAI
↓
if Goblin...
↓
if Skeleton...
↓
if Boss...
↓
if Flying Enemy...
↓
if...
Before long, your AI script becomes longer than some novels. Instead, move the behavior into assets.
Defining AI Behaviors
using UnityEngine;
public abstract class AIBehavior : ScriptableObject
{
public abstract void Execute(Enemy enemy);
}
Now create individual behaviors.
using UnityEngine;
[CreateAssetMenu(menuName = "AI/Patrol")]
public class PatrolBehavior : AIBehavior
{
public override void Execute(Enemy enemy)
{
enemy.Patrol();
}
}
Another behavior.
using UnityEngine;
[CreateAssetMenu(menuName = "AI/Chase")]
public class ChaseBehavior : AIBehavior
{
public override void Execute(Enemy enemy)
{
enemy.ChasePlayer();
}
}
The Enemy simply references the behavior.
[SerializeField] private AIBehavior behavior;
private void Update()
{
behavior.Execute(this);
} Why This Is Powerful
Need a new enemy, just create another behavior asset and then assign it. Done.
Need five enemies that patrol, well...they all share the same asset.
Need to replace patrol logic, simply modify one class.
Every enemy updates automatically.
This pattern becomes even more useful when combined with finite state machines, where each state references a different behavior asset.
10. Global Configuration Assets
Every game has values that almost every system needs.
Things like:
Gravity
Starting health
Experience curve
Difficulty modifiers
Economy settings
Audio settings
Gameplay tuning
Many beginners place these values inside a GameManager, and there's nothing wrong with that. Until the GameManager contains five hundred variables and suddenly then it becomes less of a manager...and more of a digital junk drawer.
Instead, place global configuration inside dedicated ScriptableObjects. Let me explain this one:
using UnityEngine;
[CreateAssetMenu(menuName = "Configuration/Game Settings")]
public class GameSettings : ScriptableObject
{
public float playerSpeed = 5f;
public float enemySpeedMultiplier = 1f;
public float masterVolume = 1f;
public int startingLives = 3;
}
Now every system simply references the configuration asset.
[SerializeField]
private GameSettings settings;
Much cleaner.
Splitting Configuration
As projects grow, you can divide configuration into multiple assets.
For example:
GameplaySettings.asset
GraphicsSettings.asset
AudioSettings.asset
EnemySettings.asset
PlayerSettings.asset
Each system only reads what it needs, no gigantic GameManager required.
Why Designers Love This
One of the biggest advantages is balancing. Suppose your game feels too easy, instead of opening multiple scripts you can just open one configuration asset and adjust a few values.
Programmers write the systems, designers tweak the numbers. Nobody accidentally deletes half the project while changing enemy damage.
When NOT to Use These Patterns
By now, ScriptableObjects probably sound like the answer to everything. Well, they're not. Like every design pattern, they only solve specific problems. Trying to force them everywhere usually creates more work than they save.
Don't Store Runtime State
This is probably the biggest mistake beginners make.
For example:
Player Health
Ammo Count
Current Score
Quest Progress
These values constantly change, and they're unique to a particular play session. Generally, they belong inside normal classes or MonoBehaviours.
ScriptableObjects are much better suited to shared configuration read-only than temporary game state.
Don't Replace Every Class
Sometimes, a regular C# class is exactly what you need. Not everything needs to become an asset. If an object is never reused, or shared, and only exists for one purpose, there's a good chance a normal class is the simpler solution.
Beware Shared Data
Remember that ScriptableObjects are shared assets. If twenty enemies reference the same ScriptableObject they all read the same data, and that's usually what you want.
But if you accidentally modify that data at runtime, every enemy immediately sees the change. Sometimes that's exactly what you need. Sometimes you've just made every goblin in your game twice as strong, and your players probably won't send thank-you cards.
Don't Ignore Simpler Solutions
Things like a direct reference, a serialized field or a method call is sometimes all you need. Not every problem requires a clever architecture!
One of the hardest lessons in software development is realizing that the most elegant solution is often the simplest one.
Final Thoughts
Alright that was a lot to cover. In this series we've covered ten ScriptableObject pattern, ranging from simple Data Assets to more advanced concepts like Runtime Sets, Event Channels, Factories, and AI Behaviors.
If there's one takeaway I'd like you to remember, it's this:
ScriptableObjects aren't valuable because they're ScriptableObjects. They're valuable because they encourage better separation of responsibilities.
They allow you to separate:
Data from behavior.
Configuration from implementation.
Communication from dependencies.
Object creation from object usage.
That's what makes them such a powerful part of Unity's toolbox. That being said, don't feel pressured to use every pattern you've learned here. Many successful indie games use nothing more than Data Assets and others rely heavily on Event Channels. Some projects never need Factories or AI Behavior assets at all! And that's perfectly fine.
Good architecture isn't measured by how many design patterns you've implemented. It's measured by how easy your project is to understand six months from now. If you can open your project after taking a long break and immediately understand what's going on, then you've probably made some good architectural decisions.
If, on the other hand, you find yourself staring at your own code wondering, "WTF is this??!"...
Congratulations. You've officially become a game developer! 🤪
~Happy coding!

No comments:
Post a Comment