I love ScriptableObjects.
However, they don’t support property modifications/overrides like prefabs do.
I like making RPGs, and want an easier way to override certain settings for my game’s AI / NPCs / monsters, while keeping the rest of fields the same as the original/default settings.
My understanding is that MonoBehaviour and ScriptableObject are both the same native class in C++, just that a ScriptableObject isn’t attached to a GameObject/Transform pair.
But I’m wondering if it’d be bad design in this case to use a MonoBehaviour class for my AI settings so I can use prefabs and property overrides?
Is there anything I’m overlooking?
This seems too easy… haha.
I’d still use this sparingly, and prefer ScriptableObjects if possible, but use this instead perhaps where it’s important to override individual properties?
Any other considerations I’m missing?
The only thing I really hate when using prefabs for this use-case is that you can’t use Unity’s “Object Picker” for these prefab data containers the same way you can use it for a ScriptableObject.
I’ve also implemented systems with overridable properties that allowed nesting, basically manually implementing SO inheritance with overrides. For example, there was the CharacterStats ScriptableObject used for different characters, which all used the same type of data, but different values. At one point we realised that we wanted some sort of Orc template with different children that could all have unique values for x but share value y in case the Orc base asset ever changed. So that’s what I manually implemented. It’s very difficult to turn this into a general-purpose solution though and you really would be re-implementing prefabs.
Nothing wrong with using Prefabs though unless you can prove that their overhead is the only performance bottleneck left (highly unlikely!). You can also always fix any hard issues. In the unlikely case that you have a million prefabs, you could do some processing during the build process to replace all prefabs with specialized SOs or text files that merge all of the data and remove everything unused, for example.
The pros and cons are mostly practical in nature. As peter mentioned, the Object Picker doesn’t work nicely with prefabs, and its just a little dirty if designers can start playing around with transform data or tags on objects that don’t use them, but it also doesn’t hurt.
If the project has hundreds of stats that need to be shared and overridden in a complex way, I would consider just using text files with a simple format. Use the shared base file to set all values at runtime, then override all values if they are defined in the “child” text file.
That makes a lot of sense!
Yeah ideally, my solution would be ScriptableObjects with property modifications/overrides support haha.
But if I tried to implement it myself, it’d definitely be hacked together, and I’m trying to do less fighting on the core parts of how the engine is designed, so I decided I’d give this a shot.
On a similar thought process, while looking into serialization considerations, I looked into [SerializeReference] and found a few cool things.
It looks like there’s no good default inspector for it built in to Unity, but I found this awesome SerializeReferenceExtensions package that lets your inspector show all possible data types to change your field to. (Using their [SubclassSelector] attribute)
It seems that SerializeReference is geared towards serializing something inside one MonoBehaviour / ScriptableObject.
I couldn’t easily directly reference a [SerializeReference] field on another MonoBehaviour / ScriptableObject with drag n’ drop support like UnityEngine.Object references, but I can make a public C# getter and reference it in the code (without serializing the reference to it elsewhere outside of the script that defined the field).
The awesome news is, if you have a prefab with [SerializeReference] fields, it does support property modifications/overrides!
Figured I’d write these findings somewhere cause it’d help someone!
SerializeReference indeed sounds very promising because it make polymorphic serialization of regular C# types possible. However, I found there were still a couple of rough edges or things to be aware of:
If you rename a class used somewhere as a SerializeReference, all references break. This doesn’t happen with ScriptableObject or MonoBehaviour because they are assets identified by their GUID. It could be that Unity has solved this or is working on a solution in the newer Unity versions, though. Please let me know if anyone knows more.
If you have an array of Animals and add a new Cat, then resize the array or press the plus button to add another one, the last Cat instance is not duplicated. Instead, both array entries point to the same Cat instance, which is counter-intuitive for Unity user who got used to Unity’s limitation of serializing class types as if they were structs. Also it usually just isn’t what you want, you usually want to deep-copy items in an array.
Performance is a little slower and can cause serious issues if you go overboard with deeply nested or recursive structures. The previous constraints might have felt inconvenient, but they also forced us into a linear, more data-centric design that was fast to serialize.
If you SerializeReference as a Json file (e.g. produced by some editor tool user settings) and you load it during editor start, you might see many warnings in the console, because Unity might try to deserialize the data before the required assemblies are loaded/compiled. This also seems to be an issue with shared packages. It seems difficult to control the order of dependencies in these cases, because the code will compile fine, but the order of serialization vs compilation can be off. This usually wasn’t an issue with serialized assets, because while the editor was compiling you would sometimes see things like the warning on MonoBehaviour components that can’t be loaded because scripts had compile errors, etc. But with SerializeReference you get the warnings as console output, which is much more annoying.
This is solved with the (undocumented) [MovedFrom] attribute in the UnityEngine.Scripting.APIUpdating namespace.
Learnt this one the hard way I admit. Additionally, prior to 2021 I believe you need to apply this attribute to both the parent and all derived classes for it to start working again.
I’ve just released Asset Variants, which allows (almost) any asset to be a variant of any other asset (polymorphic or even entirely unrelated types are allowed; anything in common can be copied over):