Class and variables becoming Null even when though it isn't destroyed.

Hi

I’ve created a WaveManager that calls a WaveSpawner for a TD game I’m working on that expands on a udemy tutorial…

for some reason even though I’m getting no error messages for the class in question (WavesManager.cs), it starts fine but later when I recall it for spawning another wave, its being classed as null in the debugger. And the variables being checked in an if statement is becoming 0 such as the length of a list even though the list in the debugger still has 3 elements.

public class WavesManager : MonoBehaviour
{
    [SerializeField] WaveConfig[] waves;
    List<Enemy> currentWavesEnemies;
    CurrentWaveChecker _currentWaveChecker;

    int _numberOfWaves;
    int _currentWaveNumber = 0;

    bool firstWaveSpawned = false;

    void Awake()
    {
        _currentWaveChecker = GetComponent<CurrentWaveChecker>();
        try
        {
            currentWavesEnemies = new List<Enemy>();
        }
        catch (Exception)
        {

            Debug.LogError("Couldn't make create a list bruh");
        }
    }
    void Start()
    {
        _numberOfWaves = waves.Length;
        SpawnWaves();
    }

    void SpawnWaves()
    {

        if (_currentWaveNumber <= _numberOfWaves -1)
        {
            if (firstWaveSpawned)
            {
                EventManager.RaiseOnWaveComplete();
            }
            _currentWaveChecker.SetWaveStatus(false);
            firstWaveSpawned = true;
                StartCoroutine((waves[_currentWaveNumber]).ThisWavesSpawner.SpawnCurrentWave((waves[_currentWaveNumber])));
        }
        else
        {
            EventManager.RaiseOnLevelComplete();
        }
    }

    public void WaveComplete()
    {
        _currentWaveNumber++;
        SpawnWaves();
    }
}

The if Statement I’m describing:

if (_currentWaveNumber <= _numberOfWaves -1)

With the variable _numberOfWaves is what I’m describing becomes 0 the second time round even though the list still has 3 elements in the debugger.

WaveComplete is called from another class when conditions are, but for some reason when calling WavesManager the second time round in the debugger it says this= null with the above mentioned change to some variables.

Which doesn’t make any sense to me because how is the class being called if this = null and certain variables have now changed by themselves.

If it helps this is the wave spawner that gets called and checks if the wave is complete:

public class DefautlWaveSpawner : WaveSpawner
{
     WavesManager _wavesManager;
    CurrentWaveChecker _currentWaveChecker;

    private void OnEnable()
    {
        _wavesManager = FindObjectOfType<WavesManager>();
        _currentWaveChecker = FindObjectOfType<CurrentWaveChecker>();
    }
 
    public override IEnumerator SpawnCurrentWave(WaveConfig wave)
    {
        Debug.Log(wave.DelayBeforeFirstSpawn);
        yield return new WaitForSeconds(wave.DelayBeforeFirstSpawn);

        foreach (var enemy in wave.EnemiesToSpawn())
        {
            Instantiate(enemy);
            Debug.Log("Enemy spawned from wave: " + wave);
            yield return new WaitForSeconds(wave.NextRandomSpawnDelay());
        }

        while (_currentWaveChecker.CurrentWaveComplete == false)
        {
            yield return new WaitForSeconds(1f);
            Debug.Log("Checking if wave complete");
            _currentWaveChecker.CheckIfWaveComplete();
        }
        if (_currentWaveChecker.CurrentWaveComplete == true)
        {
            Debug.Log("calling wave complete");
            _wavesManager.WaveComplete();
        }
    }
}

If you have something null, then you must figure out why it’s null. My first thought since you’re using FindObjectOfType is you have multiple copies of a script in your scene. Otherwise, if you know what is null, you need to figure out why. Use debug.log statements. Make your variables public if you need so you can see what it’s linked to. Anything to help solve why it’s null.

1 Like

I have just done A LOT of debugging log tests and testing of making certain variables public and some very strange behaviour is happening.

