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();
}
}