SingletonScriptableObject, can it be done ?

I’m current studying the “SingletonScriptableObject”, with some luck, finally make it work with editor & release.

here is the code that I’m using in my library, it may not prefect, but seem working for me.

I start from this article : http://baraujo.net/unity3d-making-singletons-from-scriptableobjects-automatically/

it’s a talk for using ScriptableObjects. with a nice example.

using System.Linq;  
using UnityEngine;

/// <summary>
/// Abstract class for making reload-proof singletons out of ScriptableObjects
/// Returns the asset created on the editor, or null if there is none
/// Based on https://www.youtube.com/watch?v=VBA1QCoEAX4
/// </summary>
/// <typeparam name="T">Singleton type</typeparam>

public abstract class SingletonScriptableObject<T> : ScriptableObject where T : ScriptableObject {  
    static T _instance = null;
    public static T Instance
    {
        get
        {
            if (!_instance)
                _instance = Resources.FindObjectsOfTypeAll<T>().FirstOrDefault();
            return _instance;
        }
    }
}

However I notices random error will throw, after 2 week, the root cause are the ScriptableObject haven’t *ACTIVE* yet.
the Resources.FindObjectsOfTypeAll<T> just not return the ScriptableObject that we needed.
finally I locate the problem usually happen when I first start up the unity project.
here is the workflow to make it happen.

  1. Write a script that will use SingletonScriptableObject in “UNITY_EDITOR” mode.
    anything up to you, [ExecuteInEditMode] or what ever you want.
    >> [ExecuteInEditMode]
    >> public class Test
    >> Update() { YOUR_SingletonScriptableObject.instance.foo(); } // something like this.
  2. close Unity3D, yeah. close it.
  3. Start Unity3D with project.
  4. put your Test.cs script on gameobject
  5. boom, null reference

however, if you do this before step (4), like click/select on the “YOUR_SingletonScriptableObject” in the project, the rest of the time will not trigger the error.
because Resources.FindObjectsOfTypeAll<T> now can return the correct instance of your scriptableObject.
therefore we need to implement another part of the code to solve this issue.
my idea is, put that into save record. playerPref…etc

follow code are directly copy from my library,

 

 

SingletonScriptableObject.cs

using System.IO;
using System.Linq;
using UnityEngine;
using Kit.IO;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace Kit
{
	/// <summary>
	/// For base singleton class for ScriptableObject
	/// <see cref="http://baraujo.net/unity3d-making-singletons-from-scriptableobjects-automatically/"/> </summary>
	/// <typeparam name="T"></typeparam>
	public abstract class SingletonScriptableObject<T> : ScriptableObject
		where T : ScriptableObject
	{
		private class SingletonAssetPath : Record<SingletonAssetPath>
		{
			public SingletonAssetPath() : base("SingletonAssetPath", FileSource.Application, "json") { }
		}

		private static T m_Instance = null;
		public static T Instance
		{
			get
			{
				// fast way to get instance if that object was *ACTIVE* before
				// fail case : the very first time load the project, and you haven't touch/select that scriptable object
				if (m_Instance == null)
				{
					m_Instance = Resources.FindObjectsOfTypeAll<T>().FirstOrDefault();
				}

				// it never loaded before, let's search it in record.
				// fail case : change the name of asset after *ACTIVE*, path in record will not match.
				if (m_Instance == null)
				{
					string path = SingletonAssetPath.GetString(typeof(T).Name, string.Empty);
					if (!string.IsNullOrEmpty(path))
					{
						string fileName = Path.GetFileNameWithoutExtension(path);
						m_Instance = Resources.Load<T>(fileName);
						if (m_Instance == null)
						{
							Debug.LogError(typeof(T).Name + " can not be found in path: " + path);
						}
						else
						{
							Debug.Log("SingletonScriptableObject<" + typeof(T).Name + "> Loaded");
						}
					}
				}

#if UNITY_EDITOR
				// Update the asset path what even it was, to ensure the record are correct.
				// prevent fail case change name after *ACTIVE*.
				UpdateAssetPathRecord();


				// we don't have that object in project, and we are under developing mode.
				// ask developer to create it.
				if (m_Instance == null && !EditorApplication.isPlaying)
				{
					bool isCreate = EditorUtility.DisplayDialog(
						"SingletonScriptableObject<" + typeof(T).Name + "> not found",
						"Instance not found, what do you want to do ?",
						"Create asset", "Search it");

					string path = string.Empty;
					if (isCreate)
					{
						path = ScriptableObjectUtility.CreateAsset<T>(
							title: "Create Singleton ScriptableObject <" + typeof(T).Name + ">",
							defaultName: typeof(T).Name,
							extension: "asset"
							);
					}
					else
					{
						path = EditorUtility.OpenFilePanelWithFilters("Search ScriptableObject <" + typeof(T).Name + ">",
							"Assets", new string[] { "*", "asset" });
					}

					if (!string.IsNullOrEmpty(path))
					{
						string fileName = Path.GetFileNameWithoutExtension(path);
						m_Instance = Resources.Load<T>(fileName);
						UpdateAssetPathRecord();
					}
				}
#endif
				return m_Instance;
			}
		}

		[System.Diagnostics.Conditional("UNITY_EDITOR")]
		private static void UpdateAssetPathRecord()
		{
#if UNITY_EDITOR
			if (m_Instance == null)
			{
				SingletonAssetPath.DeleteString(typeof(T).Name);
				SingletonAssetPath.Save();
			}
			else
			{
				string path = AssetDatabase.GetAssetPath(m_Instance);
				if (path != SingletonAssetPath.GetString(typeof(T).Name, string.Empty))
				{
					Debug.Log("Update <" + typeof(T).Name + "> singleton asset path To :" + path);
					SingletonAssetPath.SetString(typeof(T).Name, path);
					SingletonAssetPath.Save();
				}
				if (!path.ToLower().Contains("resources"))
				{
					Debug.LogError(typeof(T).Name + " must place into \"Resources\" folder.");
				}
			}
#endif
		}
	}
}

Record<T> are the library to handle the file I/O, you guys can replace by PlayerPref or any other methods that can save/load the string.
and ScriptableObjectUtility.cs you can locate in : http://wiki.unity3d.com/index.php?title=CreateScriptableObjectAsset
those are very useful tools for create scriptableObject. (don’t recreate wheel)

so enjoy, and if there is any problem feel free to discuss here.

 

and here is my test case, “SpeakerData”, are using the ScriptableObject to define the character id, name, texture.

 

發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *

*