Basically in the scene view the object and class is visible with the correct variable numbers the entire time. I also added additional logs to see if there is more than one instance of the Wavemanager.cs being instantiated and there isn’t

After adding a debug circle (don’t know the technical name) for line:

        _currentWaveNumber++;

in the WaveManager.cs in the method WaveComplete();

Visual studio has said the following error message in the debugger:

this.gameObject UnityEngine.MissingReferenceException: The object of type ‘WavesManager’ has been destroyed but you are still trying to access it. Your script should either check if it is null or you should not destroy the object. UnityEngine.GameObject
The thing is, it has NOT been destroyed and I can see it in the scene view, Is this a bug?

EDIT:
I have gone to debug the line:

_wavesManager.WaveComplete();

In DefaultWaveSpawner.cs

Which calls the waveManager and same error is happening here too so I’m guessing something in this class is causing it because this method is called from here before the debugger goes back into the Wavemanager.cs

The strange thing is the DefaultWaveSpawner should still be active and if it isn’t how is it calling the Wavemanager and the debugger going through it?

The mystery of the null error. While sometimes restarting Unity or your computer has solved issues, I’m going to say it’s unlikely to be a bug.

Some possible options.
It is being destroyed and then replaced.
You are restarting your scene and something points to the old version instead of the new version. This can happen with static variables or dontDestroyOnLoad scripts.

Can you copy and paste your full error from the console. This should include your stack trace as well.

1 Like

Thanks again for the information and advice, the scene is never so it can’t be that.

There isn’t a full an error message in the console, the errors I have shared above are in the debug window in visual studio.

Is there something else I’m supposed to do with the stack trace you’ve requested, after looking into it from what I’ve gathered it helps gather information from a Debug.Log?

https://docs.unity3d.com/Manual/IL2CPP-managed-stack-traces.html

Also is there something in an ienumator and while loop within it that maybe throwing something off?

After more debugging in the DefaultWaveSpawner.cs:

    public override IEnumerator SpawnCurrentWave(WaveConfig wave)
    {
        Debug.Log(wave.DelayBeforeFirstSpawn);
        yield return new WaitForSeconds(wave.DelayBeforeFirstSpawn);
        Debug.Log("SPAWN CURRENT WAVE BEING CALLED");

        foreach (var enemy in wave.EnemiesToSpawn())
        {
            Instantiate(enemy);
            yield return new WaitForSeconds(wave.NextRandomSpawnDelay());
        }

        while (_currentWaveChecker.CurrentWaveComplete == false)
        {
            yield return new WaitForSeconds(1f);
            Debug.Log("Checking if wave complete");
            _currentWaveChecker.CheckIfWaveComplete();
        }
        if (_currentWaveChecker.CurrentWaveComplete == true)
        {
            Debug.Log("calling wave complete");
            _wavesManager.WaveComplete();
        }
    }

As mentioned above with the:

_wavesManager.WaveComplete();

Being called, the DefaultSpawner now appears to be valid but tnow the _wavesManager is null even though it correctly finds the object OnEnable.

So, when you hit play in the editor, you’re not getting any errors in Unity’s Console as you play your game?

Unless you have a timing issue that your coroutine is causing, there shouldn’t be an issue.

The stack trace includes line numbers and a call stack which can help trace what calls it made to get to your error. This is useful for tracking why something is null.

I wasn’t getting any error message in Unitys cost, I’ve just changed my code to try and call the WaveManager.cs WaveComplete method from a class called CurrentWaveChecker.cs

But now I am getting a unity error saying:
NullReferenceException: Object reference not set to an instance of an object
CurrentWaveChecker.SetWaveStatus (System.Boolean isWaveComplete) (at Assets/Spawner/CurrentWaveChecker.cs:22)
CurrentWaveChecker.CheckIfWaveComplete () (at Assets/Spawner/CurrentWaveChecker.cs:30)
DefautlWaveSpawner+d__4.MoveNext () (at Assets/Spawner/DefautlWaveSpawner.cs:40)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at :0)

