Can't seem to catch a null

Hello!

Been a while since I’ve posted on here, this seems so simple, and quite frankly I feel like I’m missing something simple.

Below is the FindRandomEnemy() function used by my weapons class. All weapons inherit from this class, but the Sniper and Mortar specifically - for now - use this function to find and attack a random enemy.

I have done about 10 different variations of trying to catch the null, which occurs on line 19.

The enemy controller is on all enemies.

The target is where the bullet aims, where pickups go to, where ui elements appear etc… the transform that acts as the “center” of the enemy.

It sometimes comes back as null. I’d say, <5% of enemies, and only when both weapons are fully upgraded and thus firing very quickly.

I’m 99% sure it’s because by the time it’s called, the enemy is dead and thus destroyed.

But this must happen in between the TryGet and the next line, right?

Any ideas?

It’s uncommon and isn’t game breaking. It has only been found now that my testers are really pushing the edge cases. It’s infuriating that I can’t work this one out!

Thanks

No Kurt, please don’t post your null reference topic! :slight_smile:

public virtual void FindRandomEnemy()
{
    Collider2D[] hitColliders = Physics2D.OverlapCircleAll(transform.position, DetectionRadius * playerAttributes.Range);
    List<Collider2D> enemyColliders = new List<Collider2D>();

    foreach (Collider2D hitCollider in hitColliders)
    {
        if (hitCollider.CompareTag(TagNames.Enemy.ToString()))
        {
            enemyColliders.Add(hitCollider);
        }
    }
    if (enemyColliders.Count > 0)
    {
        int randomIndex = UnityEngine.Random.Range(0, enemyColliders.Count);
        nearestEnemy = enemyColliders[randomIndex].gameObject;
        if(nearestEnemy.TryGetComponent<EnemyController>(out EnemyController controller))
        {
            target = controller.GetTarget();
            if (WeaponName != WeaponNames.MORTAR)
            {                    
                Shoot();
            }
        } else
        {
            FindRandomEnemy();
        }
    }        
}

so, when you debugged it, exactly where did it complain was null? what is it claiming is null? what is WeaponName?

It’s either controller, or the target. But one does not exist without the other which is why I’m stumped.

If the controller exists, then so does the target.

In the enemy prefab the controller is on the parent, and the target is a child GO, with a function simply to return it.

WeaponName is a property of the Weapon class.

All functionality is working outside of a tiny window where the enemy is killed by a different weapon whilst this function is running.

well, it it will be possible to add debug info to find out exactly whats null, so you need to start there, then you can work backwards to find out why its null

Perhaps you’re missing the three steps??

Out of those ten things, if you make the first three things the three steps to catch a NullRerefenceException, you’ll be done!

The answer is always the same… ALWAYS!

How to fix a NullReferenceException error

Three steps to success:

  • Identify what is null ← any other action taken before this step is WASTED TIME
  • Identify why it is null
  • Fix that

NullReference is the single most common error while programming. Fixing it is always the same.

Some notes on how to fix a NullReferenceException error in Unity3D:

http://plbm.com/?p=221

These things are completely discoverable through debugging.

By debugging you can find out exactly what your program is doing so you can fix it.

Use the above techniques to get the information you need in order to reason about what the problem is.

You can also use Debug.Log(...); statements to find out if any of your code is even running. Don’t assume it is.

Once you understand what the problem is, you may begin to reason about a solution to the problem.

Remember with Unity the code is only a tiny fraction of the problem space. Everything asset- and scene- wise must also be set up correctly to match the associated code and its assumptions.

Thanks both.

I have fully debugged already, and the controller is coming back as null, because the enemy is being destroyed during this function.
Or the target is coming back as null, because the target is destroyed whilst the GetTarget() function is called.

Like i say it’s a proper edge case. Or I am missing something really simple, I’m very familiar with Unity and programming which is why I am posting here with my tail between my legs, because null references are as Kurt says, always the same problem.

How do I handle it if it is null is my question here? There is no harm done to the game, I just want it to be skipped if/when it goes null. But no matter how many ifs or try catches I can’t seem to get it.

You GOTTA find what line it is.

As one simple example, this particular expression:

will fail if the Collider in that array has been destroyed.

Why?

Because .gameObject is a special property and is no longer valid once the collider is destroyed, so break those two things apart:

  • get the collider out of the array
  • check if it is valid before using any part of it

This stuff really gets a lot easier when you use extra variables and do precisely one thing per line.

Since you loved my nullref blurb so much, here’s my “air your code out” blurb: :slight_smile:

If you have more than one or two dots (.) in a single statement, you’re just being mean to yourself.

(yes, there’s only one dot above, but I consider “dot” to be “lookup operation” and dereferencing an item from an array is a lookup operation, then there’s the dot going after the gameObject, so the line I quoted above is one example that is doing two things at once…)

Putting lots of code on one line DOES NOT make it any faster. That’s not how compiled code works.

The longer your lines of code are, the harder they will be for you to understand them.

How to break down hairy lines of code:

