1
\$\begingroup\$

I'm currently working on a boss fight for my game. My problem is that I keep getting a NullReferenceException error that prevents the boss (named The Smallest Stain) from moving in the Shoot State. I understand the different meanings of this error, but I don't understand its meaning when all references are set to specific game objects.

Let's start from the beginning to make the problem clearer.

This is the battle area. The player character is on the left and the boss is on the right. When in the Shoot State, I want the boss to change their position depending on where the player is right now.

enter image description here

To do this, I placed triggers on all the platforms and created empty children for them that act as the boss' new positions (They're visible as blue-green circles in the image).

enter image description here

This image shows the objects' organization in the hierarchy:

enter image description here

And this is how the Animator states look like for the boss (For the purposes of this question, I don't write about the Laugh state here):

enter image description here

I wrote the following code and added it to each Trigger gameobject:

using UnityEngine;

public class TheSmallestStain_ShootingTriggers : MonoBehaviour
{
    // These are the positions for the boss.
    public Transform bossPosition1;
    public Transform bossPosition2;
    public Transform bossPosition3;
    public Transform bossPosition4;
    public Transform bossPosition5;
    public Transform bossPosition6;
    public Transform bossPosition7;
    public Transform bossPosition8;
    public Transform bossPosition9;
    public Transform bossPosition10;

    public static bool enteredTrigger1;
    public static bool enteredTrigger2;
    public static bool enteredTrigger3;
    public static bool enteredTrigger4;
    public static bool enteredTrigger5;
    public static bool enteredTrigger6;
    public static bool enteredTrigger7;
    public static bool enteredTrigger8;
    public static bool enteredTrigger9;
    public static bool enteredTrigger10;

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject == Player.Instance.gameObject)
        {
            if (gameObject.name == "Trigger 1")
            {
                enteredTrigger1 = true;
                Debug.Log("Yes! It works!");  // This Debug.Log works and I see the message every time I play the game.
            }

            else if (gameObject.name == "Trigger 2")
            {
                enteredTrigger2 = true;
            }

            else if (gameObject.name == "Trigger 3")
            {
                enteredTrigger3 = true;
            }

            else if (gameObject.name == "Trigger 4")
            {
                enteredTrigger4 = true;
            }

            else if (gameObject.name == "Trigger 5")
            {
                enteredTrigger5 = true;
            }

            else if (gameObject.name == "Trigger 6")
            {
                enteredTrigger6 = true;
            }

            else if (gameObject.name == "Trigger 7")
            {
                enteredTrigger7 = true;
            }

            else if (gameObject.name == "Trigger 8")
            {
                enteredTrigger8 = true;
            }

            else if (gameObject.name == "Trigger 9")
            {
                enteredTrigger9 = true;
            }

            else if (gameObject.name == "Trigger 10")
            {
                enteredTrigger10 = true;
            }
        }
    }
}

Then, I used those references in this script, which is attached to "The Smallest Stain Shoot" State:

using UnityEngine;

public class TheSmallestStain_Shoot : StateMachineBehaviour
{
    public float timer;
    public float speed;

    private Transform playerPos;

    TheSmallestStain theSmallestStain;
    TheSmallestStain_ShootingTriggers shootingTriggers;

    GameObject theBoss;

// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    playerPos = GameObject.FindGameObjectWithTag("Player").GetComponent<Transform>();

    theSmallestStain = animator.GetComponent<TheSmallestStain>();
    shootingTriggers = animator.GetComponent<TheSmallestStain_ShootingTriggers>();

    theBoss = GameObject.Find("The Smallest Stain");

    timer = 5;
}

// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    theSmallestStain.LookAtPlayer();

    if (TheSmallestStain_ShootingTriggers.enteredTrigger1 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition1.position; // The error points out to this line.
    }


    if (TheSmallestStain_ShootingTriggers.enteredTrigger2 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition2.position;
    }


    if (TheSmallestStain_ShootingTriggers.enteredTrigger3 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition3.position;
    }


    if (TheSmallestStain_ShootingTriggers.enteredTrigger4 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition4.position;
    }


    if (TheSmallestStain_ShootingTriggers.enteredTrigger5 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition5.position;
    }


    if (TheSmallestStain_ShootingTriggers.enteredTrigger6 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition6.position;
    }


    if (TheSmallestStain_ShootingTriggers.enteredTrigger7 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition7.position;
    }


    if (TheSmallestStain_ShootingTriggers.enteredTrigger8 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition8.position;
    }


    if (TheSmallestStain_ShootingTriggers.enteredTrigger9 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition9.position;
    }


    if (TheSmallestStain_ShootingTriggers.enteredTrigger10 == true)
    {
        theBoss.transform.position = shootingTriggers.bossPosition10.position;
    }
}

// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        
    }
}

Now, we get to the error. I get it whenever the player enters the first Trigger and the boss enters the Shoot state. As you can see in the image, it points out to line 35, which is theBoss.transform.position = shootingTriggers.bossPosition1.position;. Essentially, the line that should change the position of the boss to their new position. But the strange thing is that all those references are set to their objects (both in the script and Inspector). So, I don't know why it's not working.

enter image description here

I would appreciate it if you let me know in case I'm doing something wrong here. Thanks!

