Level Independant Data

Currently, in our projects, we have an empty level with a single GameObject that contains a collection of managers.

Frankly, I find that structure quite horrible for many reasons and I’m searching for something that is more level independent when it comes to project-wide data. I would like to save all my project-wide data as asset of ScriptableObject and as soon as the project load (editor or not), it would load that data.

Is there a way to setup a project to it would automatically load specific structure of data?

I’ve tried [InitializeOnLoad], but it is an Editor only attribute.

I’ve tried a self-initializing ScriptableObject, but Unity doesn’t like ScriptableObject being created that way.

I’ve tried a self-initializing singleton that does not derive from ScriptableObject, but Unity blocks everything that is not from his own thread.

I haven’t managed to find a way to create a GameObject on load to host a MonoBehaviour.

Or am I forced to always have an empty scene with only a single GameObject hosting managers?

You could use Monostates. They are a different take on Singletons: instead of having only one object forced on the system (the singleton), Monostates store the data as static, thus ensuring only one set of data will ever exist, but the object from which the data is accessed may be created with new. So essentially, you may be using a Monostate without even knowing it; impossible with Singletons.

The code looks like this:

/*The actual Monostate object*/
[System.Serializable]
public class DataSet : ScriptableObject{
	[SerializeField]
	public List<GameObject> allEnemies;
	/*all kinds of other important data*/
	
	
	void OnEnable(){
        DontDestroyOnLoad(this);
		if(IsFirstRun()){
			/**
             * This block is run if there is no serialized 
			 * state to restore. That means the first time 
			 * this Object is created. Possibly a bit redundant
             * because this object is supposed to be created only once
			 **/
		}
	}
	
	private bool IsFirstRun(){
		bool firstRun = false;

		if(allEnemies == null){
			allEnemies = new List<GameObject>();
			firstRun = true;
		}
        //successive checks would have firstRun = firstRun && true;
		return firstRun;
	}
}

/** 
 * The object which is used to access the Monostate, 
 * can be added to as many GameObjects as necessary
 **/
public class DataHandler : MonoBehaviour{

    private static DataSet monostate;
	
	void Awake(){
        monostate = (DataSet)Object.FindObjectOfType(typeof(DataSet));
		if(monostate == null)
            monostate = ScriptableObject.CreateInstance<DataSet>();
	}
	
	public List<GameObject> GetAllEnemies(){
		return monostate.allEnemies;
	}
}

You can now add DataHandler to any GameObject that needs to access the data, and still only have one set of data to access.

I find this method to be neater than creating singletons… sometimes.

If you want the monostate to be pretty much exactly like a singleton, in that you can access it anywhere, you can do this:

public static class GameObjectExtension {

	public static DataHandler GetDataHandler(this GameObject go){
		DataHandler result = go.GetComponent<DataHandler>();
		if(result == null)
			result = go.AddComponent<DataHandler>();
		result.hideFlags = HideFlags.HideInInspector;
		return result;
	}
}

This way you don’t even have to worry about adding the DataHandler to your prefabs manually. Using it would be as simple as

gameObject.GetDataHandler().GetAllEnemies();

While Jamora’s solution does work, it also doesn’t do everything I wanted. Namely, it’s not just about data but also what I call a manager. In this case, it’s a singleton that handle a specific job, like a SoundManager. It may needs to be updated or not. But mostly, I want that data to be available for the user across all level. I also want the user to create new scene without the need to ever drop a GameObject that run logic. Let’s call that “create and play”.

Another advantage of storing data outside a scene is that the user can modify it without having to load or save any scene or update any object. Clicking the .asset in the Universe folder bring the manager on the inspector and allows you to directly change its values.

First, I enforce a singleton pattern in a class that derive from ScriptableObject. It is only a safeguard as each Manager should exist only at once spot. I’m also a lazy bastard who hates to rewrite the same pattern over and over.

[Serializable]
public abstract class Manager : ScriptableObject 
{
    /// <summary>
    /// Update called by Universe
    /// </summary>
    public virtual void Update() { }

    /// <summary>
    /// Called when a manager is loaded.
    /// </summary>
    public virtual void Deserialize() { }
}

/// <summary>
/// Base manager class that inforces singleton pattern.
/// Your class should derive from this, not directly from the non-generic Manager.
/// </summary>
/// <typeparam name="T">Self</typeparam>
[Serializable]
public abstract class Manager<T> : Manager where T : Manager<T>
{
    private static T instance = null;

    public static T Instance
    {
        get 
        {
            if (instance == null)
                instance = ScriptableObject.CreateInstance<T>();

            return instance;
        }
    }

    protected Manager() { }

