Spells as ScriptableObject vs MonoBehaviour for spell storage and spell selection system

Hi Everyone!

My question is much more architectural.

I’m working on a spell selector system, where the player can choose 4 of the abilities of the selected character on the character selection screen, which they want to use in the game.

I have several ideas and I don’t know which is the most felxible and efficient.

My main problem is that I don’t know if writing the spells as ScriptableObjects or MonoBehaviour would be a better solution for this system. If I write the spells as ScriptableObject, then storing the spells becomes much easier and the spell selector script becomes more efficient, but my problem with this is that I don’t like to write complex logic in ScriptableObject, because I feel that I violate the principle of ScriptableObject. If I write the spells as MonoBehaviour, then I have to create a prefab for each spell, even if it is an instant cast spell and does not have a prefab, so that I can store it as a GameObject within the given scene. This even makes the spell selector script less efficient due to GetComponent() calls and I also feel that it is not optimal from a memory point of view due to the many empty prefab storage.

Please help me, because I just started game development and I don’t know the best practices yet. Thanks for reading this far!

ScriptableObjects or MonoBehaviours… why not both?

SOs store the data, MB mutate the data and present it to things in the engine to be realized.

ScriptableObject usage in RPGs:

Usage as a shared common data container:

Whatever you choose, know that it WILL eventually grow into a horrible tangled mess as your project develops. This is just How It Works™ and why we refactor regularly as we identify pain points. From my experience the best you can do is be prepared to pounce catlike on any refactoring opportunities as you go along so that you don’t paint yourself into a difficult corner.

1 Like

And lets not forget about our third friend, regular old C# objects!

My usual pattern for this type of thing is: scriptable objects for immutable data that very often generates/factories a (mutable) plain C# object, which is then managed by a monobehaviour system.

You ideally don’t want to be changing data within your scriptable objects (or other assets) at runtime, so instancing regular C# objects is useful as you don’t have to manage their lifetime; as opposed to other approaches that might include Instantiate()-ing your scriptable objects to ensure you’re not modifying your raw asset.

[SerializeReference] is also very powerful in these sorts of systems with the right inspector support.

4 Likes

Yes, I know the concept borh of them and if I would use Monos for the spell scripts I would use SO to store the spell data. I’m just wondering which is more efficient, if I write the spell logic in SO or if I write it in Mono.

But if I write private fields and getters in my SO then I don’t have to Instantiate() them right?

What’s the scale of your game? If you don’t plan to have thousands of active spell users at once then don’t even worry about it. Rendering or Physics will be a bottleneck long before such a mundane choice as this. Pick the one that is most efficient for you to develop.

I personally like to create spells/skills/weapons/attacks/etc as an abstract ScriptableObject that uses a common interface and strategy pattern to choose spell-specific logic. Then I can create subclass objects to actually provide (or implement) the strategies. The actual caster of the spells is then a MonoBehaviour that hold an abstract interface of a generic spell and casts by calling it’s generic Cast() method while passing itself in as a parameter so that the concrete SO has a reference to a context state to work with.

1 Like

That is the absolute LAST thing you should be concerned about if you haven’t started.

You want to focus on a system that a) solves the problems you have, and b) that you understand and can work with.

Besides, performance hits are going to come from ways that you cannot even imagine today, and it likely won’t be to do with how you store your data. Maybe, but it’s not super-likely.

1 Like

And don’t forget that when you instantiate your own plain, old C# objects, you automatically handle your own dependency injection by calling the constructor to instantiate the object! So, DI solved!

1 Like

Well depends if the spells have any data they need to internally mutate. Timers, tracking targets, etc. This is why I generally use C# objects for these things, as I don’t mind their internal data changing, and of course I don’t have to manage their lifetime as well.

Every few years a new “trend” happens, where someone (ab)uses something and it looks like a great idea, but later it turns out that it wasn’t that good. Using ScripableObject for gameplay logic was one of these ideas.

They should be used for data, which is immutable at runtime, and not for gameplay logic.
Sure it works, but it isn’t a good idea.

Adding the spell logic inside a MonoBehaviour is better, but depending on the game context it isn’t a good idea either, because a spell is not really a “game object” which has to be part of the world. A spell could spawn a projectile, which is part of the world, but the spell itself is no such “world object”, it’s just some code with logic, so it doesn’t make sense to create a prefab or MonoBehavior for it.

Using ScriptableObject for spell configuration data and then create a plain C# object to handle the logic, like spiney199 said, is a better way.

But that’s just my opinion, in the world of programming you can solve every problem with multiple solutions, you just have to pick one, use it and then learn if it fits your style or not.

1 Like

Can you cite any specific reasons for why it’s a bad idea? This advice came directly from Unity’s own Richard Fine and I’ve personally been using it for almost a decade now with no issues. At the end of the day it seems like simple polynorphism to me but with some serialization support in the inspector. And function calls through an interface as about as readonly as it gets.

Mind you, my experience comes as a solo so I can belive that my limited experience might not scale up so well when many people are involved. I am willing to enterain better ideas though and if you can suggest some better work flows I know I’m always trying to find faster and easier ways to get things done.

Well I said it can work, but as soon as you have mutable state data, you have to find workarounds like Instantiate a copy of the SO, reset it after playmode or store the state data somewhere else etc. which can also work but feels dirty to me and can be avoided if you don’t “abuse” them for this kind of logic in the first place.

Worst case is if you think you don’t have to store any state data anyways, so it’s fine to (ab)use SO for the gameplay logic and weeks later you want to implement a new kind of effect which suddently requires you to store some kind of state data, then you suddenly have to think how you can glue it in your system, which doesn’t support it natuarlly or you decide that you don’t add this kind of effect for the game.

But everyone has a different opinion about “clean” or “dirty” code, for example you can also make everything public and static, with multiple singleton “managers” which contain way too much logic and the game will work just fine, but most people would consider it bad code, even if it works fine for a small game. I just offer a different point of view about the SO topic, if someone considers this use case as abuse or not is up to the reader, in the end everyone has to make it’s own opinion and use whatever fits best for them.

I agree that instanced SO data is generally a rough thing to work with, which is why I and Kurt both suggested passing a context object to the SO. Usually this can be as simple as the MonoBehaviour itself. It can be as simple as:


public abstract class AbstractPlugAndPlayAction : ScriptableObject
{
    public abstract void Execute(IPlugAndPlayContext context);
}

public interface IPlugAndPlayContext
{
    string SomeString { get; }
    int SomeNumber { get; set; }
}

public class UserOfPlugAndPlay : MonoBehaviour, IPlugAndPlayContext
{
    public AbstractPlugAndPlayAction Action;

    public string SomeString { get; }
    public int SomeNumber { get; set; }

    void Update()
    {
        Action.Execute(this);
    }
}

Now you have a sharable and hot-swappable object that works with the inspector (at least it works with Odin, I can’t actually remember if the default one supports abstract classes). And you can further subclass or strategize the abstract SO to provide specific functionalities that all work through the same interface. Of course, the context object doesn’t have to be the MonoBehaviour itself if that’s not your cup of tea. It could just as easily be another object that isn’t derived from anything other than the interface. And the logic within the SO might be further delegated to a strategy-pattern system that uses raw C# classes rather than subclassing an abstract class. In fact, the only difference between this and a traditional polymorphic strategy pattern IS the use of an SO so as to make it serializable as an asset and assignable in the inspector. If you like data-drive stuff this is great. If you are more of a code-only kind of person then I guess this seems like unnecessary fluff. Personally, I like the inspector. It’s the main reason I stopped rolling my own games from scratch and started using Unity as my preferred engine.

1 Like