Enemy Unit AI / Animation Glitch

Getting an odd issue with the Enemy AI/Animation.

When the player is within range they work perfectly and will follow the player unit, attacking when they are close enough.

When they are out of the follow distance they seem to just stutter on the spot, looking at the Animator the attack and stopAttack trigger is glitching like crazy.

As far as I can tell my code is correct.

using UnityEngine;
using RPG.Combat;

namespace RPG.Control
{
    public class AIController : MonoBehaviour
    {
        [SerializeField] float chaseDistance = 5f;

        Fighter fighter;
        GameObject player;

        private void Start()
        {
            fighter = GetComponent<Fighter>();
            player = GameObject.FindWithTag("Player");
        }


        private void Update()
        {
            if (InAttackRangeOfPlayer() && fighter.CanAttack(player))
            {
                fighter.Attack(player); 
            }
            else 
            {
                fighter.Cancel();
            }

        }

        private bool InAttackRangeOfPlayer()
        {
            float distanceToPlayer = Vector3.Distance(player.transform.position, transform.position);
            return distanceToPlayer < chaseDistance;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Movement;
using RPG.Core;

namespace RPG.Combat
{
    public class Fighter : MonoBehaviour, IAction
    {
        [SerializeField] float weaponRange = 2f;
        [SerializeField] float timeBetweenAttacks = 1f;
        [SerializeField] float weaponDamage = 5f;

        Health target;
        float timeSinceLastAttack = 0;

        private void Update()
        {
            timeSinceLastAttack += Time.deltaTime;

            if (target == null) { return; }
            if (target.IsDead()) { return; }

            // tutorial screenshot has if (!GetIsInRange())
            if (!GetIsInRange())
            {
                GetComponent<Mover>().MoveTo(target.transform.position);
                
            }
            else
            {
                GetComponent<Mover>().Cancel();           
                AttackBehaviour();                               
            }
        }

        private void AttackBehaviour()
        {
            transform.LookAt(target.transform.position);
            if (timeSinceLastAttack > timeBetweenAttacks)
            {
                // This will trigger the Hit() (animation) event below.
                TriggerAttack();
                timeSinceLastAttack = 0;
            }
        }

        private void TriggerAttack()
        {
            GetComponent<Animator>().ResetTrigger("stopAttack");
            GetComponent<Animator>().SetTrigger("attack");
        }



        // Animation Event
        void Hit()
        {
            if(target == null) { return; } 
            target.TakeDamage(weaponDamage);
        }

        private bool GetIsInRange()
        {
            return Vector3.Distance(transform.position, target.transform.position) < weaponRange;
        }

        public bool CanAttack(GameObject combatTarget)
        {
            if (combatTarget == null) { return false; }
            Health targetToTest = combatTarget.GetComponent<Health>();
            return targetToTest != null && !targetToTest.IsDead();
        }

        public void Attack(GameObject combatTarget)
        {
            GetComponent<ActionScheduler>().StartAction(this);
            target = combatTarget.GetComponent<Health>();
        }

        public void Cancel()
        {
            StopAttack();
            target = null;
        }

        private void StopAttack()
        {
            GetComponent<Animator>().SetTrigger("attack");
            GetComponent<Animator>().SetTrigger("stopAttack");
        }
    }
}

using UnityEngine;
using RPG.Movement;
using System;
using RPG.Combat;

namespace RPG.Control
{
    public class PlayerController : MonoBehaviour
    {
        private void Update()
        {
            if (InteractWithCombat()) return;
            if (InteractWithMovement()) return;
        }

        private bool InteractWithCombat()
        {
            RaycastHit[] hits = Physics.RaycastAll(GetMouseRay());
            foreach (RaycastHit hit in hits)
            {
                CombatTarget target = hit.transform.GetComponent<CombatTarget>();
                if (target == null) continue; 


                if (!GetComponent<Fighter>().CanAttack(target.gameObject))
                {
                    continue;
                }

                if (Input.GetMouseButtonDown(0))
                {
                    GetComponent<Fighter>().Attack(target.gameObject);                    
                }
                return true;
            } 
            return false;
        }
       
        private bool InteractWithMovement()
        {
            RaycastHit hit;
            bool hasHit = Physics.Raycast(GetMouseRay(), out hit);
            if (hasHit)
            {
                if (Input.GetMouseButton(0))
                {
                    GetComponent<Mover>().StartMoveAction(hit.point);
                }
                return true;                
            }
            return false;
        }

        private static Ray GetMouseRay()
        {
            return Camera.main.ScreenPointToRay(Input.mousePosition);
        }
    }
}

I have made sure Enemy has the scripts
Mover, Health, Fighter, Action Scheduler, Combat Target and AI controller.

Player has
Mover, Health, Player Controller, Fighter and Action Scheduler

I’m sure it’s something simple I am overlooking but I’ve been looking at it for hours.
Originally they weren’t moving at all but realised I had forgot to add Action Scheduler and Fighter to my Enemy prefab.

Any help would be appreciated.

This is generally caused because the NavMeshAgent can’t get to the “exact” location specified by the click (this is a general float comparison error that is common in computer math).

The solution is to make sure that the mover stops when the character is close… Add this to your Mover.cs Update method

if (!agent.isStopped && agent.remainingDistance < 1.0f) agent.isStopped = true; 

You’ll also need to add

agent.isStopped=false;

to your Mover.cs.
If you’re still using GetComponent<NavMeshAgent>() everywhere within Mover, it’s a good idea to create a global variable NavMeshAgent agent; and add

agent = GetComponent<NavMeshAgent>();

to Awake.cs.

Hi Brian,

Thank you for the reply.

I’ll have a look tonight/tomorrow at my code and see if I can implement that.

I don’t currently have an awake.cs, I am assuming this is something that is going to be implemented later on in the course.

Currently as part of the course my global variable for is declared in mover.cs as:

NavMeshAgent navMeshAgent;

and then initialised

 private void Start()
        {
            navMeshAgent = GetComponent<NavMeshAgent>();
        }

full mover.cs code below

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

namespace RPG.Movement
{
    public class Mover : MonoBehaviour, IAction
    {
        [SerializeField] Transform target;

        NavMeshAgent navMeshAgent;

        private void Start()
        {
            navMeshAgent = GetComponent<NavMeshAgent>();
        }

        void Update()
        {
            UpdateAnimator();
        }

        public void StartMoveAction(Vector3 destination)
        {
            GetComponent<ActionScheduler>().StartAction(this);
            MoveTo(destination);
        }

        public void MoveTo(Vector3 destination)
        {
            navMeshAgent.destination = destination;
            navMeshAgent.isStopped = false;
        }

        public void Cancel()
        {
            navMeshAgent.isStopped = true;
        }

        private void UpdateAnimator()
        {
            Vector3 velocity = navMeshAgent.velocity;
            Vector3 localVelocity = transform.InverseTransformDirection(velocity);
            float speed = localVelocity.z;
            GetComponent<Animator>().SetFloat("forwardSpeed", speed);
        }

    }
}

I’ve made a quick video and put it on youtube to show what I mean.
I plan on removing the video later.

Youtube video

As long as you’re caching the agent, Start or Awake is fine. Generally, it’s best practice to cache references in Awake() which is the very first thing called on any MonoBehavior.

It turns out, my initial diagnosis was premature, though.

After looking at the video, I think I see what’s going on…
In StopAttack, you’re calling SetTrigger("attack"); when that should be ResetTrigger("attack");
Since you’re calling Fighter.cancel every Update, this is causing the enemy fighter’s to spaz out.

It’s not ideal to call SetTrigger and ResetTrigger every frame, but once we start adding in patrol paths, this should change.

Thank you so much!
It was driving me a little crazy. I had re-read the code so many times but still managed to miss it!

It’s easy to miss.
I can write most of the classes out from memory these days (from being immersed in the code as a TA), and I had to read that method 3 times before I spotted it, and only then after the video gave me an idea what to look for.

1 Like

The animation made me think of looking there too, but I read and re-read the code and still didn’t spot it! At least I looked in the right area originally, even if I didn’t notice the issue.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms