Lecture: Singleton Scorekeeper - NullReferenceException

Whenever I click the start button or to return to the game, I get a “NullReferenceException: Object reference not set to an instance of an object.”
This is being thrown by the “scorekeeper.ResetScore()” method call in the “LoadGame()” method of the lecture.
I have a ScoreKeeper prefab in every scene. A scorekeeper is being found in my LevelManager’s Awake() method. But it’s somehow not being found when I call the “LoadGame()” method.
I am completely at a loss as to why it’s not finding my Scorekeeper object.

Here’s a snippet of my LevelManager code:

public class LevelManager : MonoBehaviour
{
    [SerializeField] float sceneLoadDelay = 2f;
    ScoreKeeper scoreKeeper;

    void Awake() 
    {
        scoreKeeper = FindObjectOfType<ScoreKeeper>();
        Debug.Log("Scorekeeper found: " + (scoreKeeper != null));        
    }

    public void LoadGame()
    {
        Debug.Log("gamestart - Scorekeeper found: " + (scoreKeeper != null));
        scoreKeeper.ResetScore();
        SceneManager.LoadScene("Game");             
    }

What does your ScoreKeeper script look like?


Edit
I haven’t done this course, but I can see an issue here. You say you have a ScoreKeeper prefab in every scene. The first scene that loads is going to put its ScoreKeeper into DontDestroyOnLoad (DDoL). Then you load a new scene. This new scene also has a ScoreKeeper so now you have 2. This is ok, because the ScoreKeeper is going to discover in its own Awake method that there are 2, and destroy the one from this scene. The problem, though, is that LevelManager may have (and apparently did) already executed its own Awake and found the ScoreKeeper of this scene instead of the one in DDoL. So, in its Awake() it reports that ScoreKeeper is not null, but then this reference gets destroyed when ScoreKeeper runs its Awake and LevelManager no longer has a reference to ScoreKeeper.

There are a few ways to solve this. The easiest way is to not cache the ScoreKeeper and just get a reference when you want to use it

public void LoadGame()
{
    FindObjectOfType<ScoreKeeper>().ResetScore();
    SceneManager.LoadScene("Game");             
}

Another way would be to move ScoreKeeper up in the execution order to make sure ScoreKeeper runs its Awake before anything else does. This can be done in the Project Settings
image

2 Likes

The whole point of that lesson was to turn the scorekeeper into a Singleton, IE only have one Scorekeeper in the scene at a time.
This is the scorekeeper code after the lesson:

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

public class ScoreKeeper : MonoBehaviour
{
    static ScoreKeeper instance;
    int score = 0;

    void Awake() 
    {
        ManageSingleton();   
    }

    public ScoreKeeper GetScoreKeeper()
    {
        return instance;
    }

    void ManageSingleton()
    {
        if(instance != null)
        {
            gameObject.SetActive(false);
            Destroy(gameObject);
        }
        else
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }

    public int GetScore()
    {
        return score;
    }

    public void ModifyScore(int value)
    {
        score += value;
        Mathf.Clamp(score, 0, int.MaxValue);        
    }

    public void ResetScore()
    {
        score = 0;
    }
}

Yes, I saw that. But the issue remains. If LevelManager runs its Awake() before ScoreKeeper runs its Awake() the LevelManager could - and as you have discovered, does - find the wrong ScoreKeeper.

There is also an issue with this singleton because the GetScoreKeeper() method should be static.

1 Like

The solution I found was sort of a bandaid fix, but it works, so it’s what I’m using. I added a new method called “findScoreKeeper()” and called it from both awake() and from LoadGame().

public class LevelManager : MonoBehaviour
{
    [SerializeField] float sceneLoadDelay = 2f;
    ScoreKeeper scoreKeeper;

    void Awake() 
    {
        findScoreKeeper();     
    }

    void findScoreKeeper()
    {
        if(scoreKeeper == null)
        {
            scoreKeeper = FindObjectOfType<ScoreKeeper>();
        }
    }

    public void LoadGame()
    {
        findScoreKeeper();
        scoreKeeper.ResetScore();
        SceneManager.LoadScene("Game");             
    }

If you make the GetScoreKeeper() method static, you could just use that to get the ScoreKeeper. Then your LevelManager would get the correct ScoreKeeper even if it does run first.

void Awake()
{
    scoreKeeper = ScoreKeeper.GetScoreKeeper();
}

This is, of course, assuming that the ScoreKeeper from another scene has already executed and put itself into the instance variable.

This will be fine.

1 Like

Hrmm, making the method static did not work. Also adjusting the execution order did not work either. It’s very strange.

public class ScoreKeeper : MonoBehaviour
{
    static ScoreKeeper instance;
    int score = 0;

    void Awake() 
    {
        ManageSingleton();   
    }

    public static ScoreKeeper GetScoreKeeper()
    {
        return instance;
    }

