I’m currently in the process of moving some data out of my MonoBehaviour classes into some data files, and I’m torn between using ScriptableObjects and just keeping JSON files that I deserialize at runtime. Could someone walk me through the use-cases for each, and whether I’m correctly understanding the benefits and drawbacks of each?
As a super broad example, say I’m using an entity-component-system. A PistolEnemy is a GameObject with a RangedAttackComponent, a MovementComponent, and a HealthComponent. A TurretEnemy is a GameObject with all the same stuff, but without a MovementComponent. A KnifeEnemy is a PistolEnemy with a MeleeAttackComponent instead of a RangedAttackComponent, etc.
I want all these entities (meaning the components that constitute them) to load data from their respective data files. In trying to think of whether to use ScriptableObjects vs JSON files, I came up with a few different approaches to what those data files actually are:
- Data files are ScriptableObjects containing classes that inherit from abstract data. EntityData implements ‘health’ and ‘name’, RangedMobileEntityData inherits from EntityData and implements ‘range’ and ‘moveSpeed,’ etc. Every entity has one serialized reference to its respective ScriptableObject, and components access that ScriptableObject for data. It seems like managing this inheritance could quickly get out of hand, though—if a TurretEnemy and a PistolEnemy have the same RangedAttack component, how will they know whether their data file is RangedMobileEntityData or a RangedStationaryEntityData?
- Much like the above, but composition over inheritance. Data classes are top-level classes that implement interfaces (like IEntity, IRangedEntity, IMobileEntity, etc.). Each component maintains its reference to its data file as an instance of whatever interface it needs. So, say that each entity has a DataReference component with a serialized reference to its ScriptableObject set in the inspector. The RangedAttackComponent retrieves this ScriptableObject and casts it to IRangedEntity for its own purposes. This solves the inheritance problem, but still seems a little wobbly to me: giving every component its own reference to the data file is a lot of redundant data, and managing all those interfaces might still be more of a headache than necessary. ThrowingKnifeComponent and RifleComponent probably have different needs—does IRangedEntity implement properties for each? Do they each get their own interface? Those concerns aren’t the end of the world, but I’d rather be dedicating that energy to other parts of the project.
- JSON approach. EntityData is kept in a big file called EntityData.json with an entry for each individual entity. At runtime, this file is deserialized using JSONObject, and individual entities hold references to their respective data as a JSONObject, which each component then references whenever it needs to access its data. On the one hand, this is really straightforward in a way I appreciate—no messing with classes or interfaces, just plop information into a JSON Object and the components read it back. On the other hand, storing and accessing all those values via keys (i.e. strings) makes me a little nervous because of the dangers of hardcoding strings, plus having to deserialize a JSON object from a string for each entity in a scene seems much heavier than each entity having one reference to the same ScriptableObject.
Am I missing something obvious, or are these roughly the pros and cons of each approach?
Hi, ScriptableObjects are great at containing data that you want to access across the scenes at run-time, or even at edit-time without worrying about its life cycle, since they can be stored as assets in your project (they are basically the better version of the infamous singletons). They’re also very useful if you want to test modifying some parameters at play mode from inspector only without losing your last changes after you exit play mode, I also use them as some sort of messaging system between MonoBehaviours. However, they fail big time at keeping the changes you make from your MonoBehaviours to the data they contain! they will reset to their original values as soon as you exit the currently open scene. Now here where JSON finds it’s place! Serializing the ScriptableObject into a JSON file is mandatory if you want to use the changed values between sessions.
So In my opinion, if your goal is to only read some preset data without modifying it then go for ScriptableObjects, but if you want to read/write then use JSON serialization, optionally with a conjunction of ScriptableObjects.
Recently had to decide how I wanted to store my game’s enemy data. It came down to four potential options:
- Hard coded directly into C#
- Scriptable Objects
(1) Hard coding is obviously too restrictive beyond prototyping, and (3) XML is much more bulky and slow than (4) JSON, so that left (2) ScriptableObjects and (4) JSON as alternatives. After an intrinsic analysis I concluded that JSON would be better than Scriptable Objects for my particular situation…
Scriptable objects designed for direct use with Unity’s interface and API. Trying to get their information out to other external tools requires extra steps (like serializing them to JSON). By just using JSON from the start you can easily load and save from a single file between different tools without creating conversion algorithms.
If you change the name of a variable in Unity, its serialization doesn’t handle this well. In most cases it will just drop all of the data that was renamed (though you can use FormallySerializedAs if you remember before the change, but that won’t help if you’re just trying to move that data to a different variable name and still keep the previous name). If you use something like JSON or XML, you can easily use a text editor to make immediate changes to every instance that is required.
The easiest way to handle modding in games is to allow players access to your serialized, human-readable data so that they can make adjustments to it and then load it at runtime. This is not intuitively possible with Scriptable Objects without extra steps (again, like deserializing from XML or JSON).
I’ve not had good luck with Unity’s serialization over the past 8 years of using it. I’ve had many issues arise during development where Unity’s serialized data can become corrupted. Unity’s default way to handle serialization errors is usually to drop previously corrupt serialization without error and then re-save on top of it. This means that important information can be lost. Using custom serializer plug-ins (like the late FullInspector or the recent OdinInspector) can complicate these situations further. For this reason, I approach any native serializing in Unity (like ScriptableObjects) with a great amount of caution and skepticism.
My mantra is: “The best way to use Unity is to not use Unity.” Meaning if you have the option to solve a problem without Unity (like by serializing your own JSON files), then that is probably the best option.