Search code examples
c#unity-game-enginescene-manager

How to preserve the full state of objects between scenes?


When loading a new Scene I run into the trouble of having my mouse drag not carry over to the next scene and having to re-click when the new scene is loaded.

I would like the mouse click to carry over seamlessly to the next scene without the player noticing and more generally I would like to know how best to preserve certain game objects and make them carry over to the next scene.

In essence, what I'm trying to do is have the entire game act like one big scene that the player can play trough but still be broken down into smaller scenes that could be accessed or transformed into levels at a later stage.

Thanks in advance.

This is the code I'm currently using

using UnityEngine;
using System.Collections;
using UnityEngine.Profiling;

public class MoveBall : MonoBehaviour
{
    public static Vector2 mousePos = new Vector2();

    private void OnMouseDrag()
    {    
        mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        transform.position = mousePos;   

        DontDestroyOnLoad(this.gameObject);     
    }
} 

Bellow is the script that is responsible for the loading of the scene:

public class StarCollision : MonoBehaviour
{

    private bool alreadyScored = false;

    private void OnEnable()
    {
        alreadyScored = false;
    }


    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.CompareTag("White Ball"))
        {
            if (!alreadyScored)
            {
                ScoreScript.scoreValue += 1;
                StartCoroutine(ChangeColor());

                alreadyScored = true;
            }

        }

        if (ScoreScript.scoreValue > 4)
        {
            SceneManager.LoadScene(1);
        }

    }


    private IEnumerator ChangeColor()
    {

        ScoreScript.score.color = Color.yellow;
        yield return new WaitForSeconds(0.1f);
        ScoreScript.score.color = Color.white;
        gameObject.SetActive(false);

    }
}

