Prefab Instance Affecting All Others

I’ve gotten a functional zombie in my game, he’ll chase me when I’m in range and I can shoot and kill him. However, when I saved him as a prefab and started dropping more into the scene, I got some problems.

1.) Shooting one zombie affects every zombie

2.) When I trigger the aggro cube for zombie (2), zombie (2) and (3) run in place while zombie (1) enables nav agent and runs toward me.

I think the problems might have to do with the health integer and aggro boolean being static, but if I remove the static part I can’t access them from other scripts, and I get the error “an object reference is required for the non-static field/method/property ‘zombie.Health’”

Main Zombie Code

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using Random = UnityEngine.Random;

public class Zombie : MonoBehaviour
{
    Animator Anim;
    public AudioSource zombieDeath;
    public AudioSource[] zombieDeathArray;

  

    public bool neverDone = true;
    bool neverAggro = true;
    public int health = 5;
    public static bool Aggro = false;
    // Start is called before the first frame update
    void Start()
    {
        Anim = GetComponent<Animator>();
        zombieDeath = zombieDeathArray[Random.Range(0, zombieDeathArray.Length)];
    }

    // Update is called once per frame
    void Update()
    {
        if (health <= 0) {
            if (neverDone == true)
            {

                PlayRandomDeathAudio();
                gameObject.GetComponent<NavMeshAgent>().enabled = false;
                Aggro = false;
                Anim.SetBool("Z_death_A", true);
                neverDone = false;
            }

        }

        if (Aggro == true && health >0)
        {
            if (neverAggro == true)
            {
            gameObject.GetComponent<NavMeshAgent>().enabled = true;
            Debug.Log("ZombieAggro!");
            Anim.SetBool("Z_run", true);
                neverAggro = false;
            }

        }
    }

    void PlayRandomDeathAudio()
    {
        if (ZombieShotBody.zombieHurt.isPlaying)
        {
                ZombieShotBody.zombieHurt.Stop();
        }

        if (AggroBoxScript.zombieAggro.isPlaying)
        {
            AggroBoxScript.zombieAggro.Stop();
        }

                zombieDeath = zombieDeathArray[Random.Range(0, zombieDeathArray.Length)];
                zombieDeath.Play();


    }
}

The Damage Script

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

public class ZombieShotBody : MonoBehaviour
{
    public static AudioSource zombieHurt;
    public AudioSource[] zombieHurtArray;
    public int damage = 1;
    // Start is called before the first frame update
    void Start()
    {
        zombieHurt = zombieHurtArray[Random.Range(0, zombieHurtArray.Length)];
    }



    // Update is called once per frame
    void Update()
    {
       
    }


    void OnTriggerEnter(Collider bullet)
    {
        if (bullet.gameObject.tag == "bullet")
        {

            Zombie.health -= damage;
            Debug.Log(Zombie.health);
            Destroy(bullet.gameObject);
            if (Zombie.health > 0)
            {
                PlayRandomHurtSound();
            }
        }
    }

    void PlayRandomHurtSound()
    {
        //   if (zombieAttack.isPlaying)
        //   {
        //       zombieAttack.Stop();
        //    }

        if (AggroBoxScript.zombieAggro.isPlaying)
        {
            AggroBoxScript.zombieAggro.Stop();
        }

        zombieHurt = zombieHurtArray[Random.Range(0, zombieHurtArray.Length)];
        zombieHurt.Play();
    }
}

Here’s the NPCmove script too, in case that’s where the nav problem is coming from

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

public class NPCMove : MonoBehaviour
{
    [SerializeField] Transform _destination;

    NavMeshAgent _navMeshAgent;

    // Start is called before the first frame update
    void Start()
    {
        _navMeshAgent = this.GetComponent<NavMeshAgent>();

        if (_navMeshAgent == null)
        {
            Debug.LogError("Navmesh agent not attached to " + gameObject.name);

        }
        else
        {
        //    SetDestination();
        }
    }



    // Update is called once per frame
    void Update()
    {
        if (Zombie.Aggro == true && Zombie.health > 0)
        {
            if (_destination.hasChanged)
            {
                _navMeshAgent.SetDestination(_destination.position);
                _destination.hasChanged = false;
            }

        }
    }
    private void SetDestination()
    {
        if (_destination != null)
        {

            Vector3 targetVector = _destination.transform.position;
            _navMeshAgent.SetDestination(targetVector);
 

        }
    }
}

My goal is to have every zombie behave independently, with separate health and navigation.

Spot on! Since you are using static variables, all the zombies share the same health value and aggro status.