\$\endgroup\$
4
  • 2
    \$\begingroup\$ "I don't understand its meaning when all references are set to specific game objects". It means at least one reference is NOT set to a specific game object. Your belief that all references the code is using are populated in this execution pathway is incorrect. This often comes from accidentally running two copies of the script: one with references populated correctly, and one with a reference missing. When you check, it looks like everything is assigned correctly, because you're looking at a different instance than the one causing the error. Add a debug log to report which reference is null. \$\endgroup\$
    – DMGregory
    Jun 24 at 3:31
  • 2
    \$\begingroup\$ Also, when you find yourself copying and pasting the same.variable declaration with sequential numbers at the end, it's time to use an array. Your code can be made much simpler and easier to maintain this way. \$\endgroup\$
    – DMGregory
    Jun 24 at 3:35
  • 2
    \$\begingroup\$ Additionally I would remove all those if conditions in OnTriggerEnter2D and replace them with something like theBoss.AddTargetPosition(this.transform.position); and then the boss can use it directly to position itself. \$\endgroup\$
    – Nikaas
    Jun 24 at 6:29
  • \$\begingroup\$ @Nikaas Thank you for your comment! If I remove the conditions completely, the boss can't move to the correct position when the player enters a specific trigger. But I will try that line of code along with the code in the answer because there were some issues with the way I assigned boss positions. \$\endgroup\$
    – Ali
    Jun 25 at 0:28

1 Answer 1

3
\$\begingroup\$

First let's simplify your shooting triggers script. We'll use arrays to make the code shorter and faster. Instead of looking for ten different hard-coded names (and paying string comparison costs for each attempt), we'll just add an integer field you can set in the inspector to let each instance know which index it should use (starting at zero).

This script doesn't use bossPositions anywhere, so that data probably does not belong in this script and should be hosted somewhere else.

using UnityEngine;

public class TheSmallestStain_ShootingTriggers : MonoBehaviour
{
    // These are the positions for the boss.
    public Transform[] bossPositions;

    // Set in inspector (way faster and more reliable
    // than tem if statements doing string
    // comparisons on the game object's name)
    public int triggerId;

    public static bool[] enteredTriggers = new bool[10];

    private void OnTriggerEnter2D(Collider2D collision)
    {
        // Consider using physics layers so that
        // the player is the ONLY thing that collides
        // with the trigger's layer, then you don't
        // need this if check at all.
        if (collision.gameObject == Player.Instance.gameObject)
        {
            enteredTriggers[triggerId] = true;
        }
    }
}

Next, we'll apply similar simplifications to your shooting script, and add error logging so you get better feedback about where your mistake is.

using UnityEngine;

public class TheSmallestStain_Shoot : StateMachineBehaviour
{
    public float timer;
    public float speed;

    private Transform playerPos;

    TheSmallestStain theSmallestStain;
    TheSmallestStain_ShootingTriggers shootingTriggers;

    GameObject theBoss;

    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // Never search by name if you have another way.
        playerPos = Player.instance.transform;

        if (!animator.TryGetComponent(out theSmallestStain)) {
            Debug.LogError("No smallest stain component found");
        }
    
        if (!animator.TryGetComponent(out shootingTriggers)) {
            Debug.LogError("No shooting triggers component found");
        };

        // Would this not be the same object that has 
        // the SmallestStain component?
        theBoss = GameObject.Find("The Smallest Stain");

        if (theBoss == null) {
            Debug.LogError("No boss found");
        }

        timer = 5;
    }

    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        theSmallestStain.LookAtPlayer();

        int maxTriggerEntered = -1;
        for (int i = 0; i < TheSmallestStain_ShootingTriggers.enteredTriggers.Length; i++) {
            if (TheSmallestStain_ShootingTriggers.enteredTriggers[i]) maxTriggerEntered = i;
        }

        if (maxTriggerEntered > -1 && maxTriggerEntered < shootingTriggers.bossPositions.Length) {
            if (shootingTriggers.bossPositions[maxTriggerEntered] == null) {
                Debug.LogError($"Boss position {maxTriggerEntered} is null");
            } else if (theBoss == null) {
                Debug.LogError("Something destroyed the boss");
            } else {
                theBoss.transform.position = shootingTriggers.bossPosition[maxTriggerEntered].position;
            }
        }
    }
}

There we go. Shorter, faster code, which will tell you exactly which reference is null when you've set it up incorrectly.

"There are no contradictions: when you perceive one, check your assumptions."

\$\endgroup\$
6
  • \$\begingroup\$ Thank you very much for your answer and comments! Before seeing the answer, I checked all the variables and found that 'shootingTriggers' was the null variable. This was because I used 'animator' to access it while it wasn't attached to the game object that had the animator component. So, I added that variable to another script and dragged it into the inspector. This solved the error; but for some reason, the boss couldn't transition to the correct positions. Now, with the help of your answer, the code is noticeably short and efficient. (Continues in the next comment) \$\endgroup\$
    – Ali
    Jun 24 at 23:37
  • \$\begingroup\$ So, I use the new scripts now (I know that Debug.LogError lines were added to find the null variable; but I'd like to keep them as well so I can fix the potential future bugs more easily). But there are some errors in both scripts that I should fix before I can test them. The first two errors are from the second half of public static bool[] enteredTriggers = new[10]; which state that the rank specifier is invalid and no best type found for implicitly typed array. \$\endgroup\$
    – Ali
    Jun 24 at 23:53
  • \$\begingroup\$ The second error is on the second part of theBoss.transform.position = shootingTriggers.bossPositions[maxTriggerEntered]; and states that it can't convert Transform to Vector3. I'm trying my best to solve them but it may take a while as I don't have experience with these specific errors. \$\endgroup\$
    – Ali
    Jun 24 at 23:56
  • 1
    \$\begingroup\$ Trivial fixes added. \$\endgroup\$
    – DMGregory
    Jun 25 at 1:38
  • 1
    \$\begingroup\$ I don't recommend adding that to this answer because I don't recommend that way of accessing this data. You can post a new question about how to access boss positions from a StateMachineBehaviour if you'd like some software engineering advice about how to structure this. \$\endgroup\$
    – DMGregory
    Jun 26 at 0:48

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .