Welcome to GEMENTAR TOUR PUO V2 Development Documentation. Enjoy your stay!

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.


QR Code
QR Code wiki:npc_prototyping (generated for current page)
Hello World!
DokuWiki with monobook... rules!