Table of Contents
NPC Prototyping
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
NPC Movement Prototype
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); } }
Behaviour Tree
Node CANVAS!
By using node canvas we can create a behaviour tree visually, this is the first time we used visual behaviour tree.
NPC Agro prototype
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.
NavMesh Agent AI
NavMesh
We use navigation mesh to map place that NPC can move along, Unity NavMesh also handle navigation.