Search Unity

Question Prefab Instance Affecting All Others

Discussion in 'Scripting' started by larynachos, Mar 20, 2021.

  1. larynachos

    larynachos

    Joined:
    Aug 6, 2012
    Posts:
    8
    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

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.AI;
    6. using Random = UnityEngine.Random;
    7.  
    8. public class Zombie : MonoBehaviour
    9. {
    10.     Animator Anim;
    11.     public AudioSource zombieDeath;
    12.     public AudioSource[] zombieDeathArray;
    13.  
    14.  
    15.  
    16.     public bool neverDone = true;
    17.     bool neverAggro = true;
    18.     public int health = 5;
    19.     public static bool Aggro = false;
    20.     // Start is called before the first frame update
    21.     void Start()
    22.     {
    23.         Anim = GetComponent<Animator>();
    24.         zombieDeath = zombieDeathArray[Random.Range(0, zombieDeathArray.Length)];
    25.     }
    26.  
    27.     // Update is called once per frame
    28.     void Update()
    29.     {
    30.         if (health <= 0) {
    31.             if (neverDone == true)
    32.             {
    33.  
    34.                 PlayRandomDeathAudio();
    35.                 gameObject.GetComponent<NavMeshAgent>().enabled = false;
    36.                 Aggro = false;
    37.                 Anim.SetBool("Z_death_A", true);
    38.                 neverDone = false;
    39.             }
    40.  
    41.         }
    42.  
    43.         if (Aggro == true && health >0)
    44.         {
    45.             if (neverAggro == true)
    46.             {
    47.             gameObject.GetComponent<NavMeshAgent>().enabled = true;
    48.             Debug.Log("ZombieAggro!");
    49.             Anim.SetBool("Z_run", true);
    50.                 neverAggro = false;
    51.             }
    52.  
    53.         }
    54.     }
    55.  
    56.     void PlayRandomDeathAudio()
    57.     {
    58.         if (ZombieShotBody.zombieHurt.isPlaying)
    59.         {
    60.                 ZombieShotBody.zombieHurt.Stop();
    61.         }
    62.  
    63.         if (AggroBoxScript.zombieAggro.isPlaying)
    64.         {
    65.             AggroBoxScript.zombieAggro.Stop();
    66.         }
    67.  
    68.                 zombieDeath = zombieDeathArray[Random.Range(0, zombieDeathArray.Length)];
    69.                 zombieDeath.Play();
    70.  
    71.  
    72.     }
    73. }
    74.  
    The Damage Script

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class ZombieShotBody : MonoBehaviour
    6. {
    7.     public static AudioSource zombieHurt;
    8.     public AudioSource[] zombieHurtArray;
    9.     public int damage = 1;
    10.     // Start is called before the first frame update
    11.     void Start()
    12.     {
    13.         zombieHurt = zombieHurtArray[Random.Range(0, zombieHurtArray.Length)];
    14.     }
    15.  
    16.  
    17.  
    18.     // Update is called once per frame
    19.     void Update()
    20.     {
    21.        
    22.     }
    23.  
    24.  
    25.     void OnTriggerEnter(Collider bullet)
    26.     {
    27.         if (bullet.gameObject.tag == "bullet")
    28.         {
    29.  
    30.             Zombie.health -= damage;
    31.             Debug.Log(Zombie.health);
    32.             Destroy(bullet.gameObject);
    33.             if (Zombie.health > 0)
    34.             {
    35.                 PlayRandomHurtSound();
    36.             }
    37.         }
    38.     }
    39.  
    40.     void PlayRandomHurtSound()
    41.     {
    42.         //   if (zombieAttack.isPlaying)
    43.         //   {
    44.         //       zombieAttack.Stop();
    45.         //    }
    46.  
    47.         if (AggroBoxScript.zombieAggro.isPlaying)
    48.         {
    49.             AggroBoxScript.zombieAggro.Stop();
    50.         }
    51.  
    52.         zombieHurt = zombieHurtArray[Random.Range(0, zombieHurtArray.Length)];
    53.         zombieHurt.Play();
    54.     }
    55. }
    56.  


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

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.AI;
    6.  
    7. public class NPCMove : MonoBehaviour
    8. {
    9.     [SerializeField] Transform _destination;
    10.  
    11.     NavMeshAgent _navMeshAgent;
    12.  
    13.     // Start is called before the first frame update
    14.     void Start()
    15.     {
    16.         _navMeshAgent = this.GetComponent<NavMeshAgent>();
    17.  
    18.         if (_navMeshAgent == null)
    19.         {
    20.             Debug.LogError("Navmesh agent not attached to " + gameObject.name);
    21.  
    22.         }
    23.         else
    24.         {
    25.         //    SetDestination();
    26.         }
    27.     }
    28.  
    29.  
    30.  
    31.     // Update is called once per frame
    32.     void Update()
    33.     {
    34.         if (Zombie.Aggro == true && Zombie.health > 0)
    35.         {
    36.             if (_destination.hasChanged)
    37.             {
    38.                 _navMeshAgent.SetDestination(_destination.position);
    39.                 _destination.hasChanged = false;
    40.             }
    41.  
    42.         }
    43.     }
    44.     private void SetDestination()
    45.     {
    46.         if (_destination != null)
    47.         {
    48.  
    49.             Vector3 targetVector = _destination.transform.position;
    50.             _navMeshAgent.SetDestination(targetVector);
    51.  
    52.  
    53.         }
    54.     }
    55. }
    56.  
    My goal is to have every zombie behave independently, with separate health and navigation.
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    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:

    Code (CSharp):
    1. Zombie myZombieComponent;
    2.  
    3. void Awake() {
    4.   myZombieComponent = GetComponent<Zombie>();
    5. }
    Then if you want to change the health of the zombie for example you do this:
    Code (CSharp):
    1. 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.
     
    larynachos and seejayjames like this.
  3. seejayjames

    seejayjames

    Joined:
    Jan 28, 2013
    Posts:
    691
    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();
     
  4. larynachos

    larynachos

    Joined:
    Aug 6, 2012
    Posts:
    8
    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 :) 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:

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.AI;
    6.  
    7. public class NPCMove : MonoBehaviour
    8. {
    9.     [SerializeField] Transform _destination;
    10.  
    11.     NavMeshAgent _navMeshAgent;
    12.     public GameObject Zombie;
    13.     Zombie myZombieComponent;
    14.  
    15.  
    16.  
    17.  
    18.  
    19.     // Start is called before the first frame update
    20.     void Start()
    21.     {
    22.         _navMeshAgent = this.GetComponent<NavMeshAgent>();
    23.  
    24.         if (_navMeshAgent == null)
    25.         {
    26.             Debug.LogError("Navmesh agent not attached to " + gameObject.name);
    27.  
    28.         }
    29.         else
    30.         {
    31.         //    SetDestination();
    32.         }
    33.     }
    34.  
    35.     void Awake()
    36.     {
    37.         myZombieComponent = Zombie.GetComponent<Zombie>();
    38.         if (myZombieComponent == null)
    39.         {
    40.             Debug.Log("myZombieComponentNULL");
    41.         }
    42.     }
    43.  
    44.  
    45.     // Update is called once per frame
    46.     void Update()
    47.     {
    48.         if (myZombieComponent.Aggro == true && myZombieComponent.health > 0)
    49.         {
    50.             if (_destination.hasChanged)
    51.             {
    52.                 _navMeshAgent.SetDestination(_destination.position);
    53.                 _destination.hasChanged = false;
    54.             }
    55.  
    56.         }
    57.     }
    58.     private void SetDestination()
    59.     {
    60.         if (_destination != null)
    61.         {
    62.  
    63.             Vector3 targetVector = _destination.transform.position;
    64.             _navMeshAgent.SetDestination(targetVector);
    65.  
    66.  
    67.         }
    68.     }
    69. }
    70.  
     
  5. larynachos

    larynachos

    Joined:
    Aug 6, 2012
    Posts:
    8
    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!
     
  6. seejayjames

    seejayjames

    Joined:
    Jan 28, 2013
    Posts:
    691
    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:

    https://docs.unity3d.com/ScriptReference/AI.NavMeshAgent.SetDestination.html

    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.

    Code (CSharp):
    1. // fill in 10 different positions for your agents to navigate towards
    2. 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!
     
    Last edited: Mar 22, 2021
    larynachos likes this.