3
\$\begingroup\$

I have been checking dozens of known 'solutions' for how to create a game manager in Unity, but none leave me satisfied.

I know I can create a singleton GameManager.Instance() that would have data to persist across multiple scenes, but it doesn't show up in the editor unless I assign it to a game object. The problem is, this game object would then be destroyed upon loading a new scene except if I use DontDestroyOnLoad().

However, when I use DontDestroyOnLoad(), and I open up a new scene in the editor, this GameManager Object doesn't exist in the new scene. The DontDestroyOnLoad() method assumes that the program entry point will always be the main/first scene, which is often not the case when working in the editor. This isn't great.

Example:

When I have events within scene2 (say a button or something), I cannot reference the gameobjects in scene1 in the Unity Editor (most importantly, the game manager).

Instead, I have to first attach a settings.cs script to my UI Button GameObject. When pressed, the Button will then call a function from the settings.cs void ChangeInput() {GameManager.Instance().ChangeInput();}. This feels quite clunky because the Button Element cannot reference the GameManager directly, since the Button is scene-specific but the gamemanager is not.

Is there a proper solution for issues like this?

\$\endgroup\$
  • \$\begingroup\$ Can you give us more information on what your game manager needs to do in your game? "Game Manager" is a pretty generic term for "whatever this game happens to need that doesn't belong in a more concrete/task-specific type". Depending on what you need, different solutions might be more or less appropriate, so these details will help you get useful, actionable answers. \$\endgroup\$ – DMGregory May 25 at 15:23
  • \$\begingroup\$ @DMGregory Let's say save certain settings like Input, game modes, etc. I don't really know all the details yet but I'm thinking everything that spans the entire application outside of specific scenes in Unity. I'm moving on from small scale projects into something more intermediate. \$\endgroup\$ – xcrypt May 25 at 15:29
  • \$\begingroup\$ If you can document your needs in more detail in your question, we can help you sort out whether this or other solutions meets those needs. If you haven't figured out your needs yet, then it might be premature to ask this question. \$\endgroup\$ – DMGregory May 25 at 15:45
3
\$\begingroup\$

Your DontDestroyOnLoad is exactly how I did it in Cognizer. I have hooks in the first script of each scene that will create the Game Manager object if it doesn't already exist. So I can start any scene, and everything works fine.

Cognizer doesn't work unless the player has selected game settings through MainMenu, so each scene has BootToMainMenu.cs attached:

public class BootToMainMenu : MonoBehaviour
{
    void Awake ()
    {
        GameObject globalObj = GameObject.Find("Globals");
        if(globalObj == null)
        {
            SceneManager.LoadScene("Splashes");
            return;
        }
    }
}

The Splashes scene has Booter.cs:

