5
\$\begingroup\$

I’ve tried a bunch of different implementations, and trawled everything online I could get my hands on. Everything has downsides, nothing plays nicely with how Unity wants you to work.

Let’s keep it simple. You want to track in-game time. You want this to be available as a ScriptableObject. You need a MonoBehaviour to keep the time updated. You want to ensure there’s just one such GameState ScriptableObject and one such GameStateManager MonoBehaviour. You don’t want to manually add the manager to all scenes.

Is there a standard and sane way to achieve this without resorting to black magic?


To keep this grounded, here’s the boilerplate ScriptableObject:

    [CreateAssetMenu(fileName = "GameState", menuName = "ScriptableObjects/GameState")]
    public class GameState : ScriptableObject {

        /// <summary>
        /// The time passed in game, in game time, in seconds.
        /// </summary>
        public float TimePassed { get; private set; } = 0.0f;

        /// <summary>
        /// Moves the in-game time and date forward.
        /// </summary>
        /// <param name="addedGameTime">Time to add, in in-game seconds.</param>
        public void MoveTimeForward(float addedGameTime) {
            // Move the time passed forward
            TimePassed += addedGameTime;
        }

    }

And MonoBehaviour:

    public class GameStateManager : MonoBehaviour {

    /// <summary>
    /// The state of the game.
    /// </summary>
    public GameState gameState;

    // Start is called before the first frame update
    void Start() {
        // If we use a Singleton, and not drag a reference using the editor:
        gameState = GameState.Instace;
        // Mark that this should not to be destroyed when loading new scenes
        DontDestroyOnLoad(gameObject);
    }

    // Update is called once per frame
    void Update() {
        // Update the in-game time
        gameState.MoveTimeForward(Time.deltaTime);
    }

}
\$\endgroup\$
  • 1
    \$\begingroup\$ Sidestepping from the question a little bit: I suggest learning Unity's new ECS framework. As a paradigm, it solves all these design problems that come up with the MonoBehaviour architecture. \$\endgroup\$ – S. Tarık Çetin Jan 29 at 21:57
  • \$\begingroup\$ Thanks. I understand it’s not production-ready or out of beta testing, right? \$\endgroup\$ – CamelCamelCamel Jan 29 at 22:21
  • \$\begingroup\$ Correct, officially it is still in the preview phase. However, some people are already using it in production, see here an example article: forum.unity.com/threads/… \$\endgroup\$ – S. Tarık Çetin Jan 30 at 11:23
3
+100
\$\begingroup\$

Reload-proof Singleton

There is no way without black magic, but here is that black magic ^_^:

using UnityEngine;
using System.IO;

#if UNITY_EDITOR
using UnityEditor;
#endif

#if UNITY_EDITOR
public static class ScriptableObjectUtility
{
    /// <summary>
    /// This makes it easy to create, name and place unique new ScriptableObject asset files.
    /// </summary>
    public static T CreateAsset<T>(string path, string name) where T : ScriptableObject
    {
        T asset = ScriptableObject.CreateInstance<T>();

        if (path == string.Empty)
        {
            path = "Assets";
        }
        else if (Path.GetExtension(path) != string.Empty)
        {
            path = path.Replace(Path.GetFileName(AssetDatabase.GetAssetPath(Selection.activeObject)), string.Empty);
        }

        string assetPathAndName = AssetDatabase.GenerateUniqueAssetPath(Path.Combine(path, name + ".asset"));

        AssetDatabase.CreateAsset(asset, assetPathAndName);

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        EditorUtility.FocusProjectWindow();
        Selection.activeObject = asset;

        return asset;
    }
}
#endif

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

// Reload-proof Singleton
[CreateAssetMenu(fileName = GameState.DEFAULT_GAME_STATE_NAME, menuName = "Game State", order = 202)]
public class GameState : ScriptableObject
{
    private const string DEFAULT_GAME_STATE_NAME = "Game State";

    public static GameState instance;
    public static GameState Instance
    {
        get { return instance ?? 
                (instance = Resources.Load<GameState>(GameState.DEFAULT_GAME_STATE_NAME)) ?? 
                (instance = ScriptableObjectUtility.CreateAsset<GameState>(Path.Combine("Assets", "Resources"), GameState.DEFAULT_GAME_STATE_NAME)); }
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void CreateGameStateManager()
    {
        new GameObject("[Game State Manager]", typeof(GameStateManager));
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameStateManager : MonoBehaviour
{
    private void Awake()
    {
        if (GameState.Instance == null) // Null check is important, it's also a way to initialize and find GameState on Awake to prevent spike during gameplay.
        {
#if UNITY_EDITOR
            Debug.LogError("Game State instance could neither be found nor created.");
#endif
        }
    }
}

Just Thoughts

The basic idea behind this is that ScriptableObjects preserve data between scenes (if I am not mistaken). So having only 1 instance ofGameStateshould keep things in check. It's automaticallylazyloaded fromResourceswhen addressed.[Game State Manager]is created before scene load, so it should be first to initialize (but I am not sure, I didn't test it) and is created automatically for every scene and deleted when scene is unloaded. If you load scenes inAdditive` way then you would have to make some manipulations to keep only 1 instance of it for both scenes, I didn't have time to test additive scene loading with it, but it's great approach, I will probably use it in a few months and update my answer to share more info.

Inspired by: Overthrowing the MonoBehaviour tyranny in a glorious ScriptableObject revolution

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.