NPC Prototyping
This is the editor view of our NPC working out! It's still a lot of bugs and a lot of works need to be done to optimize it, but for proof of concept I think this is acceptable

ZombieNpcMovingNashMeshController Script
using Sirenix.OdinInspector; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using DG.Tweening; using Micosmo.SensorToolkit; using UnityEngine.Events; using HighlightPlus; using System; public class ZombieNpcMovingNashMeshController : MonoBehaviour { private AudioSource _audioSource; [SerializeField] private AudioClip _attackAudio; [SerializeField] private AudioClip _dieAudio; [BoxGroup("Debug Value")] [SerializeField] private string d_tranfsormName; [BoxGroup("Debug Value")] [SerializeField] private float d_remainingDistance; [BoxGroup("Debug Value")] [SerializeField] private float d_velocity; [BoxGroup("Debug Value")] [SerializeField] private bool d_hasPath; [BoxGroup("Debug Value")] [SerializeField] private Vector3 d_velocityMagnitude; enum ZombieBehaviour { FollowCheckPoint, EatDeadBody, Chase, ChaseAttack, Die, Idle, } [EnumToggleButtons] [BoxGroup("Behaviour Selection")] [SerializeField] private ZombieBehaviour _zombieBehaviour = ZombieBehaviour.FollowCheckPoint; public bool isDead { get; private set; } = false; public bool isEatingHuman { get; private set; } = false; [BoxGroup("Movement Tweak")] [SerializeField] public float _walkSpeed { get; private set; } = 1; [BoxGroup("Movement Tweak")] [SerializeField] public float _runSpeed { get; private set; } = 5; [BoxGroup("CheckPoint Transform")] [SerializeField] private List<Transform> _checkPointTransform; [BoxGroup("CheckPoint Transform")] [SerializeField] private Transform _deadBodyTransform; [BoxGroup("CheckPoint Transform")] [SerializeField] private Transform _playerTransform; private int _checkPointIndex = 0; private int _currentIndex = 0; [BoxGroup("Sensor GameObject")] [SerializeField] private RangeSensor _bodyRangeSensor; [BoxGroup("Sensor GameObject")] [SerializeField] private RangeSensor _shortRangeSensor; [BoxGroup("Sensor GameObject")] [SerializeField] private RangeSensor _longRangeSensor; private NavMeshAgent _navMeshAgent; private ZombieAnimationController _zombieAnimationController; private HighlightEffect _highlight; private ZombieHealth _health; public UnityEvent<int> _sendEventToUI; private void Awake() { _health = GetComponent<ZombieHealth>(); _navMeshAgent = GetComponent<NavMeshAgent>(); _highlight = GetComponent<HighlightEffect>(); _zombieAnimationController = GetComponent<ZombieAnimationController>(); _playerTransform = GameObject.FindGameObjectWithTag("Player").transform; _sendEventToUI = GameObject.FindGameObjectWithTag("Player").GetComponent<ZombieGunController>().sendEventToUI; _health.sendEventOnMinusHealth.AddListener(ReceivedEventOnMinusHealth); _navMeshAgent.updateRotation = false; _audioSource = GetComponent<AudioSource>(); } private void Update() { UpdateBehaviour(); UpdateDebugList(); } private bool _hadDead = false; private void UpdateBehaviour() { switch (_zombieBehaviour) { case ZombieBehaviour.FollowCheckPoint: ResetAllBoolean(); BehaviourNpcFollowCheckPoint(); break; case ZombieBehaviour.EatDeadBody: ResetAllBoolean(); MakeCharacterRunning(); isEatingHuman = true; MoveAgentToDestination(_deadBodyTransform); break; case ZombieBehaviour.Chase: ResetAllBoolean(); MakeCharacterRunning(); MoveAgentToDestination(_playerTransform); break; case ZombieBehaviour.ChaseAttack: ResetAllBoolean(); MakeCharacterPunch(); MakeCharacterRunning(); MoveAgentToDestination(_playerTransform); break; case ZombieBehaviour.Idle: ResetAllBoolean(); MoveAgentToDestination(this.transform); break; case ZombieBehaviour.Die: ResetAllBoolean(); isDead = true; MoveAgentToDestination(this.transform); break; default: break; } } private void UpdateDebugList() { d_tranfsormName = transform.gameObject.name; d_remainingDistance = _navMeshAgent.remainingDistance; d_velocity = _navMeshAgent.velocity.magnitude; d_hasPath = _navMeshAgent.hasPath; d_velocityMagnitude = _navMeshAgent.velocity.normalized; } private void ResetAllBoolean() { MakeCharacterWalk(); isDead = false; isEatingHuman = false; } private void BehaviourNpcFollowCheckPoint() { if (_navMeshAgent.remainingDistance <= _navMeshAgent.stoppingDistance) { _currentIndex = _checkPointIndex == _checkPointTransform.Count - 1 ? _checkPointIndex = 0 : _checkPointIndex += 1; } MoveAgentToDestination(_checkPointTransform[_currentIndex]); } private void MoveAgentToDestination(Transform transform) { _navMeshAgent.destination = transform.position; if (_navMeshAgent.velocity.sqrMagnitude > Mathf.Epsilon) { //this.transform.rotation = Quaternion.LookRotation(_navMeshAgent.velocity.normalized, Vector3.up); Vector3 vector3 = new Vector3(_navMeshAgent.velocity.normalized.x, 0f, _navMeshAgent.velocity.normalized.z); this.transform.rotation = Quaternion.LookRotation(vector3, Vector3.up); } } private void MakeCharacterWalk() => _navMeshAgent.speed = _walkSpeed; private void MakeCharacterRunning() => _navMeshAgent.speed = _runSpeed; private void MakeCharacterPunch() => _zombieAnimationController.PlayAction(_zombieAnimationController.attack); public bool DetectPlayerOnLongAgro() { if (_longRangeSensor.GetNearestDetection() != null) { return true; } return false; } public bool DetectPlayerOnShortAgro() { if (_shortRangeSensor.GetNearestDetection() != null) { return true; } return false; } public bool DetectPlayerOnBodyAgro() { if (_bodyRangeSensor.GetNearestDetection() != null) { return true; } return false; } #region ENUM SETTER public void SetBehaviourFollowCheckPoint() => _zombieBehaviour = ZombieBehaviour.FollowCheckPoint; public void SetBehaviourEatBody() => _zombieBehaviour = ZombieBehaviour.EatDeadBody; public void SetBehaviourChase() => _zombieBehaviour = ZombieBehaviour.Chase; public void SetBehaviourChaseAttack() { _zombieBehaviour = ZombieBehaviour.ChaseAttack; transform.LookAt(_playerTransform, Vector3.up); } public void SetBehaviourDie() => _zombieBehaviour = ZombieBehaviour.Die; public void SetBehaviourIdle() => _zombieBehaviour = ZombieBehaviour.Idle; #endregion #region EVENT RECEIVED private void ReceivedEventOnMinusHealth() { _highlight.HitFX(); } public void HitPlayerIfDetected() { _audioSource.PlayOneShot(_attackAudio); try { if (_bodyRangeSensor.GetNearestDetection().TryGetComponent(out ZombieHealth health)) { if (health == null) return; health.MinusHealth(10f); } } catch (NullReferenceException) { //TODO FIX THIS NULL EXCEPTION! } } #endregion }
ZombieAnimationController Script
using System.Collections; using System.Collections.Generic; using UnityEngine; using DG.Tweening; using Micosmo.SensorToolkit; using Sirenix.OdinInspector; public class ZombieNpcMovingController : MonoBehaviour { public List<Transform> checkPoint; private int checkPointIndex = 0; [SerializeField] private AudioClip _attackAudio; private CharacterController _characterController; private ZombieAnimationController _zombieAnimationController; private AudioSource _audioSource; [BoxGroup("Movement Tweak")] [SerializeField] private float _walkSpeed = 1; [BoxGroup("Movement Tweak")] [SerializeField] private float _runSpeed = 3; private ISteeringSensor _steering; private SteeringSensor _steeringSensor; private void Start() { Initialization(); _steeringSensor.Locomotion.MaxForwardSpeed = _walkSpeed; } private void Initialization() { _steering = GetComponentInChildren<ISteeringSensor>(); _steeringSensor = GetComponentInChildren<SteeringSensor>(); _characterController = GetComponent<CharacterController>(); _zombieAnimationController = GetComponent<ZombieAnimationController>(); _audioSource = GetComponent<AudioSource>(); } void Update() { MoveToCheckPoint(); } private void MoveToCheckPoint() { if (_steering.IsDestinationReached) { int index = checkPointIndex == checkPoint.Count - 1 ? checkPointIndex = 0 : checkPointIndex += 1; _steering.Seek.DestinationTransform = checkPoint[index]; } } [Button] public void MakeCharacterWalk() { _steeringSensor.Locomotion.MaxForwardSpeed = _walkSpeed; } [Button] public void MakeCharacterRunning() { _steeringSensor.Locomotion.MaxForwardSpeed = _runSpeed; } [Button] public void MakeCharacterPunch() { _zombieAnimationController.PlayAction(_zombieAnimationController.attack); _audioSource.PlayOneShot(_attackAudio); } }
Node CANVAS!
By using node canvas we can create a behaviour tree visually, this is the first time we used visual behaviour tree.
Agro
There's basically three layers of detection - outer layer when zombie heard shoots they will go agro, and this layer player need to escape from it to stop the agro. - first inner when player move close to zombie. - nearest layer for zombie detect when can it hit player.