    /// <summary>
    /// Called when a deserialized version is loaded.
    /// </summary>
    public override void Deserialize()
    {
        instance = (T)this;
    }
}

Secondly, I have a Universe MonoBehaviour (yes, I didn’t find a way without it) that loads those Managers in run time. There is a “Loading” scene, but it only contains an empty Universe. Other scene created by the users does not, but a tool adds a hidden one automatically.

/// <summary>
/// Entry point of all game-wide serialized data.
/// The Universe is a self-regulating script.
/// When awaken, it loads all the possible game Managers existing in the Assemblies.
/// </summary>
[Serializable]
[AddComponentMenu("")]
public sealed class Universe : MonoBehaviour
{
    private List<Manager> managers = new List<Manager>();

    public Manager[] Managers
    {
        get { return managers.ToArray(); }
    }

    // Self initializing.
    private static Universe instance;

    public static Universe Instance
    {
        get { return instance; }
    }

    private bool initialized = false;

#if UNITY_EDITOR
    public delegate void NewManagerEventHandler(object sender, NewManagerEventArgs e);

    /// <summary>
    /// Fired when a new Manager type is found. Editor Only.
    /// </summary>
    public static event NewManagerEventHandler NewManager;

    public class NewManagerEventArgs
    {
        private Manager manager;

        public Manager Manager
        {
            get { return manager; }
        }

        public NewManagerEventArgs(Manager manager)
        {
            this.manager = manager;
        }
    }
#endif

    private void OnEnable()
    {
        Initialize();
    }

    private void Update()
    {
        foreach (Manager manager in managers)
            manager.Update();
    }

    public void Initialize()
    {
        if (instance == null)
            instance = this;
        else if (instance != null && instance != this)
            Destroy(gameObject);

        if (initialized)
            return;

        Deserialize();

        initialized = true;
    }

    private static void Deserialize()
    {
        if (instance == null)
            return;

        foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            foreach (Type type in assembly.GetTypes())
            {
                if (typeof(Manager).IsAssignableFrom(type) && !type.IsAbstract)
                {
                    Manager manager = Resources.Load("Universe/" + type.Name) as Manager;

                    // If a manager is not loaded, it's because it is a new one that was never serialized before.
                    // In all aspect, that should only happens within the scope of the Editor as a coder add a new Manager type.
                    if (manager == null)
                    {
#if UNITY_EDITOR
                        PropertyInfo info = type.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
                        manager = info.GetValue(null, null) as Manager;

                        if (manager != null && NewManager != null)
                            NewManager(Universe.Instance, new NewManagerEventArgs(manager));
#endif
                    }
                    else
                        manager.Deserialize();

                    if (manager != null && !ManagerExist(type))
                        Instance.managers.Add(manager);
                }
            }
        }
    }

    private static bool ManagerExist(Type type)
    {
        foreach (Manager manager in Instance.managers)
        {
            if (manager.GetType() == type)
                return true;
        }

        return false;
    }
}

Finally, the tool (editor only) that make sure the Universe always exists. Should a new Manager type be found, if a coder creates a new one, it serialize it as a .asset in the Resources folder so the Universe can load it in-game.

// <summary>
/// This simple tool is there to guaranty that one Universe exist at all time.
/// Should a new Manager be found, it saves it as an Asset.
/// </summary>
[InitializeOnLoad]
public class UniverseTool
{
    static UniverseTool()
    {
        EditorApplication.hierarchyWindowChanged += HierarchyChanged;
        Universe.NewManager += NewManager;
    }

    private static void NewManager(object sender, Universe.NewManagerEventArgs e)
    {
        CreateAsset<Manager>(e.Manager);
    }

    private static void HierarchyChanged()
    {
        GameObject go = GameObject.Find("Universe");

        if (go == null)
        {
            go = new GameObject("Universe");
            go.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector | HideFlags.NotEditable;
        }

        Universe universe = go.GetComponent<Universe>();

        if (universe == null)
            universe = go.AddComponent<Universe>();

        if (!Application.isPlaying)
            universe.Initialize();
    }

    /// <summary>
    /// TODO: Rework to show a display of all Managers contained in the Universe.
    /// </summary>
    [MenuItem("Tool/Show Universe")]
    public static void DisplayUniverse()
    {
        Selection.activeObject = Universe.Instance;
    }

    /// <summary>
    /// This saves the Manager in the Resources Folder for in-game retreival.
    /// </summary>
    public static void CreateAsset<T>(T asset) where T : ScriptableObject
    {
        string assetPathAndName = "Assets/Resources/Universe/" + asset.GetType().ToString() + ".asset";
        AssetDatabase.CreateAsset(asset, assetPathAndName);
        AssetDatabase.SaveAssets();
    }
}