Issue saving between scenes with portals

Hi,
I am having an issue with the saving of the scene when going through the portals. I thought it was just something I have done wrong when following along, but I also get the same problem when I run your code from GitHub.

I have tried using versions 2018.4.7f1, 2019.2.21f1 and 2019.3.2f1.

When I enter the portal the scene changes to the next scene, but it seems to load multiple times. It looks like the Sandbox 2 scene is being loaded multiple times as it shows multiple time in the Hierarchy. When it has finished loading I am left with multiple instances of the portal from the old scene under the do not destroy on load object (please see image). The number of portal objects left here varies from run to run.

I also get multiple errors in the console (please see image), there is one for each portal left over in the do not destroy object

This error refers to this bit of code

When I start to run the code I make sure there is no existing save file.

Are you able to assist me to figure out what is going on here?

Thank you
Kind Regards
Leah

I had a similar issue a while back and was able to fix it, but I don’t recall off the top of my head if the issue was in the portal script or the saving system script. If you post your portal script in its entirety, I’ll compare it to what I have and we’ll see if we can’t solve your issue.

Hi Seth,
Thanks for your help :grinning:

I have copied my Portal Script below. I checked my saving system script against the one in the repo to make sure they are the same.

public class Portal : MonoBehaviour {
        enum DestinationIdentifier {
            A,
            B,
            C,
            D,
            E
        }

        [SerializeField] int sceneToLoad = -1;
        [SerializeField] Transform spawnPoint;
        [SerializeField] DestinationIdentifier destination;
        [SerializeField] float fadeOutTime = 1f;
        [SerializeField] float fadeInTime = 2f;
        [SerializeField] float fadeWaitTime = 0.5f;

        private void OnTriggerEnter(Collider other) {
            if (other.tag == "Player") {
                StartCoroutine(Transition());
            }
        }

        private IEnumerator Transition() {
            if (sceneToLoad < 0) {
                Debug.LogError("Scene to load not set.");
                yield break;
            }

            DontDestroyOnLoad(gameObject);

            Fader fader = FindObjectOfType<Fader>();
            SavingWrapper savingWrapper = FindObjectOfType<SavingWrapper>();
            PlayerController playerController = GameObject.FindWithTag("Player").GetComponent<PlayerController>();
            playerController.enabled = false;

            yield return fader.FadeOut(fadeOutTime);

            savingWrapper.Save();

            yield return SceneManager.LoadSceneAsync(sceneToLoad);
            PlayerController newPlayerController = GameObject.FindWithTag("Player").GetComponent<PlayerController>();
            newPlayerController.enabled = false;

            savingWrapper.Load();

            Portal otherPortal = GetOtherPortal();
            UpdatePlayer(otherPortal);

            savingWrapper.Save();

            yield return new WaitForSeconds(fadeWaitTime);
            fader.FadeIn(fadeInTime);

            newPlayerController.enabled = true;
            Destroy(gameObject);
        }

        private void UpdatePlayer(Portal otherPortal) {
            GameObject player = GameObject.FindWithTag("Player");
            player.GetComponent<NavMeshAgent>().enabled = false;
            player.transform.position = otherPortal.spawnPoint.position;
            player.transform.rotation = otherPortal.spawnPoint.rotation;
            player.GetComponent<NavMeshAgent>().enabled = true;
        }

        private Portal GetOtherPortal() {
            foreach (Portal portal in FindObjectsOfType<Portal>()) {
                if (portal == this) continue;
                if (portal.destination != destination) continue;

                return portal;
            }

            return null;
        }
}

Thanks again
Kind Regards
Leah

Hmm well, first thing I would check is that the spawnPoint of both portals is far enough away from the portal itself. Otherwise, you may have a situation where you very quickly bounce back and forth from portal to portal triggering OnTriggerEnter for each portal multiple times. Try this to double check:

        private void OnTriggerEnter(Collider other) {
            if (other.tag == "Player") {
                print("Player has entered portal " + gameObject.name); //add this line
                StartCoroutine(Transition());
            }
        }