    void ManageSingleton()
    {
        if(instance != null)
        {
            gameObject.SetActive(false);
            Destroy(gameObject);
        }
        else
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }
public class LevelManager : MonoBehaviour
{
    [SerializeField] float sceneLoadDelay = 2f;
    ScoreKeeper scoreKeeper;

    void Awake() 
    {
        //findScoreKeeper();
        scoreKeeper = ScoreKeeper.GetScoreKeeper();     
    }

    void findScoreKeeper()
    {
        if(scoreKeeper == null)
        {
            scoreKeeper = FindObjectOfType<ScoreKeeper>();
        }
    }

    public void LoadGame()
    {
        //findScoreKeeper();
        scoreKeeper.ResetScore();
        SceneManager.LoadScene("Game");             
    }

image

If the ScoreKeeper wasn’t being instantiated before the LevelManager was running it’s awake() method, wouldn’t the error be happening in the LevelManager’s Awake() method instead of happening in the findScoreKeeper() method?

Yeah, like I said; it assumes ScoreKeeper was already loaded. You will have to change the execution order, even if you use GetScoreKeeper().

1 Like

Did I not change the execution order? That’s the pic I posted at the bottom.

No. The object is there so the LevelManager will find it. It hasn’t executed Awake() yet, so it hasn’t sorted out its own stuff. It’s a common race condition. You should really just move the cache in LevelManager from Awake() to Start().

I don’t know. I can’t see anything in that picture. But it doesn’t really matter. I realised that the issue is a common race condition. You shouldn’t do stuff in Awake() that assumes other Awake()'s have executed. Here, we do something in LevelManager.Awake() that assumes the ScoreKeeper.Awake() has executed.

1 Like

I understand what you’re saying. But the LoadGame() method is not running until I hit a button. I can see the ScoreKeeper object in the Unity Game Hierarchy. Unless I don’t understand how the awake method works. That ScoreKeeper object is definitely created, and thus ScoreKeeper’s Awake method has been run. That’s why I’m confused as to why the error isn’t being thrown when LevelManager runs the Awake Method, but IS being thrown when it runs the LoadGame method.

At any rate, I appreciate your help. You are very kind to give me your input.

Yes. The ScoreKeeper object is created, but that doesn’t mean its Awake() method has executed.
When the scene loads, Unity collects all the objects that exists in the scene and start running their Awake() methods, one after the other. In this case it seems to run the Awake() from LevelManager before it runs the Awake() from ScoreKeeper. So, LevelManager will find the ScoreKeeper object - even if the ScoreKeeper.Awake() has not executed yet - because the object exists. Now, when the ScoreKeeper gets its turn to execute its Awake() method, it may find that there is already a ScoreKeeper in the scene - the one that’s been put in DDoL - and so it will destroy itself because it is not needed. If this also happens to be the one that LevelManager found, that reference that LevelManager stored in its Awake() has now been destroyed. So, when you click the button and LoadGame() tries to call scoreKeeper.ResetScore(), that scoreKeeper is null because it destroyed itself. When you run your findScoreKeeper() because the reference is null, you end up finding the original ScoreKeeper that was put in DDoL and all is fine again.

If, however, this happens on the very first scene, this whole theory breaks down because there will only be one ScoreKeeper and that would also be the one that goes into DDoL. Unless you happen to have two ScoreKeeper prefabs in your first scene. Else, I would love to take a look at the project and try to figure out what’s happening

1 Like

THANK YOU! That makes so much sense. I’m certain that’s what is happening. I still don’t know why the load order fix you suggested didn’t fix it. I just deleted the Awake method entirely from LevelManager, and just am using the findScoreKeeper method I wrote, and now I don’t have to worry about the problem.

In Summary: The issue is a race condition where the LevelManager is loading the ScoreKeeper object in the Awake() method, but the ScoreKeeper ManageSingleton() method is deleting the object that the LevelManager has loaded, thus losing the reference.

Here is my cleaned up solution to the issue. I deleted the Awake() method from Level Manager, and added a method that loads the ScoreKeeper object only when necessary (when the LoadGame() method is called.

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

public class LevelManager : MonoBehaviour
{
    [SerializeField] float sceneLoadDelay = 2f;
    ScoreKeeper scoreKeeper;

    void findScoreKeeper()
    {
        if(scoreKeeper == null)
        {
            scoreKeeper = FindObjectOfType<ScoreKeeper>();
        }
    }

    public void LoadGame()
    {
        findScoreKeeper();
        scoreKeeper.ResetScore();
        SceneManager.LoadScene("Game");             
    }

    public void LoadMainMenu()
    {
        SceneManager.LoadScene("Main Menu");
    }

    public void LoadGameOver()
    {
        StartCoroutine(WaitAndLoad("Game Over Menu", sceneLoadDelay));
    }

    public void QuitGame()
    {
        Debug.Log("Quitting");
        Application.Quit();
    }

    IEnumerator WaitAndLoad(string sceneName, float delay)
    {
        yield return new WaitForSeconds(delay);
        SceneManager.LoadScene(sceneName);
    }
}

2 Likes

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

Privacy & Terms