Skip to content

Instantly share code, notes, and snippets.

@Lazersquid
Last active March 14, 2021 10:21
Show Gist options
  • Save Lazersquid/4f04327da0741f2e1dea5038026701f2 to your computer and use it in GitHub Desktop.
Save Lazersquid/4f04327da0741f2e1dea5038026701f2 to your computer and use it in GitHub Desktop.
ScriptableObject Reference Cache / Database
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;
}
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
}
using UnityEngine;
[CreateAssetMenu]
public class Item : ScriptableObject, ISerializeReferenceByAssetGuid
{
}
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