Does the console print the debug line multiple times when you enter a portal?

Hi, I checked to make sure the spawn points are far from the portals. I changed the portal names so they are unique. When I added the log out put it shows that the first portal is being activated multiple times. It does not print the name of the 2nd portal so I don’t think it the player is bouncing between the two, just the first one is triggering multiple times. Which then calls the save method loads of times.


I see, then we should be able to implement a fix by disabling the collider of the portal just after you enter it (alternatively, you could disable the collider on the Player).

//in portal script....
        private void OnTriggerEnter(Collider other) {
            if (other.tag == "Player") {
                GetComponent<Collider>().enabled = false; //add this and test again .
                StartCoroutine(Transition());
            }
        }

I changed my Portal trigger to disable the portal trigger, I still saw the same result so I disabled the player collider as well and still get the issue.

        private void OnTriggerEnter(Collider other) {
            if (other.tag == "Player") {
                GetComponent<Collider>().enabled = false;
                other.GetComponent<Collider>().enabled = false;
                print("Player has entered portal " + gameObject.name); 
                StartCoroutine(Transition());
            }
        }

I did notice that as it was processing the portal the scene name at the top of the hierarchy tab showed Sandbox2, but when it finished it is actually showing the original scene name, although in the scene view it is showing the player in Sandbox2.

This image shows the player before he enters the first portal and the sandbox scene in the hierarchy.

This image shows after going through the portal in the scene view into sandbox2, but it still shows the original scene information in the hierarchy panel (and all the extra portals still in the do not delete section)

Gotcha … the next place to check is inside your saving system script. It sounds like you have a save/load loop happening in there. When you get a chance, post your SavingSystem.cs file.

Hi,
Please find below my SavingSystem script.