http://plbm.com/?p=248

Break it up, practice social distancing in your code, one thing per line please.

“Programming is hard enough without making it harder for ourselves.” - angrypenguin on Unity3D forums

“Combining a bunch of stuff into one line always feels satisfying, but it’s always a PITA to debug.” - StarManta on the Unity3D forums

Ok, I will break it down next, if I can post this to you whilst I do so.

I have asked the AI overlord to comprehensively debug these 2 functions.

Ask me why I am only just showing you Shoot now?

No idea, it’s now throwing the error too/instead, perhaps with all the added if and buts. Again, I will stress that this code is nearly 1 year old and has never been a problem!

Anyway.

The console log is below the code.

public virtual void FindRandomEnemy()
{
    Debug.Log("Starting FindRandomEnemy...");

    Collider2D[] hitColliders = Physics2D.OverlapCircleAll(transform.position, DetectionRadius * playerAttributes.Range);
    Debug.Log($"Found {hitColliders.Length} colliders in range.");

    List<Collider2D> enemyColliders = new List<Collider2D>();
    foreach (Collider2D hitCollider in hitColliders)
    {
        if (hitCollider.CompareTag(TagNames.Enemy.ToString()))
        {
            Debug.Log($"Enemy collider found: {hitCollider.name}");
            enemyColliders.Add(hitCollider);
        }
    }

    if (enemyColliders.Count > 0)
    {
        int randomIndex = UnityEngine.Random.Range(0, enemyColliders.Count);
        nearestEnemy = enemyColliders[randomIndex]?.gameObject;
        Debug.Log(nearestEnemy != null
            ? $"Selected nearestEnemy: {nearestEnemy.name}"
            : "Failed to assign nearestEnemy (null).");

        if (nearestEnemy != null && nearestEnemy.TryGetComponent<EnemyController>(out EnemyController controller))
        {
            Debug.Log($"nearestEnemy has EnemyController: {controller.name}");
            target = controller.GetTarget();
            Debug.Log(target != null
                ? $"Assigned target: {target.name}"
                : "Target returned by GetTarget() is null.");

            if (WeaponName != WeaponNames.MORTAR)
            {
                Debug.Log("Weapon is not a Mortar. Calling Shoot().");
                Shoot();
            }
            else
            {
                Debug.Log("Weapon is a Mortar. Not shooting.");
            }
        }
        else
        {
            Debug.LogWarning(nearestEnemy == null
                ? "nearestEnemy is null, retrying FindRandomEnemy."
                : $"nearestEnemy does not have EnemyController. Retrying FindRandomEnemy.");
            FindRandomEnemy();
        }
    }
    else
    {
        Debug.LogWarning("No enemy colliders found in range.");
    }
}





public virtual void Shoot()
{
    Debug.Log("Starting Shoot...");

    if (nearestEnemy != null)
    {
        Debug.Log($"nearestEnemy is assigned: {nearestEnemy.name}");
        enemy = nearestEnemy.GetComponent<EnemyController>();
        Debug.Log(enemy != null
            ? $"EnemyController component found on nearestEnemy: {enemy.name}"
            : "EnemyController component is null on nearestEnemy.");

        if (enemy != null && !enemy.GetIsDead())
        {
            Debug.Log($"Enemy is alive: {enemy.name}");

            if (target != null)
            {
                Vector3 direction = (target.transform.position - bulletSpawn.transform.position).normalized;
                Debug.Log($"Calculated direction to target: {direction}");

                EnableAndInitializeBullet(direction);

                if (CalculateBPS())
                {
                    Debug.Log("Calculated BPS is true. Initializing second bullet.");
                    EnableAndInitializeBullet(direction, true);
                }

                if (IsAkimbo)
                {
                    Debug.Log("IsAkimbo is true. Initializing dual-wield bullet.");
                    Vector3 akimboDirection = (-target.transform.position - -bulletSpawn.transform.position).normalized;
                    EnableAndInitializeBullet(akimboDirection);
                }
            }
            else
            {
                Debug.LogWarning("Target is null in Shoot(). Cannot calculate direction.");
            }
        }
        else
        {
            Debug.LogWarning(enemy == null
                ? "Enemy is null in Shoot()."
                : $"Enemy is dead: {enemy.name}");
        }
    }
    else
    {
        Debug.LogWarning("nearestEnemy is null in Shoot().");
    }
}

CONSOLE LOG

nearestEnemy has EnemyController: Enemy_Tracksuit(Clone)
UnityEngine.Debug:Log (object)
Weapon:FindRandomEnemy () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:404)
WSniper:Update () (at Assets/Scripts/Weapons/Weapons/WSniper.cs:21)

Assigned target: Target
UnityEngine.Debug:Log (object)
Weapon:FindRandomEnemy () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:406)
WSniper:Update () (at Assets/Scripts/Weapons/Weapons/WSniper.cs:21)

Weapon is not a Mortar. Calling Shoot().
UnityEngine.Debug:Log (object)
Weapon:FindRandomEnemy () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:412)
WSniper:Update () (at Assets/Scripts/Weapons/Weapons/WSniper.cs:21)