public class Booter : MonoBehaviour
{
    void Start ()
    {
        Application.targetFrameRate = 30;
        GameObject obj = GameObject.Find("Globals");
        if(obj == null)
        {
            obj = new GameObject("Globals");
            GameObject.DontDestroyOnLoad(obj);
            SharedData sharedData = obj.AddComponent<SharedData>();
            sharedData.Initialize();
            obj.AddComponent<AnalyticsHandler>();
            ...

And the Splashes scene has Splashes.cs which includes this:

void Update()
{
#if UNITY_EDITOR
    if(Input.GetMouseButtonDown(0))
    {
        SceneManager.LoadScene("MainMenu");
    }
#endif
    ...

So running any scene in Cognizer in the editor loads the Splashes scene, which prepares the DontDestroyOnLoad object, and allows a single click to skip straight to the main menu.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ oh I see so you use a "SceneMain" GameObject with an awake() function that creates the GameManager object or something like that? But in that case don't you need to execute/run the scene before it would show up in the editor? \$\endgroup\$ – xcrypt May 25 at 15:19
2
\$\begingroup\$

One way to handle functions that don't logically belong to any one scene is to put them into an object that doesn't live in a scene at all, but rather as an asset in your assets folder.

For instance, we could make our manager a ScriptableObject:

[CreateAssetMenu(fileName = "GameManager.asset", menuName = "Managers/Game Manager")]
public class GameManager : ScriptableObject
{
    public void ChangeInput(string input) {
        // TODO: act on the input change in some way 
        // (the more you tell us in your question, 
        // the better we can tailor our examples)
    }
}

Then you can make a new Game Manager via the Assets > Create menu or by right-clicking in your project window.

Drag that game manager into your button's OnClick event, and you can access its methods directly - no extra "glue" script needed:

Example of button OnClick calling GameManager.ChangeInput

One thing to watch for with assets that aren't inside your scene: changes to them that you make in play mode aren't automatically undone when you exit play mode. So if you want to maintain an initial state that's unaffected by runtime changes, you can do so with a nested struct or class.

[System.Serializable]
public struct GameManagerState {
   // TODO: put all your necessary state information here.
}

// Set this in the inspector to configure the starting state of your script,
// but treat it as "read-only" in your code, so it's preserved unmodified.
[SerializeField]
GameManagerState _initialState;

// Use this as your read-write copy at runtime to handle any mutation you need.
// Peek at this in the inspector to view runtime changes to your state.
[SerializeField]
GameManagerState _currentState;

void Awake() {
    // Shallow copy. Do a deep copy if you have mutable nested data 
    // that needs to be separated from the initial template.
    _currentState = _initialState;
}
| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

The current setup I'm using is to have a scene dedicated to GameManager objects (input, audio, game services, etc). Obviously that means I'm using a multi-screen setup. It looks like this:

  • Loading scene
  • Game Manager scene
  • Main menu scene
  • UI scene
  • Base level scene (used by all levels)
  • Unique scene per level

At runtime, the Loading scene scene is set as the default and loads any other needed scenes (SceneManager.LoadSceneAsync(names[index], LoadSceneMode.Additive)): Game Managers, UI then Menu.

In the editor, only the Loading scene needs to be loaded at all times then any of the other scenes I want to be working on. When I hit play, the Loading scene again takes care of loading the rest of the stuff as needed. There is one catch however: you must prevent the scenes opened in the editor from calling their children's Awake() before they should (the order being Loading -> Game Manager -> UI).

To solve that, I have a script attached to the root of each scene that detects PlayModeChanged and disable the children until the right moment. I've attached my version of that script below to give an idea.

Finally, here is how to load the scenes in the proper order. This is a SceneManager (not to be confused with Unity's SceneManager). It needs to be able to load a scene additively, remove loaded scenes and detect scenes already loaded. I've removed that code since for clarity.

   IEnumerator Start () {
      isLoadingAfterInitialLoad = false;

      yield return StartCoroutine(LoadSceneDirect(SceneType.Manager));
      yield return StartCoroutine(LoadSceneDirect(SceneType.UI));

      // are we already in table or menu for development?
      if( IsSceneActive(SceneType.Menu || IsSceneActive(SceneType.TableBase) )
         yield break;

     yield return StartCoroutine(LoadSceneDirect(SceneType.Menu));
    }

The EditorRunningHelper (needs a better name!) below

    /// <summary>
/// Utility to help run the game in the editor. On device, the game always starts at the Loading scene, but not so much
/// in the editor. We want to be able to test by running the scene we are in. This helps us do that by disabling entire scenes
/// that would normally be loaded much later.
/// 
/// Also, using multi-scene we are not garanteed anymore that the Awakes will be called in the proper order ACROSS scenes. It
/// appears that each scene present in the editor when Play is pressed will execute in an undeterminate order. In each scene, 
/// Awake call order is respected.
/// </summary>
public class EditorRunningHelper : MonoBehaviour {

   [SerializeField]
   bool waitForGameContextToBeReady = false, waitForUI = false, waitForTable = false, waitForTableBase = false;

   [SerializeField]
   bool debug = false;

   #if UNITY_EDITOR
   static EditorRunningHelper()
   {
      UnityEditor.EditorApplication.playModeStateChanged += PlayModeChanged;
      //      UnityEditor.EditorApplication.update += OnEditorUpdate;
   }

   //   static List<EditorRunningHelper> helpers;
   //   static bool hasStartedHelpers = true;
   //   static bool firstTime = false;

   static void PlayModeChanged(UnityEditor.PlayModeStateChange state)
   {

      if (!UnityEditor.EditorApplication.isPlaying && UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) {
         //         Debug.Log("About to enter play mode...");
         foreach (EditorRunningHelper helper in GameObject.FindObjectsOfType<EditorRunningHelper>()) {
            //            Debug.Log("Found: " + helper.name);
            helper.SetChildrenActive(false);
         }
      } else if (!UnityEditor.EditorApplication.isPlaying) {
         //         Debug.Log("Editor has stopped");
         foreach (EditorRunningHelper helper in GameObject.FindObjectsOfType<EditorRunningHelper>()) {
            helper.SetChildrenActive(true);
         }
      }
   }

   void SetChildrenActive(bool value)
   {
      foreach (Transform t in transform) {
         t.gameObject.SetActive(value);
      }
   }

   void Awake()
   {
      //      // if the scene was not loaded when we pressed play, we need to turn off the children. Let's just do it in all case, it doesn't hurt
      //      SetChildrenActive(false);
      StartCoroutine(Wait());
   }

   WaitForSeconds s = new WaitForSeconds(0.1f);

   IEnumerator Wait()
   {
      if( debug )
         Debug.Log(name + ": started waiting");
      if (waitForGameContextToBeReady && !Managers.GameContext.isReady)
         yield return s;

      if (waitForUI && FindObjectOfType<MadeOfBits.UI.UIRoot>() == null)
         yield return s;

      if (waitForTable && FindObjectOfType<PinballEngine4.TableSerializer>() == null)
         yield return s;

      if (waitForTableBase && FindObjectOfType<TableBaseTag>() == null)
         yield return s;

      if( debug )
         Debug.Log(name + ": done waiting");
      SetChildrenActive(true);
   }
#endif
}
| improve this answer | |
\$\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.