/// <summary>
        /// Will load the last scene that was saved and restore the state. This
        /// must be run as a coroutine.
        /// </summary>
        /// <param name="saveFile">The save file to consult for loading.</param>
        public IEnumerator LoadLastScene(string saveFile)
        {
            Dictionary<string, object> state = LoadFile(saveFile);
            int buildIndex = SceneManager.GetActiveScene().buildIndex;
            if (state.ContainsKey("lastSceneBuildIndex"))
            {
                buildIndex = (int)state["lastSceneBuildIndex"];
            }
            yield return SceneManager.LoadSceneAsync(buildIndex);
            RestoreState(state);
        }

        /// <summary>
        /// Save the current scene to the provided save file.
        /// </summary>
        public void Save(string saveFile)
        {
            Dictionary<string, object> state = LoadFile(saveFile);
            CaptureState(state);
            SaveFile(saveFile, state);
        }

        /// <summary>
        /// Delete the state in the given save file.
        /// </summary>
        public void Delete(string saveFile)
        {
            File.Delete(GetPathFromSaveFile(saveFile));
        }

        // PRIVATE

        private void Load(string saveFile)
        {
            RestoreState(LoadFile(saveFile));
        }

        private Dictionary<string, object> LoadFile(string saveFile)
        {
            string path = GetPathFromSaveFile(saveFile);
            if (!File.Exists(path))
            {
                return new Dictionary<string, object>();
            }
            using (FileStream stream = File.Open(path, FileMode.Open))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                return (Dictionary<string, object>)formatter.Deserialize(stream);
            }
        }

        private void SaveFile(string saveFile, object state)
        {
            string path = GetPathFromSaveFile(saveFile);
            print("Saving to " + path);
            using (FileStream stream = File.Open(path, FileMode.Create))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(stream, state);
            }
        }

        private void CaptureState(Dictionary<string, object> state)
        {
            foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>())
            {
                state[saveable.GetUniqueIdentifier()] = saveable.CaptureState();
            }

            state["lastSceneBuildIndex"] = SceneManager.GetActiveScene().buildIndex;
        }

        private void RestoreState(Dictionary<string, object> state)
        {
            foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>())
            {
                string id = saveable.GetUniqueIdentifier();
                if (state.ContainsKey(id))
                {
                    saveable.RestoreState(state[id]);
                }
            }
        }

        private string GetPathFromSaveFile(string saveFile)
        {
            return Path.Combine(Application.persistentDataPath, saveFile + ".sav");
        }```

I noticed something when comparing to my version of portal. The second time I grab hold of the PlayerController and disable it happens AFTER calling Load(). Try moving the second set of these two lines:

PlayerController newPlayerController = GameObject.FindWithTag("Player").GetComponent<PlayerController>();
newPlayerController.enabled = false;

after the line:

savingWrapper.Load();

for me, it looks like this:

  1. Enter portal
  2. Disable player control on player in current scene
  3. Fade out / Save Data in the current scene
  4. Load the new scene
  5. Load the save data for that new scene
  6. Disable player control on player in new scene

Although, now that I think about it, I don’t think it will make a difference since you are waiting for the new scene to load before grabbing the player, and the player in the old scene should already be destroyed.

The next thing to look at is savingWrapper.Load(). Which method in the saving system is it calling?

Hi,
Here is my SavingWrapper class.

public class SavingWrapper : MonoBehaviour {
        const string defaultSaveFile = "save";

        [SerializeField] float fadeInTime = 0.2f;

        private void Awake() {
            StartCoroutine(LoadLastScene());
        }

        private IEnumerator LoadLastScene() {
            yield return GetComponent<SavingSystem>().LoadLastScene(defaultSaveFile);
            Fader fader = FindObjectOfType<Fader>();
            fader.FadeOutImmediate();
            yield return fader.FadeIn(fadeInTime);
        }

        private void Update() {
            if (Input.GetKeyDown(KeyCode.S)) {
                Save();
            }

            if (Input.GetKeyDown(KeyCode.L)) {
                Load();
            }

            if (Input.GetKeyDown(KeyCode.Delete)) {
                Delete();
            }
        }

        public void Load() {
            StartCoroutine(GetComponent<SavingSystem>().LoadLastScene(defaultSaveFile));
        }

        public void Save() {
            GetComponent<SavingSystem>().Save(defaultSaveFile);
        }

        public void Delete() {
            GetComponent<SavingSystem>().Delete(defaultSaveFile);
        }
    }

Ok, we’re close to solving your problem. savingWrapper.Load() is essentially calling SavingSystem.LoadLastScene() and inside that method, your are doing SceneManager.LoadSceneAsync([your old scene build index here]). One sec, and I think I can help you fix it… just need to review my setup real quick.

Ok, make these changes if you would please. In SavingSystem.cs change your Load() to this:

        public IEnumerator Load(string saveFile)
        {
            int buildIndex = SceneManager.GetActiveScene().buildIndex;
            yield return SceneManager.LoadSceneAsync(buildIndex);
            RestoreGameState(LoadFile(saveFile));
        }

in SavingWrapper.cs change Load()

        public IEnumerator Load()
        {
            yield return GetComponent<SavingSystem>().Load(defaultSaveFile);
        }

and lastly, in Portal.cs, change:
savingWrapper.Load(); to

yield return savingWrapper.Load();

…that should do it. Let me know the result!

1 Like

That’s awesome, that fixed it :grinning:

Thanks so much for spending so much time to help out.

Glad I could be of help! :slight_smile:

Bookmarking this, as I wrote my saving system differently, and never encountered this bug, but see people asking often as the enounter it. Good job working through this, Seth!

1 Like

I went ahead and forked the Course Repo from Github, and the bug exists in the final version Sam put out as well.

@sampattuzzi note

What exactly are the repro steps in the Github version?

I’ll email you the diff when I get home.
(I don’t have access to my Unity projects at work)

Privacy & Terms