Is this the stack trace you’re talking about?

I have found within visual studio the cause once again is for some reason in DefaultWaveSpawner.cs later on for some reason the cached references called:

 private void OnEnable()
    {
        //_wavesManager = FindObjectOfType<WavesManager>();
        _currentWaveChecker = FindObjectOfType<CurrentWaveChecker>();
    }

Become null even though they are successfully found OnEnable. I have commented out _wavesManager because this was used until my recent changes to attempt to call the WaveComplete in WaveManager.cs

That is what a stack trace looks like. It shows that line 22 of CurrentWaveChecker is where your null error is. And if you go down the stack trace it will tell you where the calls come from for each method. So it starts with line 40 of DefautlWaveSpawner.

I’m having a little trouble following through your scripts currently.

1 Like

in your WaveManager class add this method

public void OnDestroy()
{
    Debug.LogError{"WaveManager destroyed!"};
}

and play the scene again navigate to the gameobject that has the wavemanager script and keep it in the inspector while you spawn the 1st wave, complete the 1st wave, and then spawn and complete the 2nd wave. The test here that you want to confirm is that wavemanager remains selected and visible in the inspector over the course of both waves, and that the debug logerror you just added does not show up until you unload/exit the scene.

based on the errors you’re getting my bet is that either the GameObject the wavemanager script is on, or the script itself is getting deleted at some point between the 1st wave starting to when the 2nd wave completes.

if at any point the wavemanager get deselected and unloaded from the inspector during this test (or if the debug logerror shows) then it means the original wavemanager is getting deleted. it doesn’t matter if you can still find it in the editor. its likely not the same original instance that your WaveSpawner found during its OnEnable.

2 Likes

Its a line where it goes to call a method using the cached reference to WavesManager:

 public void SetWaveStatus(bool isWaveComplete)
    {
        _currentWaveComplete = isWaveComplete;
        if (isWaveComplete == true)
        {
            _currentWaveComplete = false;
            _wavesManager.WaveComplete();
        }
    }
 _wavesManager.WaveComplete();

To be exact.

The line 40 in defaultWaveSpawner is the end of a while loop:

        while (_currentWaveChecker.CurrentWaveComplete == false)
        {
            yield return new WaitForSeconds(1f);
            Debug.Log("Checking if wave complete");
            _currentWaveChecker.CheckIfWaveComplete();
        }

Its the line with the }

I apologize for my scripts being difficult to follow, I’m still quite inexperienced with debugging and still learning C# and unity. Let me know if there’s anything I can do to make it easier to follow, I’ve purposely left out some scripts to not over bloat what I’m going through and to try and only include what I think maybe causing the issue.

I’ve just tried what you’ve suggested with the OnDestroy Debbug.logerror in WavesManager.cs but unfortunately, the logerror was never called before the null reference.

Alright then this means that WaveManager isn’t getting destroyed between waves. what about the DefaultWaveSpawner (or to be more abstract, Wavespawner)

I’m seeing that the Coroutine “SpawnCurrentWave” is defined on the WaveSpawner… but technically its run on the Wavemanager… this means that even if the Wavespawner is unloaded, WaveManager will still attempt to continue running Wavespawner’s coroutine, regardless if its unloaded. Since the coroutine itself is not checking if its still loaded, that would explain why you are getting these null or missing ref exceptions.

When the spawner is unloaded things like

WavesManager _wavesManager;
CurrentWaveChecker _currentWaveChecker;

will be nulled/defaulted out. meanwhile the method SpawnCurrentWave is still running on WaveManager. Unity doesn’t stop coroutines that were defined in other monobehaviours that have unloaded since technically certain coroutines can still function this way, however coroutines called this way should follow certain rules:

  • if the routine ever references any in-scene reference, it must consider them volatile and thus must verify that its still there in-between each yield
  • such coroutines should have an exit strategy of how it should react in the event that any of its volatile memory is unloaded. does it skip steps? does it invoke callbacks/events? or does it simply yield break?

