- Home /
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?
Answer by LightStriker · Oct 07, 2013 at 06:52 PM
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();
}
}
Good thinking on saving the ScriptableObject on disk to be modifiable in the editor.
Answer by Jamora · Oct 07, 2013 at 05:17 PM
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();
Little problem... You're using a $$anonymous$$onoBehaviour, which forces the existence of a scene and a GameObject. I'm trying to get ride of empty scene that only hold data. I would like to also make all my levels playable without having to pass by a "Loading" scene first and without the need to add GameObject or managers to all my scenes.
You'll need a scene to be able to play your game. You will also need GameObjects to make a game. I don't see the problem.
Any scene will do to initialize the monostate, as long as it has a GameObject that contains an instance of DataHandler
. That GameObject could be an enemy prefab, or maybe the player. Any, or both, will do. Heck, have an instance of DataHandler in each GameObject, hide it in the inspector so you can access if from anywhere, like a Singleton.
Depending on how you want to initialize the monostate, it could start with an empty list of enemies, or it could maybe even instantiate a few...
I agree GameObject are needed... for a level. I just don't see why we should need them to have a scene dedicated only to loading other scene or even handling project-wide data.
I've implemented a solution with ScriptableObject saved as .asset. On top, I got a tool that make sure any level loaded in the editor is playable without passing by a "Loading" level. This setup also means if I add a new "manager", I don't have to add it to any scene at all as it exist as an Asset loaded in-game.
With $$anonymous$$onostates, you don't need a Loading scene; the data is initialized when the GameObject that contains an instance of its handler is loaded. That might happen in the main menu, perhaps when the first enemy is killed... maybe never.
If you found a solution that works for you, do post it and mark it as correct so future visitors may be helped as well.
I just did. Feel free to comment if you think what I did is stupid (it may very well be).