Solution

  • I think the main reason why it doesn't work is that you probably also have another Camera in the new Scene.

    The OnMouseDrag rely on the Physics system internally using the objects Collider and raycasts from the Camera. Now if you switch Scene I'ld guess the one Camera gets disabled so your drag gets interrupted.

    Also using LoadScene instead of LoadSceneAsync causes a visible lag and might also be related to the issue.


    I have a maybe a bit more complex solution but that is what I usually do:

    1. Have one Global Scene "MainScene"

    This Scene contains stuff like e.g. the MainCamera, global ligthning, global manager components that should never be destroyed anyway.

    2. Use additive async Scene loading

    You said you do not want your user to not note when the scene switches so I would recommend using SceneManager.LoadSceneAsync anyway.

    Then in order to not unload the before mentioned MainScene you pass the optional parameter LoadSceneMode.Additive. This makes the new Scene be loaded additional to the already present one. Then later you only have to exchange those by unloading the previously additive loaded scene.

    I created a very simple static manager for this:

    public static class MySceneManager
    {
        // store build index of last loaded scene
        // in order to unload it later
        private static int lastLoadedScene = -1;
    
        public static void LoadScene(int index, MonoBehaviour caller)
        {
            caller.StartCoroutine(loadNextScene(index));
        }
    
        // we need this to be a Coroutine (see link below)
        // in order to correctly set the SceneManager.SetActiveScene(newScene);
        // after the scene has finished loading. So the Coroutine is required 
        // in order to wait with it until the reight moment
        private static IEnumerator loadNextScene(int index)
        {
            // start loading the new scene async and additive
            var _async = SceneManager.LoadSceneAsync(index, LoadSceneMode.Additive);
    
            // optionally prevent the scene from being loaded instantly but e.g.
            // display a loading progress
            // (in your case not but for general purpose I added it)
            _async.allowSceneActivation = false;
            while (_async.progress < 0.9f)
            {
                // e.g. show progress of loading
    
                // yield in a Coroutine means
                // "pause" the execution here, render this frame
                // and continue from here in the next frame
                yield return null;
            }
    
            _async.allowSceneActivation = true;
            // loads the remaining 10% 
            // (meaning it runs all the Awake and OnEnable etc methods)
            while (!_async.isDone)
            {
                yield return null;
            }
    
            // at this moment the new Scene is supposed to be fully loaded
    
            // Get the new scene
            var newScene = SceneManager.GetSceneByBuildIndex(index);
    
            // would return false if something went wrong during loading the scene
            if (!newScene.IsValid()) yield break;
    
            // Set the new scene active
            // we need this later in order to place objects back into the correct scene
            // if we do not want them to be DontDestroyOnLoad anymore
            // (see explanation in SetDontDestroyOnLoad)
            SceneManager.SetActiveScene(newScene);
    
            // Unload the last loaded scene
            if (lastLoadedScene >= 0) SceneManager.UnloadSceneAsync(lastLoadedScene);
    
            // update the stored index
            lastLoadedScene = index;
        }
    }
    

    This MySceneManager is a static class so it is not attached to any GameObject or Scene but simply "lives" in the Assets. You can now call it from anywhere using

     MySceneManager.LoadScene(someIndex, theMonoBehaviourCallingIt);
    

    The second parameter of type MonoBehaviour (so basically your scripts) is required because someone has to be responsible for running the IEnumerator Coroutine which can't be done by the static class itself.

    3. DontDestroyOnLoad

    Currently you are adding any GameObject you dragged at any time to DontDestroyOnLoad. But you never undo this so anything you touched meanwhile will be carried on from that moment ... forever.

    I would rather use e.g. something like

    public static class GameObjectExtensions
    {
        public static void SetDontDestroyOnLoad(this GameObject gameObject, bool value)
        {
            if (value)
            {
                // Note in general if DontDestroyOnLoad is called on a child object
                // the call basically bubbles up until the root object in the Scene
                // and makes this entire root tree DontDestroyOnLoad
                // so you might consider if you call this on a child object to first do
                //gameObject.transform.SetParent(null);
                UnityEngine.Object.DontDestroyOnLoad(gameObject);
            }
            else
            {
                // add a new temporal GameObject to the active scene
                // therefore we needed to make sure before to set the
                // SceneManager.activeScene correctly
                var newGO = new GameObject();
                // This moves the gameObject out of the DontdestroyOnLoad Scene
                // back into the currently active scene
                gameObject.transform.SetParent(newGO.transform, true);
                // remove its parent and set it back to the root in the 
                // scene hierachy
                gameObject.transform.SetParent(null, true);
                // remove the temporal newGO GameObject
                UnityEngine.Object.Destroy(newGO);
            }
        }
    }
    

    This is an Extension Method which allows you to simply call

    someGameObject.SetDontDestroyOnLoad(boolvalue);
    

    on any GameObject reference.

    Then I changed your script to

    public class MoveBall : MonoBehaviour
    {
        public static Vector2 mousePos = new Vector2();
    
        // On mouse down enable DontDestroyOnLoad
        private void OnMouseDown()
        {
            gameObject.SetDontDestroyOnLoad(true);
        }
    
        // Do your dragging part here
        private void OnMouseDrag()
        {
            // NOTE: Your script didn't work for me
            // in ScreenToWorldPoint you have to pass in a Vector3
            // where the Z value equals the distance to the 
            // camera/display plane
            mousePos = Camera.main.ScreenToWorldPoint(new Vector3(
                Input.mousePosition.x,
                Input.mousePosition.y,
                transform.position.z)));
            transform.position = mousePos;
        }
    
        // On mouse up disable DontDestroyOnLoad
        private void OnMouseUp()
        {
            gameObject.SetDontDestroyOnLoad(false);
        }
    }
    

    And in your StarCollision script you only have to exchange

    SceneManager.LoadScene(1);
    

    with

    MySceneManager.LoadScene(2, this);
    

    Demo

    For a little demonstration I "faked" it using two simple scripts

    This one in the Main scene

    public class LoadFirstscene : MonoBehaviour
    {
        // Start is called before the first frame update
        private void Start()
        {
            MySceneManager.LoadScene(1, this);
        }
    }
    

    And this one in the other scenes

    public class LoadNextScene : MonoBehaviour
    {
        [SerializeField] private int nexSceneIndex;
    
        private void Update()
        {
            if (!Input.GetKeyDown(KeyCode.Space)) return;
    
            MySceneManager.LoadScene(nexSceneIndex, this);
        }
    }
    

    And have 3 Scenes:

    • Main: As mentioned contains

      • the MainCamera
      • a DirectionalLight
      • the LoadFirstScene

      enter image description here

    • test: contains

      • a MoveBall "Sphere"
      • the LoadNextScene

      enter image description here

    • test2: contains

      • a MoveBall "Cube"
      • the LoadNextScene

      enter image description here

    With the indexes matching the build settings (make sure Main is always at 0 ;) )

    enter image description here

    I can now switch between test and test2 using the Space key.

    If I drag one of the objects meanwhile I can carry it on into the next scene (but only 1 at a time). I can even take it on again back to the first scene in order to have e.g. two sphere objects I can play with ;)

    enter image description here