You absolutely can access them from other scripts, and the warning is describing how you are supposed to do that. You have several zombies in your scene. Naturally writing Zombie.health to access their health doesn’t make sense. Which zombie’s health? When it’s a static variable, it’s all of their health. To access the health of a single zombie you need a reference to that zombie.

From every other script that is attached to the same GameObject as the main zombie script, you can do this to get a reference to the Zombie component:

Zombie myZombieComponent;

void Awake() {
  myZombieComponent = GetComponent<Zombie>();
}

Then if you want to change the health of the zombie for example you do this:

myZombieComponent.health -= damage;

Of course, make those fields not static so that all the zombies are not sharing health and aggro status anymore.

If you look at your scripts, you’re already using this exact technique for the NavMeshAgent component. Just do the same for the Zombie component.

2 Likes

Only use statics when you want one and only one copy of a variable or object. The AudioSource is a good example. Others include player score (doesn’t have to be static, but if so, it will persist across scenes).

For independent health and navigation on each zombie, they must not be static. To get a reference to them, you need a reference to their script. If you put them in using the Hierarchy, you can then drag them to the game object that needs the script reference (maybe the Player). This is fine for a few instances. If you’re going to make lots of them during runtime, you’ll hold an array of the zombies as you instantiate them, and a second array of references to their attached script (not essential but means only looking them up once). Then you set/get variables and call methods in each instance as needed. (I’m calling your script “zombieScript”)

zombie[5].zombieScript.health = 10;
zombie[9].zombieScript.PlayRandomHurtSound();

Thank you so much for the help! They now all aggro and take damage individually. Now I know how to do that in the future tysm :slight_smile: However, I’m still having an issue getting anyone other than the first zombie instance to navigate towards the player. When I trigger aggro on additional enemies, they enable their nav agent components, but aren’t moving. The first zombie does navigate to the player, but oddly it also gets an error:

““SetDestination” can only be called on an active agent that has been placed on a NavMesh.
UnityEngine.AI.NavMeshAgent:SetDestination (UnityEngine.Vector3)
NPCMove:Update () (at Assets/Scripts/NPCMove.cs:52)”

I’ve tried adding “this”,“Zombie”, and “myZombieComponent” to the _navMeshAgent and _destination, but it didn’t make a difference. Here’s what I have now:

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

public class NPCMove : MonoBehaviour
{
    [SerializeField] Transform _destination;

    NavMeshAgent _navMeshAgent;
    public GameObject Zombie;
    Zombie myZombieComponent;





    // Start is called before the first frame update
    void Start()
    {
        _navMeshAgent = this.GetComponent<NavMeshAgent>();

        if (_navMeshAgent == null)
        {
            Debug.LogError("Navmesh agent not attached to " + gameObject.name);

        }
        else
        {
        //    SetDestination();
        }
    }

    void Awake()
    {
        myZombieComponent = Zombie.GetComponent<Zombie>();
        if (myZombieComponent == null)
        {
            Debug.Log("myZombieComponentNULL");
        }
    }


    // Update is called once per frame
    void Update()
    {
        if (myZombieComponent.Aggro == true && myZombieComponent.health > 0)
        {
            if (_destination.hasChanged)
            {
                _navMeshAgent.SetDestination(_destination.position);
                _destination.hasChanged = false;
            }

        }
    }
    private void SetDestination()
    {
        if (_destination != null)
        {

            Vector3 targetVector = _destination.transform.position;
            _navMeshAgent.SetDestination(targetVector);
  

        }
    }
}

I GOT IT!!! All I had to do was change the _destination into a gameobject and have them navigate to the transform.position of that. Thanks again for everyone’s help!

I know it’s working now, but just to clarify, you only need a Vector3 for the _destination variable, not a GameObject or even a Transform. (It can be helpful to use a GO because you can visualize it in the scene, but ultimately all you’re referencing is the position, which is just a Vector3.) It’s important to know what arguments are expected from methods, all of which are listed in the docs:

As an example, maybe you want a list of possible destinations for your NavMeshAgents, so they can head towards different areas based on events in the game, time passing, etc. That would just be a list of Vector3s which you could create in code and set in the Inspector. No GameObjects holding the positions would be needed.

// fill in 10 different positions for your agents to navigate towards
public Vector3[] agentDestinations = new Vector3[10];

Also you created a method called SetDestination(), and as you’ve seen, the NavMeshAgent also has a method called that. Although what you’re doing works because the methods are in different namespaces, it’s really not a good idea…at the very least it’s confusing. If I’m ever concerned that I might be doing this with one of my methods or variables, I preface it with “my”, or if it’s very local, “temp”. Anyway, just a heads up. Glad you got things working!

1 Like