Starting Shoot...
UnityEngine.Debug:Log (object)
Weapon:Shoot () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:440)
Weapon:FindRandomEnemy () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:413)
WSniper:Update () (at Assets/Scripts/Weapons/Weapons/WSniper.cs:21)

nearestEnemy is assigned: Enemy_Tracksuit(Clone)
UnityEngine.Debug:Log (object)
Weapon:Shoot () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:444)
Weapon:FindRandomEnemy () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:413)
WSniper:Update () (at Assets/Scripts/Weapons/Weapons/WSniper.cs:21)

EnemyController component found on nearestEnemy: Enemy_Tracksuit(Clone)
UnityEngine.Debug:Log (object)
Weapon:Shoot () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:446)
Weapon:FindRandomEnemy () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:413)
WSniper:Update () (at Assets/Scripts/Weapons/Weapons/WSniper.cs:21)

NullReferenceException: Object reference not set to an instance of an object
Weapon.Shoot () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:450)
Weapon.FindRandomEnemy () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:413)
WSniper.Update () (at Assets/Scripts/Weapons/Weapons/WSniper.cs:21)

Starting FindRandomEnemy...
UnityEngine.Debug:Log (object)
Weapon:FindRandomEnemy () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:379)
WMortar:Update () (at Assets/Scripts/Weapons/Weapons/WSniper.cs:21)

You will see that it finds the tracksuit as per the top of the Shoot(), but then jumps back to FindRandomEnemy().

This is consistent with all other findings.

Null reference exception, AND NO CATCH, next line should work as not null, but instead just gives me the null. No warning as per line 68 - 76 in the code.

Please help am cry. Will look into breaking it all down as per your previous reply.

Looked at your hairy code link.

The parts that fail are

        enemy = nearestEnemy.GetComponent<EnemyController>();

&

        target = controller.GetTarget();

How could I make that any more concise?

its a little hard for us to map stuff when your errors and your provided code lines dont match… but 412 seems to be line 36… and that 450 where the null reference is, is likely line 80

so, whats the value of bulletSpawn?

Of course, sorry.

The main lines in question are the ones in my last post. Will confirm others when back at my computer.

Thanks

if Im right your problem is most likely bulletspawn.

BulletSpawn is assigned in the inspector and tied to the player, so is not null. If it was, all weapons would fail on every call.

Weapon.Shoot () (at Assets/Scripts/Weapons/Weapons/Weapon.cs:450)

if (enemy != null && !enemy.GetIsDead())

Yea, line 412 in the console is 36 in the post. The Mortar has it’s own firing function in its class, Shoot() is all about projectiles, whereas the mortar call works a little differently with spawning UI targets and then explosions.

The below shows in the console,

  • enemy is assigned, then “found on nearestEnemy: name”

then it’s null.

enemy = nearestEnemy.GetComponent<EnemyController>();
Debug.Log(enemy != null
    ? $"EnemyController component found on nearestEnemy: {enemy.name}"
    : "EnemyController component is null on nearestEnemy.");

if (enemy != null && !enemy.GetIsDead())
{

controller GetIsDead() function:

public bool GetIsDead()
{
    return _enemyHealth.GetIsDead();
}

then the enemyHealth function returns the variable

public bool GetIsDead()
{
    return isDead;
}

I took a big step back after a small cry.

I have an EnemyManager class that registers/deregisters alive/dead enemies that I use for one of the powerups to apply random damage.

This class has a GetRandomEnemy() function.

So I removed the circle overlap thing and used this.

I still had the same problem.

Then I saw it.

At the top of the Weapon class was “protected EnemyController enemy;”

The ambiguity here, with inherited classes referencing it is what I believe was causing the issue. I think it was being assigned, but then called, and re-assigned with a new random enemy whilst other logic was running.

I now need to work around this, as the EnemyManager class doesn’t factor distance when selecting a random enemy, so now my mortar and sniper are screen-wide, but I have fixed the null.

Nice and tidy too

public virtual void FindRandomEnemy()
{
    EnemyController randomEnemy = enemyManager.GetRandomEnemy();
    if (randomEnemy != null)
    {
        enemyToAttack = randomEnemy.gameObject;
        enemyTarget = randomEnemy.GetTarget();
        if (WeaponName != WeaponNames.MORTAR)
        {                
            Shoot();
        }
    }
}

Ok, so further investigation as the null resurfaced. I wrapped every function related to GetIsDead().

I found that it was the _enemyHealth script that wasn’t initialized in time. So wrapping that in a conditional and handling it has solved the problem.

Good learning opportunity, but also resulted in a much cleaner code, and a more performant enemy detection logic.

Thanks both.

well done, least you have now identified and fixed it, as you found it isnt always easy identifying why, One way to assist that sometimes, is to turn the external variables to properties or use other tricks so when it changes you can notify yourself, then you can see changes going on

1 Like