In the end, you have two options:

  • either make the coroutine private/protected inside wavespawner, and run the routine locally. you could have a public wrapper method that WaveManager will call instead and this method will make the StartCoroutine call. doing it this way will mean that Unity will automatically kill the coroutine for if the waveSpawner is unloaded while its still running.
  • keep it public but you must conform to the rules above. you need to check if WaveSpawner is still loaded between each yield (you could simply write “if(!this) yield break;” after every yield return)

plus you should handle the case for what should happen if the coroutine is ended prematurely regardless of which path you choose. it may become important to devise this exit strategy to avoid soft locks in the classes that were waiting for the coroutine to finish normally.

you could either raise some WaveCancelled event or just as simply consider a wave that ended prematurely complete and just continue on as if its already completed.

1 Like

Well, your SpawnWaves(); method is called from two different places. First it’s called in Start, this may be the first time it’s called and it works. The other place is the WaveComplete() method and here we don’t know where, when and who is actually calling the method.

If suddenly you get a null reference exception, chances are high that you actually wired up events in the inspector somewhere but you may have choosen the wrong object. A very common mistake is to reference a prefab instead of the actual instance in the game. You can run code in prefabs as well, however many thing would break down in such a case. Another possible error is that you may have created an instance of your script manually by using the “new” keyword. This would create a dead / zombie instance which will essentially be an empty fake-null instance that behaves like a destroyed class.

We don’t have enough information to verify or check any of those cases. You may want to pay more attention to the complete stacktrace of your error, so you can follow the exact event chain back to the root. This may not necessarily lead to the actual issue, but may give some insight where it may go wrong.

Keep in mind that you can add a second “context” argument to Debug,Log statements which can be any UnityEngine.Object derived class instance. Clicking on the log message that has such a context object provided would highlight / ping that context object in the scene / project if it can be found. This can also help to narrow down the actual involved objects.

1 Like

I have just added the OnDestory on each relevant class and it still doesn’t get called for them either, however after reading your response I’ve added additional references in the classes that were getting null references to cached objects:

In the DefaultWaveSpawner.cs in the while loop I also added another reference:

   public override IEnumerator SpawnCurrentWave(WaveConfig wave)
    {
        Debug.Log(wave.DelayBeforeFirstSpawn);
        yield return new WaitForSeconds(wave.DelayBeforeFirstSpawn);
        Debug.Log("SPAWN CURRENT WAVE BEING CALLED");

        foreach (var enemy in wave.EnemiesToSpawn())
        {
            Instantiate(enemy);
            yield return new WaitForSeconds(wave.NextRandomSpawnDelay());
        }

        while (_currentWaveChecker.CurrentWaveComplete == false)
        {
            yield return new WaitForSeconds(1f);
            Debug.Log("Checking if wave complete");
            _currentWaveChecker = FindObjectOfType<CurrentWaveChecker>();
            _currentWaveChecker.CheckIfWaveComplete();
        } 
    }

EDIT:
I was also then running into issues of having multiple waves being spawned and certain classes being called in an infinite loop, but fixed it by moving the wavesManager being called back in the DefaultWaveSpawner.cs I added an if statement at the end of the above code after the while:

        if (_currentWaveChecker.CurrentWaveComplete)
        {
            Debug.Log("Current wave complete = true calling spawn wave");
            _wavesManager = FindObjectOfType<WavesManager>();
            _wavesManager.WaveComplete();
        }

Thanks for the detailed response and information, I don’t understand what is meant by unloading I’m currently looking into it. I already assumed something must be happening using a coroutine and being called in other classes might be causing weird behaviour. But how can the coroutine prevent existing objects in classes in the scene from losing cached references to other classes?

The only issue I appear to have now is having to keep calling for new references to the classes I had already cached which is bad for performance and I can’t find anything that clearly explains why this is happening.

I’m going to do some heavy research into coroutines…