Null reference issues when returning to first scene

I experienced an issue with my game where if I had killed one or more enemies in scene one on my way to the portal to scene two, on returning to scene one I was receiving a load of null reference errors related to the ‘if health is 0, run the die method’ that is called during the restore state method in the health class.

After spending a while tracing and trying to resolve this, I ended up inserting a new waitforseconds to allow everything to load in correctly before trying to restore the saved states.

My Transition method ended up as below.

IEnumerator Transition()
{
if(sceneToLoad < 0)
{
Debug.LogError(“Scene to Load has not been set”);
yield break;
}

        DontDestroyOnLoad(this.gameObject);

        Fader fader = FindObjectOfType<Fader>();

        yield return fader.FadeOut(fadeOutTimer);

        // Save Progress before loading next scene
        SavingWrapper wrapper = FindObjectOfType<SavingWrapper>();
        wrapper.Save();

        // Load next scene
        yield return SceneManager.LoadSceneAsync(sceneToLoad);

        /* Pause to allow all game objects to load in correctly before running the wrapper.load() method.
         * This is to stop null reference errors encountered when dealing with dead enemies */
        yield return new WaitForSeconds(waitBetweenScenesTimer);
        
        // Load Progress before updating player position etc.
        wrapper.Load();

        // Update Player position in the new scene
        Portal otherPortal = GetOtherPortal();
        UpdatePlayer(otherPortal);

        yield return new WaitForSeconds(waitBetweenScenesTimer);

        yield return fader.FadeIn(fadeIntimer);
        
        Destroy(this.gameObject);
    }

Is this an OK way to deal with this issue or is there a better method that I should have used. This resolution does appear to work OK but obviously does introduce another delay between scenes.

Thanks in advance

1 Like

I think I have resolved this in a much cleaner way from information in the discussions on the next lecture. I was getting the same errors on starting the game regarding the dead enemies after the checkpoint system was implemented.

I have now been able to remove the extra wait from this script by moving the cached references of affected scripts into the Awake method instead of Start. I am now able to load scenes both from the portals and starting the game from the second scene.

Edit:
If it is of use to anyone, I moved the reference to the CanvasGroup in Fader.cs and the Animator and Action Scheduler in Health.cs from Start to Awake in their respective scripts.

2 Likes

This will work, but is technically only masking the underlying structural issue at hand.
Here’s what’s happening, and why your patch works:

yield return SceneManager.LoadSceneAsync(sceneToLoad);

At this point every GameObject is loaded, and every component on every GameObject will have it’s Awake() method run

Wrapper.Load();

All RestoreState(); methods are run on all ISaveable components.

Portal otherPortal = GetOtherPortal();
UpdatePlayer(otherPortal);
yield return new WaitForSeconds(waitBetweenScenesTimer);

After this line of code, all Start() methods are run…
The issue is that many of our MonoBehaviors are caching references and initializing important information in the Start() method, which hasn’t run yet when we go to Load();

By inserting the

yield return new WaitForSeconds(waitBetweenScenesTimer);

you’re allowing the Start() method to run, getting all of those references cached.

This sounds like a good solution, and in the short term, it is, but as I said before, it masks an underlying structural issue (and confession time, it’s our fault, as early on in the course, we cache so many variables in the Start() method. This is fine if no other class references the class, but dangerous when classes communicate as it creates a race condition . In a race condition, bugs can be hidden until “surprise” moments because we really can’t coordinate what order components get their methods called. In our Portal method, we’ve artificially exposed the race condition by putting a hard road block in the middle of the life cycle.

Fortunately, there are some simple things you can do stop this race condition in it’s tracks by following some simple rules with components. The key is to make sure that certain things are done in certain callback events, not too soon, and not too late.

  • void Awake(); This is where you want to cache all variable references (e.g. health = GetComponent<Health>(); as well as make sure that things like Lists and are properly initialized.
  • void OnEnable() This is where you should subscribe to events in other classes. (Using this structure means you should always unsubscribe from those events in OnDisable();
  • void Start() It is now safe to reference other classes, for example: heatlh.IsAlive();

In short, many of our classes do their initialization in Start(); You should move these to Awake();

3 Likes

LOL, as I was typing all this out, you arrived at the correct solution. Well done!

2 Likes

@Brian_Trotter It was actually your solution to another person’s problem that led me to the solution for mine. :grinning:.

I have marked your answer as the topic solution. It explains really well exactly what was happening, why things were failing and why my original resolution was a temporary workaround and not an actual fix.

Thank you very much for your help with this and your very clear explanation.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms