Skip to content

Instantly share code, notes, and snippets.

@DirtyHarryE
Last active March 6, 2023 18:18
Show Gist options
  • Save DirtyHarryE/379806a7beb17b0aef94d23fd0785974 to your computer and use it in GitHub Desktop.
Save DirtyHarryE/379806a7beb17b0aef94d23fd0785974 to your computer and use it in GitHub Desktop.
Universal Loader
/*
The Universal Loader automatically searches the project and adds scriptable objects that implement "ILoadable" to a list.
The problem this was meant to solve was to be able to access Scriptableobjects easily within code. One could have serialized
references wherever they are needed, but those can be changed and if code needs to access the SAME scriptable object, this
can be a problem. So the idea came to having a static reference to the scriptable object within it, but if the scriptable
object isn't loaded, its Awake() method is never called. Having a Universal Loader Scriptable object that is referenced
in the scene that automatically calls a method on that scriptable object that implements ILoadable will allow you to make
that reference quickly and easily.
*/
namespace ItalicPig.PaleoPines.Loading
{
public interface ILoadable
{
void Load();
int Priority { get; }
}
public interface ILoadable<T> : ILoadable
where T : UnityEngine.Object, ILoadable
{
}
}
#if UNITY_EDITOR
#define USE_EDITOR
#endif
using System.Collections.Generic;
using UnityEngine;
using System.Text;
using System;
using Object = UnityEngine.Object;
#if USE_EDITOR
using UnityEditor;
using UnityEditor.Build;
using System.Linq;
using ItalicPig.PaleoPines.Editor;
using UnityEditor.Build.Reporting;
#endif
namespace ItalicPig.PaleoPines.Loading
{
[CreateAssetMenu(menuName = "Italic Pig/Paleo/Loader", fileName = "Loader")]
#if USE_EDITOR
#if AUTO_INITIALISE
[InitializeOnLoad]
#endif
public class UniversalLoader : ScriptableObject, IPreprocessBuildWithReport
#else
public class UniversalLoader : ScriptableObject
#endif
{
private const string INTERVAL_PREF = "LOADER_UNITY_INTERVAL";
private const string TIME_PREF = "LOADER_UNITY_UPDATE_TIME_CHECK";
public static bool PauseImport = false;
public static bool IsLoaded { get; private set; } = false;
#region EDITOR
#if USE_EDITOR
int IOrderedCallback.callbackOrder => 0;
void IPreprocessBuildWithReport.OnPreprocessBuild(BuildReport report)
{
InitialiseLoaders(false, false);
}
[SettingsProvider]
public static SettingsProvider CreateMyCustomSettingsProvider()
=> new SettingsProvider("Preferences/Universal Loader", SettingsScope.User)
{
label = "Universal Loader",
guiHandler = (searchContext) =>
{
EditorGUILayout.LabelField("Time between Getting Loaders", EditorStyles.boldLabel);
EditorPrefs.SetFloat(INTERVAL_PREF,
EditorGUILayout.FloatField("Minutes",
Mathf.Max(0, EditorPrefs.GetFloat(INTERVAL_PREF, 30))));
if (EditorPrefs.HasKey(TIME_PREF))
{
EditorGUILayout.LabelField("Last Loader Refresh", EditorPrefs.GetString(TIME_PREF));
}
},
keywords = new HashSet<string>(new[] { "Universal Loader" })
};
#if AUTO_INITIALISE
[MenuItem("CONTEXT/UniversalLoader/Test Auto-Initialise (Time check)")]
public static void AutoInitialise()
{
if (PauseImport)
{
return;
}
if (EditorApplication.isPlaying)
{
return;
}
if (EditorPrefs.HasKey(TIME_PREF))
{
string timeString = EditorPrefs.GetString(TIME_PREF);
try
{
DateTime dateTime = DateTime.Parse(timeString);
TimeSpan span = DateTime.Now.Subtract(dateTime);
if (span.TotalMinutes < EditorPrefs.GetFloat(INTERVAL_PREF, 5))
{
return;
}
}
catch (FormatException)
{
return;
}
}
string nowStr = DateTime.Now.ToString();
EditorPrefs.SetString(TIME_PREF, nowStr);
InitialiseLoaders(true);
}
#endif
[MenuItem("CONTEXT/UniversalLoader/Test Initialise")]
public static void InitialiseLoaders() => InitialiseLoaders(true);
public static void InitialiseLoaders(bool cancellable, bool trimExisting = true)
{
Debug.Log("Refreshing Loaders");
UniversalLoader[] loaders = AssetDatabase.FindAssets("t:UniversalLoader")
.Select(AssetDatabase.GUIDToAssetPath)
.OrderBy(s => s)
.Select(AssetDatabase.LoadAssetAtPath<UniversalLoader>)
.ToArray();
Object[] loadables = trimExisting
? GetLoadablesInProject(cancellable, loaders)
: GetLoadablesInProject(cancellable);
if (loadables == null || loadables.Length <= 0)
{
return;
}
IEnumerable<SerializedObject> serializedObjects = loaders.Select(u => new SerializedObject(u));
foreach (SerializedObject serializedObject in serializedObjects)
{
SerializedProperty arr = serializedObject.FindProperty("objects");
if (NeedsUpdating(serializedObject, loadables))
{
arr.arraySize = loadables.Length;
for (int i = 0; i < loadables.Length; i++)
{
SerializedProperty entry = arr.GetArrayElementAtIndex(i);
entry.objectReferenceValue = loadables[i];
}
serializedObject.ApplyModifiedProperties();
}
}
}
private static bool NeedsUpdating(SerializedObject loader, Object[] loadables)
{
SerializedProperty arr = loader.FindProperty("objects");
if (arr.arraySize != loadables.Length)
{
return true;
}
var hash = new HashSet<Object>(loadables);
for (int i = 0; i < arr.arraySize; i++)
{
Object obj = arr.GetArrayElementAtIndex(i).objectReferenceValue;
if (obj == null)
{
return true;
}
if (hash.Contains(obj))
{
hash.Remove(obj);
}
}
return hash.Count > 0;
}
public static Object[] GetLoadablesInProject(bool cancellable, params UniversalLoader[] loaders)
{
try
{
Type loaderType = typeof(ILoadable);
IEnumerable<Type> loadingTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.Where(t => loaderType.IsAssignableFrom(t) && !t.IsAbstract && !t.IsGenericType);
string search = string.Join(" ", loadingTypes.Select(t => "t:" + t.Name).ToArray());
var list = new SortedList<Object>(comparer: CompareLoadables);
var pathnameHash = new HashSet<string>();
var guidHash = new HashSet<string>();
var instanceIDHash = new HashSet<int>();
int i, j;
for (i = 0; i < loaders.Length; i++)
{
IEnumerable<Object> alreadyLoaded = new SerializedObject(loaders[i]).FindProperty("objects")
.GetPropertiesInArray()
.Select(o => o.objectReferenceValue)
.Where(o => o != null);
foreach (Object obj in alreadyLoaded)
{
int instanceID = obj.GetInstanceID();
if (!instanceIDHash.Contains(instanceID))
{
instanceIDHash.Add(instanceID);
}
string path = AssetDatabase.GetAssetPath(obj);
if (!pathnameHash.Contains(path))
{
pathnameHash.Add(path);
}
string guid = AssetDatabase.AssetPathToGUID(path);
if (!guidHash.Contains(guid))
{
guidHash.Add(guid);
}
list.Add(obj);
}
}
string[] guids = AssetDatabase.FindAssets(string.IsNullOrEmpty(search) ? "t:ScriptableObject" : search);
float max = guids.Length;
for (i = 0; i < guids.Length; i++)
{
if (guidHash.Contains(guids[i]))
{
continue;
}
guidHash.Add(guids[i]);
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
if (pathnameHash.Contains(path))
{
continue;
}
pathnameHash.Add(path);
if (cancellable)
{
if (EditorUtility.DisplayCancelableProgressBar("Getting Loaders", path, i / max))
{
return null;
}
}
else
{
EditorUtility.DisplayProgressBar("Getting Loaders", path, i / max);
}
try
{
Object[] objects = AssetDatabase.LoadAllAssetsAtPath(path);
for (j = 0; j < objects.Length; j++)
{
if (objects[j] == null)
{
continue;
}
int instanceID = objects[j].GetInstanceID();
if (instanceIDHash.Contains(instanceID))
{
continue;
}
instanceIDHash.Add(instanceID);
if (objects[j] is ILoadable || objects[j].GetType().IsSubclassOf(typeof(ILoadable)))
{
list.Add(objects[j]);
}
}
}
catch (Exception e)
{
Debug.LogError("Error during Universal Loader");
Debug.LogError(e);
}
}
EditorUtility.DisplayProgressBar("Getting Loaders", "Finalising", 1);
return list.Distinct().ToArray();
}
catch (System.Exception e)
{
Debug.LogError(e);
return null;
}
finally
{
EditorUtility.ClearProgressBar();
}
}
private static int CompareLoadables(Object a, Object b)
{
var lA = a as ILoadable;
var lB = b as ILoadable;
if (lA == lB)
{
return 0;
}
if (lA.Priority != lB.Priority)
{
return lA.Priority.CompareTo(lB.Priority);
}
string xType = lA.GetType().Name;
string yType = lB.GetType().Name;
if (xType != yType)
{
return xType.CompareTo(yType);
}
return GetObjectString(lA as Object).CompareTo(GetObjectString(lB as Object));
}
private static string GetObjectString(Object o)
{
if (o == null)
{
return string.Empty;
}
return AssetDatabase.GetAssetPath(o) + "_" + o.name;
}
#if AUTO_INITIALISE
static UniversalLoader()
{
EditorApplication.update += OnUpdate;
void OnUpdate()
{
EditorApplication.update -= OnUpdate;
AutoInitialise();
}
}
#endif
#endif
#endregion
[SerializeField]
private Object[] objects = Array.Empty<Object>();
public ILoadable[] Loadables { get; private set; } = Array.Empty<ILoadable>();
public void DoLoad()
{
if (IsLoaded)
{
return;
}
IsLoaded = true;
if (Loadables == null || Loadables.Length == 0)
{
Loadables = GetLoadables();
}
var builder = new StringBuilder();
builder.AppendLine("Loading");
for (int i = 0; i < Loadables.Length; i++)
{
ILoadable l = Loadables[i];
l.Load();
builder.AppendLine(l.ToString());
}
Debug.Log(builder.ToString());
}
public ILoadable[] GetLoadables()
{
var builder = new StringBuilder();
builder.AppendLine("Get Loadable");
var list = new List<ILoadable>();
for (int i = 0; i < objects.Length; i++)
{
try
{
if (objects[i] == null)
{
builder.Append("FAIL : [").Append(i).Append("] - IS NULL").AppendLine();
continue;
}
var loadable = objects[i] as ILoadable;
if (loadable != null)
{
list.Add(loadable);
builder.Append("Success : [").Append(i).Append("] - ").Append(loadable.ToString()).AppendLine();
}
else
{
builder.Append("FAIL : [").Append(i).Append("] - ").Append(objects[i].ToString()).Append(" :: IS NOT LOADABLE").AppendLine();
}
}
catch (Exception e)
{
builder.Append("FAIL : [").Append(i).Append("] - ").Append(objects[i].ToString()).Append(" :: ").Append(e).AppendLine();
}
}
Debug.Log(builder.ToString());
return list.ToArray();
}
#if USE_EDITOR
private void Reset()
{
Object[] objs = GetLoadablesInProject(false);
if (objs != null && objs.Length >= 0)
{
objects = objs;
}
}
#endif
private void OnValidate()
{
var list = new List<Object>();
for (int i = 0; i < objects.Length; i++)
{
if (objects[i] == null || !(objects[i] is ILoadable))
{
continue;
}
list.Add(objects[i]);
}
objects = list.ToArray();
}
}
}
#if UNITY_EDITOR
#define USE_EDITOR
#endif
//#define DEBUG_LOG
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ItalicPig.PaleoPines.Loading;
using UnityEditor;
namespace ItalicPig.PaleoPines.Editor.Loading
{
#if USE_EDITOR
public class UniversalLoaderAssetPostprocessor : AssetPostprocessor
{
private static readonly HashSet<string> ignoredFiles = new HashSet<string>(new[]
{
"AudioManager",
"ClusterInputManager",
"DynamicsManager",
"EditorBuildSettings",
"GraphicsSettings",
"EditorSettings",
"InputManager",
"NavMeshAreas",
"PackageManagerSettings",
"Physics2DSettings",
"PresetManager",
"ProjectSettings",
"QualitySettings",
"TagManager",
"TimeManager",
"UnityConnectSettings",
"URPProjectSettings",
"VersionControlSettings",
"VFXManager",
"XRSettings",
"UniversalRenderPipelineAsset_Renderer"
});
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
if (Check(importedAssets, deletedAssets, movedAssets, movedFromAssetPaths))
{
UniversalLoader.InitialiseLoaders();
}
}
private static bool Check(params string[][] arrays)
{
HashSet<string> alreadyLoaded = null;
if (arrays == null || arrays.Length <= 0)
{
return false;
}
#if DEBUG_LOG
var builder = new StringBuilder();
#endif
try
{
const string assetPref = "Assets/";
const string assetExt = "asset";
#if DEBUG_LOG
builder.Append("Checking for changed assets");
#endif
for (int i = 0; i < arrays.Length; i++)
{
if (arrays[i] == null || arrays[i].Length <= 0)
{
#if DEBUG_LOG
builder.AppendLine().Append(i).Append("|- :: ").Append("IS NULL");
#endif
continue;
}
for (int j = 0; j < arrays[i].Length; j++)
{
#if DEBUG_LOG
builder.AppendLine().Append(i).Append('|').Append(j).Append(" :: ");
#endif
string value = arrays[i][j];
if (string.IsNullOrEmpty(value))
{
#if DEBUG_LOG
builder.Append("IS EMPTY");
#endif
continue;
}
#if DEBUG_LOG
builder.Append(value).Append(" - ");
#endif
string ext = Path.GetExtension(value);
bool match = ext.ToLower().Contains(assetExt);
if (!value.StartsWith(assetPref))
{
continue;
}
if (ignoredFiles.Contains(Path.GetFileNameWithoutExtension(value)))
{
continue;
}
if (match)
{
switch (i)
{
case 0:
case 2:
if (alreadyLoaded == null)
{
alreadyLoaded = new HashSet<string>();
IEnumerable<string> allLoaded =
AssetDatabase.FindAssets("t:UniversalLoader")
.Select(AssetDatabase.GUIDToAssetPath)
.Select(AssetDatabase.LoadAssetAtPath<UniversalLoader>)
.Select(l => new SerializedObject(l))
.Select(o => o.FindProperty("objects"))
.SelectMany(p => p.GetPropertiesInArray())
.Select(a => a.objectReferenceValue)
.Where(o => o != null)
.Select(AssetDatabase.GetAssetPath);
foreach (string loaded in allLoaded)
{
if (!alreadyLoaded.Contains(loaded))
{
alreadyLoaded.Add(loaded);
}
}
}
if (alreadyLoaded.Contains(value))
{
#if DEBUG_LOG
builder.Append("already loaded");
#endif
continue;
}
break;
}
#if DEBUG_LOG
builder.Append("matches: \"").Append(ext).Append("\"");
#endif
return true;
}
}
}
return false;
}
finally
{
#if DEBUG_LOG
UnityEngine.Debug.Log(builder.ToString());
#endif
}
}
}
#endif
}
using System.Linq;
using ItalicPig.PaleoPines.Loading;
using UnityEditor;
using UnityEngine;
namespace ItalicPig.PaleoPines.Editor.Loading
{
[CustomEditor(typeof(UniversalLoader))]
public class UniversalLoaderEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
SerializedProperty objectsProperty = serializedObject.FindProperty("objects");
if (objectsProperty.arraySize > 0)
{
IOrderedEnumerable<IGrouping<string, SerializedProperty>> grouped = objectsProperty.GetPropertiesInArray()
.Where(o => o != null)
.Where(o => o.propertyType == SerializedPropertyType.ObjectReference)
.Where(o => o.objectReferenceValue != null)
.GroupBy(p => p.objectReferenceValue.GetType().Name)
.OrderBy(delegate (IGrouping<string, SerializedProperty> g)
{
SerializedProperty p = g.FirstOrDefault();
Object o = p.objectReferenceValue;
if (!(o is ILoadable l))
{
return 0;
}
return l.Priority;
}
);
foreach (IGrouping<string, SerializedProperty> group in grouped)
{
EditorGUILayout.LabelField(group.Key, EditorStyles.boldLabel);
int c = 0;
SerializedProperty first = group.FirstOrDefault();
if (first != null)
{
var l = first.objectReferenceValue as ILoadable;
if (l != null)
{
EditorGUILayout.LabelField("Priority", l.Priority.ToString());
}
}
EditorGUI.indentLevel += 1;
foreach (SerializedProperty prop in group)
{
//EditorGUILayout.PropertyField(prop, new GUIContent("Element " + (++c).ToString()));
EditorGUILayout.ObjectField(new GUIContent("Element " + (++c).ToString()), prop.objectReferenceValue, typeof(Object), false);
}
EditorGUI.indentLevel -= 1;
EditorGUILayout.Space();
}
}
else
{
EditorGUILayout.HelpBox("No Loadables have been found.\n\nPress \"Find Loadables\" to automatically find all Loadables in the project.", MessageType.Warning);
}
if (GUILayout.Button("Find Loadables"))
{
objectsProperty.arraySize = 0;
Object[] objects = UniversalLoader.GetLoadablesInProject(false);
if (objects != null && objects.Length >= 0)
{
objectsProperty.arraySize = objects.Length;
for (int i = 0; i < objects.Length; i++)
{
objectsProperty.GetArrayElementAtIndex(i).objectReferenceValue = objects[i];
}
objectsProperty.serializedObject.ApplyModifiedProperties();
}
}
}
[MenuItem("CONTEXT/UniversalLoader/Refresh")]
private static void ContextRefresh(MenuCommand command)
{
if (command.context == null)
{
return;
}
var serializedObject = new SerializedObject(command.context);
SerializedProperty objectsProperty = serializedObject.FindProperty("objects");
objectsProperty.arraySize = 0;
Object[] objects = UniversalLoader.GetLoadablesInProject(false);
if (objects != null && objects.Length >= 0)
{
objectsProperty.arraySize = objects.Length;
for (int i = 0; i < objects.Length; i++)
{
objectsProperty.GetArrayElementAtIndex(i).objectReferenceValue = objects[i];
}
objectsProperty.serializedObject.ApplyModifiedProperties();
}
}
}
}
using ItalicPig.PaleoPines.Loading;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
namespace ItalicPig.PaleoPines.Editor.Loading
{
public class UniversalLoaderPreProcessBuild : IPreprocessBuildWithReport
{
public int callbackOrder => 0;
public void OnPreprocessBuild(BuildReport report)
{
try
{
EditorUtility.DisplayProgressBar("Finding Loaders", "", 0);
string[] guids = AssetDatabase.FindAssets("t:UniversalLoader");
if (guids == null || guids.Length <= 0)
{
ThrowError("Universal Loader Asset cannot be found!");
return;
}
bool foundSomethingToLoad = false;
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
EditorUtility.DisplayProgressBar("Checking Loaders", path, (i + 1f) / guids.Length);
UniversalLoader loader = AssetDatabase.LoadAssetAtPath<UniversalLoader>(path);
if (loader == null)
{
ThrowError("Universal Loader Asset cannot be found!");
return;
}
try
{
ILoadable[] loadables = loader.GetLoadables();
if (loadables.Length > 0)
{
foundSomethingToLoad = true;
}
}
catch (System.Exception e)
{
ThrowError("Error occured when loading UniversalLoader \"" + path + "\"\n" + e);
return;
}
}
if (!foundSomethingToLoad)
{
ThrowError("All loadables have nothing to load. This could mean they are corrupt");
}
}
finally
{
EditorUtility.ClearProgressBar();
}
}
private void ThrowError(string errorMessage)
{
EditorUtility.DisplayDialog("Error", errorMessage, "Ok");
throw new BuildFailedException(errorMessage);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment