I’m working on a game right now in which I want to serialize references to scriptableobjects, and potentially other assets, into my save file. (For pointing, e.g., to an “EnemyDefinition” asset describing a type of enemy currently being fought.)
I am working with Newtonsoft JSON, rather than the unity-builtin JSON utility.
Given a unity Object
, is there some way to access a unique ID or path to this asset at runtime? A run-time equivalent to the Editor’s pairing of AssetDatabase.GetAssetPath
and AssetDatabase.LoadAssetAtPath
.
If anyone has an alternative solution (maybe something other than a ScriptableObject workflow for storing things like enemy types, or a nicer way to save), I’m also open to that.
Don’t!
It’s really ugly practice - assuming one can succeed - to serialize UnityEngine.Object derived types yourself.
Several. As you may be aware, scenes have a different API in editor compared to runtime. Thus I use a wrapper SceneReference
which has serialized fields for the scene’s name and its path. These are available at runtime for scene loading purposes and referencing those in Inspector fields.
In the editor I use OnValidate
with #if UNITY_EDITOR
to update the name and path string fields from the SceneAsset (editor only) type. You could use a similar mechanism for any asset by keeping an up to date asset path string that the AssetDatabase provides to the script in the editor.
Alternatively, you could have a ScriptableObjectsRegistry of sorts. Also something I use. It’s a MonoBehaviour singleton to which you add SOs in a list. Then you can simply access SOs by index. Or name, or type, whatever you prefer.
Lastly, and generally advisable, is to not store fields directly in the SO (and in many cases MonoBehaviour too). It’s preferable in general to encapsulate similar fields in a struct (or class), like this for instance:
public class TheSO : ScriptableObject
{
public TheData Data;
public DebugData DebugData;
[Serializable]
public struct TheData
{
public int fieldThatUsedToBeAFieldInSO;
}
[Serializable]
public struct DebugData
{
public bool EnableDebugMode;
}
}
You can use struct or class. They can be nested in the SO or outside, doesn’t matter.
The good thing about this is that it allows you to send a complete set of data around, eg pass it into methods, without exposing the SO itself. Also these containers will be foldout items in the Inspector, so you needn’t use the [Header]
attribute to organize fields.
Separating the fields to their own types allow you to simply serialize TheData
with NewtonSoft, and generally bypassing all the quirks of UnityEngine.Object types such as not being able to new
them.
Thank you. I do feel like that third suggestion of serializing the contents goes against my intention, though - if I were to update the game to fix an issue with an enemy’s stats or something along those lines, their save file would still be storing a redundant copy of the old enemy stats. Not to mention if my ScriptableObject for enemies contains data like the specific sprites or prefabs to use…
I’ve ended up going with your second suggestion, keeping a ScriptableObjectsRegistry singleton. I’ve made a special “SaveableSOConverter” for Newtonsoft JSON that saves the given ScriptableObject by its index into that singleton’s array. I’ve yet to put it through its paces, but I do think it’s a good solution, if not a bit laborious to have to update the registry.
You could just make a database/dictionary with kvp mapping to your SO and serialize the key for reference in the db/dict later. Seems a little easier to me, but I think I don’t really understand the struggle here or why you would really need to do this.
Just be careful changing the list post release. If the indexes of existing SO change, the savegames will be broken.
I might look into changing the list into a map that uses the asset GUIDs, or something along those lines.
I’m sure I could get away with automating assembling the list at build-time somehow…
You would have introduce your own unique ID system and use that. Ideally you use an interface so objects can express their unique ID, and potentially you can use convertors or whatever API Newtonsoft.JSON has to convert these into said ID when serializing, and then look up the asset from the ID to restore the reference when de-serialising.
Though I never felt good about having this in my serialisation code, so I ended up developing a way to have ‘indirect references’ to assets. These wrap around the ID, and contain an implementation (expressed via an interface) which knows how to look up said asset from ID.
This system works well as I don’t need to do any extra work with serialisation, I just have to ensure any references to scriptable objects (or other assets) are done via these indirect references. It also works well with non-asset objects that need to be referenced indirectly, such as ones that are stored in a larger registry.
To explain my motivations: I’ve been planning to lean into a ScriptableObject-based workflow as some relatively recent Unity talks have espoused: ScriptableObjects for definitions for things like different enemy types, “ScriptableObjects as Enums”, etc… but it’s complicating saving state.
These talks go all the way back for eight years or so.
Saving state should still be straightforward.
I think we haven’t pointed out that perhaps you don’t even need to reference the SO in the savegame at all. Say you have an enemy and there are three types and each type has its own SO. How do you assign that SO in the first place?
If the assignment is static (SO reference in Inspector) you don’t need to serialize the SO.
If the assignment can be deducted from logic you could do the same when deserializing. For instance the enemy’s type or name BossEnemy
may load the corresponding “BossEnemy.asset” through the rule that type/name matches the asset name.
Also, if you have runtime modifiable data in the SO make sure to separate this from the design-time values. I split those in two, the SO contains a class like “AssetData”. I have a separate struct that contains the “RuntimeData” which is primarily if not exclusively value types, no managed objects. It gets initialized with an AssetData instance for cases where you may need to have a modifiable copy of a design-time value (eg hitpoints). You’d only have to save the RuntimeData which needn’t be a field in the SO.