Last active
March 14, 2021 10:21
-
-
Save Lazersquid/4f04327da0741f2e1dea5038026701f2 to your computer and use it in GitHub Desktop.
ScriptableObject Reference Cache / Database
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using Sirenix.OdinInspector; | |
using Sirenix.Serialization; | |
using UnityEngine; | |
public class Inventory : MonoBehaviour | |
{ | |
[SerializeField] private List<Item> items; | |
[SerializeField] private List<InventoryUpgrade> upgrades; | |
#region serialization related | |
[Required] | |
[SerializeField] private ScriptableObjectReferenceCache referenceCache; | |
[FolderPath(AbsolutePath = true, RequireExistingPath = true, UseBackslashes = true)] | |
[SerializeField] private string savegameFolder; | |
[SerializeField] private DataFormat savegameDataFormat = DataFormat.JSON; | |
private string SavegameFilePath => savegameFolder + "\\" + "savegame.txt"; | |
#endregion | |
private void Awake() | |
{ | |
referenceCache.Initialize(); | |
} | |
// load on enable | |
private void OnEnable() | |
{ | |
if (string.IsNullOrEmpty(savegameFolder) || !File.Exists(SavegameFilePath)) return; | |
var context = new DeserializationContext | |
{ | |
StringReferenceResolver = referenceCache | |
}; | |
var bytes = File.ReadAllBytes(SavegameFilePath); | |
var state = SerializationUtility.DeserializeValue<InventoryState>(bytes, savegameDataFormat, context); | |
items = state.Items; | |
upgrades = state.Upgrades; | |
} | |
// save on disable | |
private void OnDisable() | |
{ | |
if (string.IsNullOrEmpty(savegameFolder)) return; | |
var context = new SerializationContext | |
{ | |
StringReferenceResolver = referenceCache | |
}; | |
var state = new InventoryState() | |
{ | |
Items = items, | |
Upgrades = upgrades | |
}; | |
var bytes = SerializationUtility.SerializeValue(state, savegameDataFormat, context); | |
File.WriteAllBytes(SavegameFilePath, bytes); | |
} | |
} | |
[Serializable] | |
public class InventoryState | |
{ | |
public List<Item> Items; | |
public List<InventoryUpgrade> Upgrades; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
[CreateAssetMenu] | |
public class InventoryUpgrade : ScriptableObject, ISerializeReferenceByCustomGuid | |
{ | |
[SerializeField] private string guid; | |
public string Guid => guid; | |
#if UNITY_EDITOR | |
/// <summary> | |
/// Generate a random guid if it has no guid | |
/// </summary> | |
private void Reset() | |
{ | |
if (string.IsNullOrEmpty(guid)) | |
guid = UnityEditor.GUID.Generate().ToString(); | |
} | |
#endif | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
[CreateAssetMenu] | |
public class Item : ScriptableObject, ISerializeReferenceByAssetGuid | |
{ | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Sirenix.OdinInspector; | |
using Sirenix.Serialization; | |
using Sirenix.Utilities; | |
using UnityEditor; | |
using UnityEngine; | |
[CreateAssetMenu] | |
public class ScriptableObjectReferenceCache : ScriptableObject, IExternalStringReferenceResolver | |
{ | |
[FolderPath(RequireExistingPath = true)] | |
[SerializeField] private string[] foldersToSearchIn; | |
[InlineButton(nameof(ClearReferences))] [InlineButton(nameof(FetchReferences))] [LabelWidth(90)] [PropertySpace(10)] | |
[SerializeField] private bool fetchInPlaymode = true; | |
[ReadOnly] | |
[SerializeField] private List<SOCacheEntry> cachedReferences; | |
private Dictionary<string, ScriptableObject> guidToSoDict; | |
private Dictionary<ScriptableObject, string> soToGuidDict; | |
[ShowInInspector][HideInEditorMode] | |
public bool IsInitialized => guidToSoDict != null && soToGuidDict != null; | |
/// <summary> | |
/// Populate the dictionaries with the cached references so that they can be retrieved fast for serialization | |
/// </summary> | |
public void Initialize() | |
{ | |
if (IsInitialized) return; | |
#if UNITY_EDITOR | |
if(fetchInPlaymode) | |
FetchReferences(); | |
#endif | |
guidToSoDict = new Dictionary<string, ScriptableObject>(); | |
soToGuidDict = new Dictionary<ScriptableObject, string>(); | |
foreach (var cacheEntry in cachedReferences) | |
{ | |
guidToSoDict[cacheEntry.Guid] = cacheEntry.ScriptableObject; | |
soToGuidDict[cacheEntry.ScriptableObject] = cacheEntry.Guid; | |
} | |
} | |
#if UNITY_EDITOR | |
private void ClearReferences() | |
{ | |
cachedReferences = new List<SOCacheEntry>(); | |
} | |
/// <summary> | |
/// Searches for all scriptable objects that implement ISerializeReferenceByAssetGuid or ISerializeReferenceByAssetGuid and saves them in a list together with their guid | |
/// </summary> | |
private void FetchReferences() | |
{ | |
cachedReferences = new List<SOCacheEntry>(); | |
var assetGuidTypes = GetSoTypesWithInterface<ISerializeReferenceByAssetGuid>(); | |
var instancesWithAssetGuid = GetAssetsOfTypes<ISerializeReferenceByAssetGuid>(assetGuidTypes, foldersToSearchIn); | |
foreach (var scriptableObject in instancesWithAssetGuid) | |
{ | |
var assetGuid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(scriptableObject)); | |
cachedReferences.Add(new SOCacheEntry(assetGuid, scriptableObject)); | |
} | |
var customGuidTypes = GetSoTypesWithInterface<ISerializeReferenceByCustomGuid>(); | |
var instancesWithCustomGuid = GetAssetsOfTypes<ISerializeReferenceByCustomGuid>(customGuidTypes, foldersToSearchIn); | |
foreach (var scriptableObject in instancesWithCustomGuid) | |
{ | |
var guid = ((ISerializeReferenceByCustomGuid) scriptableObject).Guid; | |
cachedReferences.Add(new SOCacheEntry(guid, scriptableObject)); | |
} | |
} | |
/// <summary> | |
/// Get all types that derive from scriptable object and implement interface T | |
/// </summary> | |
private List<Type> GetSoTypesWithInterface<T>() | |
{ | |
return AssemblyUtilities.GetTypes(AssemblyTypeFlags.All) | |
.Where(t => | |
!t.IsAbstract && | |
!t.IsGenericType && | |
typeof(T).IsAssignableFrom(t) && | |
t.IsSubclassOf(typeof(ScriptableObject))) | |
.ToList(); | |
} | |
/// <summary> | |
/// Returns all scriptable objects that are of one of the passed in types and implement T as well. | |
/// </summary> | |
/// <param name="searchInFolders"> Optionally limit the search to certain folders </param> | |
private List<ScriptableObject> GetAssetsOfTypes<T>(IEnumerable<Type> types, params string[] searchInFolders) | |
{ | |
return types | |
.SelectMany(type => | |
AssetDatabase.FindAssets($"t:{type.Name}", searchInFolders)) | |
.Select(AssetDatabase.GUIDToAssetPath) | |
.Select(AssetDatabase.LoadAssetAtPath<ScriptableObject>) | |
.Where(scriptableObject => scriptableObject is T) // make sure the scriptable object implements the interface T because AssetDatabase.FindAssets might return wrong assets if types of different namespaces have the same name | |
.ToList(); | |
} | |
#endif | |
#region Members of IExternalStringReferenceResolver | |
public bool CanReference(object value, out string id) | |
{ | |
EnsureInitialized(); | |
id = null; | |
if (!(value is ScriptableObject so)) | |
return false; | |
if (!soToGuidDict.TryGetValue(so, out id)) | |
id = "not_in_database"; | |
return true; | |
} | |
public bool TryResolveReference(string id, out object value) | |
{ | |
EnsureInitialized(); | |
value = null; | |
if (id == "not_in_database") return true; | |
var containsId = guidToSoDict.TryGetValue(id, out var scriptableObject); | |
value = scriptableObject; | |
return containsId; | |
} | |
public IExternalStringReferenceResolver NextResolver { get; set; } | |
private void EnsureInitialized() | |
{ | |
if (IsInitialized) return; | |
Initialize(); | |
Debug.LogWarning($"Had to initialize {nameof(ScriptableObjectReferenceCache)} lazily because it wasn't initialized before use!"); | |
} | |
#endregion | |
} | |
[Serializable] | |
public class SOCacheEntry | |
{ | |
[SerializeField] private string guid; | |
public string Guid => guid; | |
[SerializeField] private ScriptableObject scriptableObject; | |
public ScriptableObject ScriptableObject => scriptableObject; | |
public SOCacheEntry(string guid, ScriptableObject scriptableObject) | |
{ | |
this.guid = guid; | |
this.scriptableObject = scriptableObject; | |
} | |
} | |
public interface ISerializeReferenceByAssetGuid | |
{ | |
} | |
public interface ISerializeReferenceByCustomGuid | |
{ | |
string Guid { get; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment