Enemy Chasing State Line of Sight [Further Code Review]

Hello. I’m trying to introduce a line of sight into my enemy chasing state part of my state Machine, so that my enemies can chase me around Obstacles. However, for some reason, mine is a little unpredictable. In other words, sometimes it works, sometimes it doesn’t

Can someone please have a look at my script and see if it can be fixed?

using UnityEngine;
using RPG.Skills;
using UnityEngine.AI;

namespace RPG.States.Enemies
{
    public class EnemyChasingState : EnemyBaseState
    {
        public EnemyChasingState(EnemyStateMachine stateMachine) : base(stateMachine) {}

        private float attackingRange;

        public readonly int TargetingForwardSpeedHash = Animator.StringToHash("TargetingForwardSpeed");

        public override void Enter()
        {
            Debug.Log($"{stateMachine.gameObject.name} has entered chasing state");
            stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
            attackingRange = stateMachine.Fighter.GetAttackingRange();
            attackingRange *= attackingRange;
        }

        public override void Tick(float deltaTime)
        {

        if (!IsInChaseRange() && !IsAggrevated())
        {
            stateMachine.TriggerOnPlayerOutOfChaseRange();
            stateMachine.SwitchState(new EnemyIdleState(stateMachine));
            return;
        }

        Vector3 lastPosition = stateMachine.transform.position;

        if (IsInAttackingRange())
        {
            // Solution to ensure the enemy can move around Obstacles, if his enemy (Player/another NPC) is behind a wall of some sort:
            if (stateMachine.LastAttacker != null)
            {
                if (stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Ranged || stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Magic)
                {
                    Debug.Log($"{stateMachine.gameObject.name} is wielding a ranged weapon, and LastAttacker is {stateMachine.LastAttacker.gameObject.name}");
                    // if (HasLineOfSight()) stateMachine.SwitchState(new EnemyAttackingState(stateMachine));
                    // else
                    if (!HasLineOfSight())
                    {
                        // Play the movement animation towards your goal:
                        stateMachine.Animator.SetFloat(FreeLookSpeedHash, 1f, stateMachine.AnimatorDampTime, deltaTime);
                        Debug.Log($"{stateMachine.gameObject.name} has no line of sight of {stateMachine.LastAttacker.gameObject.name}");
                        // the direction changing and movement itself should happen consistently as we are moving, so they're done inside the following while loop:
                        if (CanMoveTo(stateMachine.LastAttacker.transform.position))
                        {
                            Debug.Log($"{stateMachine.gameObject.name} is attempting to move to {stateMachine.LastAttacker.gameObject.name}");
                            // Get the movement direction through the Agent's desired velocity, including any contributions to obstacle avoidance:
                            Vector3 movementDirection = stateMachine.Agent.desiredVelocity.normalized;
                            // Face the direction of your movement:
                            FaceMovementDirection(movementDirection, deltaTime);
                            // Move to the target:
                            MoveToTarget(deltaTime);
                            return;
                        }
                    }
                }
            }

            if (!stateMachine.CooldownTokenManager.HasCooldown("Attack"))
            {
                // you're not on Attack Cooldown, so you're quite aggressive, so switch to attacking state
                Debug.Log($"{stateMachine.gameObject.name} is exiting chase state to attack state");
                stateMachine.SwitchState(new EnemyAttackingState(stateMachine));
                return;
            }

            else
            {
                Move(deltaTime);
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0);
                // TEST (SUCCESS): get the enemies to point at each other, instead of the player, if they're fighting one another:
                if (stateMachine.LastAttacker == null) stateMachine.SwitchState(new EnemyIdleState(stateMachine)); // ensures the enemy knows what to do if the enemy is dead (if this isn't here, NREs will be unleashed like bombs...!)
                else FaceTarget(stateMachine.LastAttacker.transform.position, deltaTime);

                // ORIGINAL:
                // FaceTarget(stateMachine.Player.transform.position, deltaTime);                    
                return;
            }
        }

            else MoveToTarget(deltaTime);

            // Updating the Movement Animation, so it all acts out smoothly (ALL STUFF BELOW):
            Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
            float deltaMagnitude = deltaMovement.magnitude;

            if (deltaMagnitude > 0) 
            {
                // if the game is not paused:
                FaceTarget(stateMachine.transform.position - deltaMovement, deltaTime);
                float grossSpeed = deltaMagnitude / deltaTime;
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, grossSpeed / stateMachine.MovementSpeed, stateMachine.AnimatorDampTime, deltaTime);
            }

            else 
            {
                // if the game is paused:
                FaceTarget(stateMachine.Player.transform.position, deltaTime);
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
            }
        }

        public override void Exit()
        {
            if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;
            stateMachine.Agent.ResetPath();
            stateMachine.Agent.velocity = Vector3.zero;
        }

        private bool IsInAttackingRange()
        {
            if (stateMachine.LastAttacker == null)
            {
                return Vector3.SqrMagnitude(stateMachine.Player.transform.position - stateMachine.transform.position) <= attackingRange;
            }

            else return Vector3.SqrMagnitude(stateMachine.LastAttacker.transform.position - stateMachine.transform.position) <= attackingRange;
        }

        public void MoveToTarget(float deltaTime)
        {
            if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true; // turn on the NavMeshAgent, otherwise the enemy won't be able to chase you down...
            
            /* // stateMachine.Agent.destination = stateMachine.Player.transform.position;
            // TEST: Instead of just hunting the player down everytime this enemy is attacked, hunt down the LastAttacker (it can be the player or another NPC...). 
            // If you found none, and this function is called, hunt the player down:
            stateMachine.Agent.destination = stateMachine.LastAttacker != null ? stateMachine.LastAttacker.transform.position : stateMachine.Player.transform.position;

            // The following 4 lines of code are replaced by a Test below them:

            Vector3 desiredVelocity = stateMachine.Agent.desiredVelocity.normalized; // Normalized Desired Speed of the NPC
            Move(desiredVelocity * stateMachine.MovementSpeed, deltaTime); // Go to the player, at that desired speed
            stateMachine.Agent.velocity = stateMachine.CharacterController.velocity; // The velocity of the player
            stateMachine.Agent.nextPosition = stateMachine.transform.position; // The next position to aim for */

            // Entire rest of this function is a TEST:
            Vector3 destination = stateMachine.LastAttacker != null ? stateMachine.LastAttacker.transform.position : stateMachine.Player.transform.position;
            if (CanMoveTo(destination))
            {
                stateMachine.Agent.destination = destination;
                Vector3 desiredVelocity = stateMachine.Agent.desiredVelocity.normalized;
                Move(desiredVelocity * stateMachine.MovementSpeed, deltaTime);
                stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
                stateMachine.Agent.nextPosition = stateMachine.transform.position;
            }
            else 
            {
                Debug.Log($"{stateMachine.gameObject.name} cannot move to the target. Stopping");
                stateMachine.Agent.ResetPath();
                stateMachine.Agent.velocity = Vector3.zero;
            }
        }

        private bool HasLineOfSight()
        {
            Debug.Log($"HasLineOfSight has been called");
            Ray ray = new Ray(stateMachine.transform.position + Vector3.up, stateMachine.LastAttacker.transform.position - stateMachine.transform.position);
            if (Physics.Raycast(ray, out RaycastHit hit, Vector3.Distance(stateMachine.transform.position, stateMachine.LastAttacker.transform.position - stateMachine.transform.position), stateMachine.LayerMask))
            {
                return false;
            }
            return true;
        }

        private bool CanMoveTo(Vector3 destination) 
        {
            var path = new NavMeshPath();
            var hasPath = NavMesh.CalculatePath(stateMachine.transform.position, destination, NavMesh.AllAreas, path);
            if (!hasPath) {Debug.Log($"no path found"); return false;}
            if (path.status != NavMeshPathStatus.PathComplete) {Debug.Log($"Incomplete Path found, Can't take this path"); return false;}
            if (GetPathLength(path) > stateMachine.maxNavPathLength) {Debug.Log($"Path is too long"); return false;}
            return true;
        }

        private float GetPathLength(NavMeshPath path) 
        {
            float total = 0;
            if (path.corners.Length < 2) return total;

            for (int i = 0; i < path.corners.Length - 1; i++) 
            {
                total += Vector3.Distance(path.corners[i], path.corners[i + 1]);
            }
            return total;
        }

        private void FaceMovementDirection(Vector3 forward, float deltaTime) 
        {
            if (forward == Vector3.zero) return;
            Quaternion desiredRotation = Quaternion.LookRotation(forward, Vector3.up);
            stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, desiredRotation, stateMachine.FreeLookRotationSpeed * deltaTime);
        }
    }
}

For the most part, I realized that if the enemy gets hit, it works. If he just had an arrow get near him in range, it won’t work

which… leads me to believe that I also need to set the last attacker properly when the arrow approaches the enemy, so he knows who to chase

and… yup, my suspicions were true. It was indeed because a ‘LastAttacker’ was never assigned, and I just did it in another fancy way:

using RPG.Combat;
using RPG.States.Enemies;
using UnityEngine;

public class IncomingRangedAttackDetector : MonoBehaviour
{
    [field: SerializeField] public EnemyStateMachine parentStateMachine {get; private set;}
    // later, introduce a 'probability' factor here,
    // to determine the chances of the dodge occuring,
    // based on other factors like weight, size, dexterity, etc.

    private void OnTriggerEnter(Collider other) 
    {
        Debug.Log($"Collider entered: {other.name}");
        if (other.TryGetComponent<Projectile>(out var projectile)) 
        {
            Debug.Log($"Projectile detected: {projectile.name}");
            if (projectile.GetInstigator().GetComponent<EnemyStateMachine>() == parentStateMachine) 
            {
                // the 'IncomingRangedAttackDetector' gameObject holder fired the arrow, and was detected by his detector:
                Debug.Log($"Arrow fired by me, ignore");
            }
            else 
            {
                Debug.Log($"Arrow fired by {projectile.GetInstigator().name}");
                if (parentStateMachine.GetLastAttacker() == null) {

                // only assign new LastAttacker if the victim wasn't busy 
                // with another enemy, otherwise prioritize the main threat
                Debug.Log($"Prioritizing the main goal, not getting distracted by arrows");
                parentStateMachine.SetLastAttacker(projectile.GetInstigator().gameObject);
                parentStateMachine.SetHasOpponent(true);
                parentStateMachine.SetOpponent(projectile.GetInstigator().gameObject);
                }
                else Debug.Log($"That was a little scary, but it didn't hit me");
            }
        }
    }
}

apparently scaring the NPC away with an arrow, and getting him to play a dodge animation out of fear of being hit, does not guarantee that he’ll pursue you intelligently around the map, because he’ll only do that if you are setup as his last attacker. So… I had to do it myself :stuck_out_tongue: (under the condition that he wasn’t in a fight with someone else. If he is, and the arrow barely passed by him, he can safely ignore that arrow because he didn’t get hit, and he has a bigger priority against someone else right now)

With everything I’m adding, I’m either shooting myself in the foot, or this is going to be an amazing game to play

Here’s to hoping for the second option!

and for the past few hours, after noticing some serious problems with the algorithm to follow the player through complex environment, where sometimes they’ll try to walk into the player or just don’t act right, I tried coming up with this:

            /* // Solution to ensure the enemy can move around Obstacles, if his enemy (Player/another NPC) is behind a wall of some sort:
            if (stateMachine.LastAttacker != null)
            {
                if (stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Ranged || stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Magic || stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Attack)
                {
                    Debug.Log($"{stateMachine.gameObject.name} is wielding a ranged or melee weapon, and LastAttacker is {stateMachine.LastAttacker.gameObject.name}");

                    if (!HasLineOfSight())
                    {
                        // Play the movement animation towards your goal:
                        stateMachine.Animator.SetFloat(FreeLookSpeedHash, 1f, stateMachine.AnimatorDampTime, deltaTime);
                        Debug.Log($"{stateMachine.gameObject.name} has no line of sight of {stateMachine.LastAttacker.gameObject.name}");
                        // the direction changing and movement itself should happen consistently as we are moving, so they're done inside the following while loop:
                        if (CanMoveTo(stateMachine.LastAttacker.transform.position))
                        {
                            Debug.Log($"{stateMachine.gameObject.name} is attempting to move to {stateMachine.LastAttacker.gameObject.name}");
                            // Get the movement direction through the Agent's desired velocity, including any contributions to obstacle avoidance:
                            Vector3 movementDirection = stateMachine.Agent.desiredVelocity.normalized;
                            // Face the direction of your movement:
                            FaceMovementDirection(movementDirection, deltaTime);
                            // Move to the target:
                            MoveToTarget(deltaTime);
                            return;
                        }
                    }

                    else 
                    {
                        if (IsInChaseRange()) 
                        {
                        stateMachine.SwitchState(new EnemyAttackingState(stateMachine));
                        return;
                        }
                        else 
                        {
                            Vector3 movementDirection = stateMachine.Agent.desiredVelocity.normalized;
                            // Face the direction of your movement:
                            FaceMovementDirection(movementDirection, deltaTime);
                            // Move to the target:
                            MoveToTarget(deltaTime);
                            return;
                        }
                    }
                }
            } */

in my ‘EnemyChasingState.cs’ script:

using UnityEngine;
using RPG.Skills;
using UnityEngine.AI;

namespace RPG.States.Enemies
{
    public class EnemyChasingState : EnemyBaseState
    {
        public EnemyChasingState(EnemyStateMachine stateMachine) : base(stateMachine) {}

        private float attackingRange;

        public readonly int TargetingForwardSpeedHash = Animator.StringToHash("TargetingForwardSpeed");

        public override void Enter()
        {
            Debug.Log($"{stateMachine.gameObject.name} has entered chasing state");
            stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
            attackingRange = stateMachine.Fighter.GetAttackingRange();
            attackingRange *= attackingRange;
        }

        public override void Tick(float deltaTime)
        {

        if (!IsInChaseRange() && !IsAggrevated())
        {
            stateMachine.TriggerOnPlayerOutOfChaseRange();
            stateMachine.SwitchState(new EnemyIdleState(stateMachine));
            return;
        }

        Vector3 lastPosition = stateMachine.transform.position;

        if (IsInAttackingRange())
        {
            /* // Solution to ensure the enemy can move around Obstacles, if his enemy (Player/another NPC) is behind a wall of some sort:
            if (stateMachine.LastAttacker != null)
            {
                if (stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Ranged || stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Magic || stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Attack)
                {
                    Debug.Log($"{stateMachine.gameObject.name} is wielding a ranged or melee weapon, and LastAttacker is {stateMachine.LastAttacker.gameObject.name}");

                    if (!HasLineOfSight())
                    {
                        // Play the movement animation towards your goal:
                        stateMachine.Animator.SetFloat(FreeLookSpeedHash, 1f, stateMachine.AnimatorDampTime, deltaTime);
                        Debug.Log($"{stateMachine.gameObject.name} has no line of sight of {stateMachine.LastAttacker.gameObject.name}");
                        // the direction changing and movement itself should happen consistently as we are moving, so they're done inside the following while loop:
                        if (CanMoveTo(stateMachine.LastAttacker.transform.position))
                        {
                            Debug.Log($"{stateMachine.gameObject.name} is attempting to move to {stateMachine.LastAttacker.gameObject.name}");
                            // Get the movement direction through the Agent's desired velocity, including any contributions to obstacle avoidance:
                            Vector3 movementDirection = stateMachine.Agent.desiredVelocity.normalized;
                            // Face the direction of your movement:
                            FaceMovementDirection(movementDirection, deltaTime);
                            // Move to the target:
                            MoveToTarget(deltaTime);
                            return;
                        }
                    }

                    else 
                    {
                        if (IsInChaseRange()) 
                        {
                        stateMachine.SwitchState(new EnemyAttackingState(stateMachine));
                        return;
                        }
                        else 
                        {
                            Vector3 movementDirection = stateMachine.Agent.desiredVelocity.normalized;
                            // Face the direction of your movement:
                            FaceMovementDirection(movementDirection, deltaTime);
                            // Move to the target:
                            MoveToTarget(deltaTime);
                            return;
                        }
                    }
                }
            } */

            if (!stateMachine.CooldownTokenManager.HasCooldown("Attack"))
            {
                // you're not on Attack Cooldown, so you're quite aggressive, so switch to attacking state
                Debug.Log($"{stateMachine.gameObject.name} is exiting chase state to attack state");
                stateMachine.SwitchState(new EnemyAttackingState(stateMachine));
                return;
            }

            else
            {
                Move(deltaTime);
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0);
                // TEST (SUCCESS): get the enemies to point at each other, instead of the player, if they're fighting one another:
                if (stateMachine.LastAttacker == null) stateMachine.SwitchState(new EnemyIdleState(stateMachine)); // ensures the enemy knows what to do if the enemy is dead (if this isn't here, NREs will be unleashed like bombs...!)
                else FaceTarget(stateMachine.LastAttacker.transform.position, deltaTime);

                // ORIGINAL:
                // FaceTarget(stateMachine.Player.transform.position, deltaTime);                    
                return;
            }
        }

            else MoveToTarget(deltaTime);

            // Updating the Movement Animation, so it all acts out smoothly (ALL STUFF BELOW):
            Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
            float deltaMagnitude = deltaMovement.magnitude;

            if (deltaMagnitude > 0) 
            {
                // if the game is not paused:
                FaceTarget(stateMachine.transform.position - deltaMovement, deltaTime);
                float grossSpeed = deltaMagnitude / deltaTime;
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, grossSpeed / stateMachine.MovementSpeed, stateMachine.AnimatorDampTime, deltaTime);
            }

            else 
            {
                // if the game is paused:
                FaceTarget(stateMachine.Player.transform.position, deltaTime);
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
            }
        }

        public override void Exit()
        {
            if (!stateMachine.Agent.enabled)
            {
                stateMachine.Agent.enabled = true;
                stateMachine.Agent.ResetPath();
                stateMachine.Agent.velocity = Vector3.zero;
                stateMachine.Agent.enabled = false;
            }

            else
            {
                stateMachine.Agent.ResetPath();
                stateMachine.Agent.velocity = Vector3.zero;
            }
        }

        private bool IsInAttackingRange()
        {
            if (stateMachine.LastAttacker == null)
            {
                return Vector3.SqrMagnitude(stateMachine.Player.transform.position - stateMachine.transform.position) <= attackingRange;
            }

            else return Vector3.SqrMagnitude(stateMachine.LastAttacker.transform.position - stateMachine.transform.position) <= attackingRange;
        }

        public void MoveToTarget(float deltaTime)
        {
            if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true; // turn on the NavMeshAgent, otherwise the enemy won't be able to chase you down...
            
            /* // stateMachine.Agent.destination = stateMachine.Player.transform.position;
            // TEST: Instead of just hunting the player down everytime this enemy is attacked, hunt down the LastAttacker (it can be the player or another NPC...). 
            // If you found none, and this function is called, hunt the player down:
            stateMachine.Agent.destination = stateMachine.LastAttacker != null ? stateMachine.LastAttacker.transform.position : stateMachine.Player.transform.position;

            // The following 4 lines of code are replaced by a Test below them:

            Vector3 desiredVelocity = stateMachine.Agent.desiredVelocity.normalized; // Normalized Desired Speed of the NPC
            Move(desiredVelocity * stateMachine.MovementSpeed, deltaTime); // Go to the player, at that desired speed
            stateMachine.Agent.velocity = stateMachine.CharacterController.velocity; // The velocity of the player
            stateMachine.Agent.nextPosition = stateMachine.transform.position; // The next position to aim for */

            // Entire rest of this function is a TEST:
            Vector3 destination = stateMachine.LastAttacker != null ? stateMachine.LastAttacker.transform.position : stateMachine.Player.transform.position;
            if (CanMoveTo(destination))
            {
                stateMachine.Agent.destination = destination;
                Vector3 desiredVelocity = stateMachine.Agent.desiredVelocity.normalized;
                Move(desiredVelocity * stateMachine.MovementSpeed, deltaTime);
                stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
                stateMachine.Agent.nextPosition = stateMachine.transform.position;
            }
            else 
            {
                Debug.Log($"{stateMachine.gameObject.name} cannot move to the target. Stopping");
                stateMachine.Agent.ResetPath();
                stateMachine.Agent.velocity = Vector3.zero;
            }
        }

        private bool HasLineOfSight()
        {
            Debug.Log($"HasLineOfSight has been called");
            Ray ray = new Ray(stateMachine.transform.position + Vector3.up, stateMachine.LastAttacker.transform.position - stateMachine.transform.position);
            if (Physics.Raycast(ray, out RaycastHit hit, Vector3.Distance(stateMachine.transform.position, stateMachine.LastAttacker.transform.position - stateMachine.transform.position), stateMachine.LayerMask))
            {
                return false;
            }
            return true;
        }

        private bool CanMoveTo(Vector3 destination) 
        {
            var path = new NavMeshPath();
            var hasPath = NavMesh.CalculatePath(stateMachine.transform.position, destination, NavMesh.AllAreas, path);
            if (!hasPath) {Debug.Log($"no path found"); return false;}
            if (path.status != NavMeshPathStatus.PathComplete) {Debug.Log($"Incomplete Path found, Can't take this path"); return false;}
            if (GetPathLength(path) > stateMachine.maxNavPathLength) {Debug.Log($"Path is too long"); return false;}
            return true;
        }

        private float GetPathLength(NavMeshPath path) 
        {
            float total = 0;
            if (path.corners.Length < 2) return total;

            for (int i = 0; i < path.corners.Length - 1; i++) 
            {
                total += Vector3.Distance(path.corners[i], path.corners[i + 1]);
            }
            return total;
        }

        private void FaceMovementDirection(Vector3 forward, float deltaTime) 
        {
            if (forward == Vector3.zero) return;
            Quaternion desiredRotation = Quaternion.LookRotation(forward, Vector3.up);
            stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, desiredRotation, stateMachine.FreeLookRotationSpeed * deltaTime);
        }
    }
}

but it’s not acting right for some reason…

Anyone knows as of why?

(I also reversed all the changes for pathfinding I found in Patrolling. Apparently they’re useless and only introduced unwanted bugs. But this… this one is notoriously hard to get it right, because sometimes the enemies will keep walking into you, and the debuggers won’t tell me why)

A little bit of an update. I did a few tweaks and cleaning up of my code, and got the pursuing around obstacles to work nearly as expected, BUT… My NPCs now come with the sliding animations. In other words, they don’t play the walk animation anymore when walking to me. I probably messed something up by accident… Anyone can kindly take a second look?:

// AS CLOSE AS I CAN GET TO MY NEEDS, BUT MY ANIMATION IS FROZEN FOR SOME REASON...:
using UnityEngine;
using RPG.Skills;
using UnityEngine.AI;

namespace RPG.States.Enemies
{
    public class EnemyChasingState : EnemyBaseState
    {
        public EnemyChasingState(EnemyStateMachine stateMachine) : base(stateMachine) { }

        private float attackingRange;
        private readonly int TargetingForwardSpeedHash = Animator.StringToHash("TargetingForwardSpeed");

        public override void Enter()
        {
            Debug.Log($"{stateMachine.gameObject.name} has entered chasing state");
            stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
            attackingRange = stateMachine.Fighter.GetAttackingRange();
            attackingRange *= attackingRange;
        }

        public override void Tick(float deltaTime)
        {
            if (!IsInChaseRange() && !IsAggrevated())
            {
                stateMachine.TriggerOnPlayerOutOfChaseRange();
                stateMachine.SwitchState(new EnemyIdleState(stateMachine));
                return;
            }

            if (IsInAttackingRange())
            {
                if (stateMachine.LastAttacker != null)
                {
                    if (stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Ranged || stateMachine.Fighter.GetCurrentWeaponConfig().GetSkill() == Skill.Magic)
                    {
                        Debug.Log($"{stateMachine.gameObject.name} is wielding a ranged weapon, and LastAttacker is {stateMachine.LastAttacker.gameObject.name}");
                        if (!HasLineOfSight())
                        {
                            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 1f, stateMachine.AnimatorDampTime, deltaTime);
                            Debug.Log($"{stateMachine.gameObject.name} has no line of sight of {stateMachine.LastAttacker.gameObject.name}");

                            if (CanMoveTo(stateMachine.LastAttacker.transform.position))
                            {
                                Debug.Log($"{stateMachine.gameObject.name} is attempting to move to {stateMachine.LastAttacker.gameObject.name}");
                                MoveToTarget(deltaTime);
                                return;
                            }
                        }
                    }
                }

                if (!stateMachine.CooldownTokenManager.HasCooldown("Attack"))
                {
                    Debug.Log($"{stateMachine.gameObject.name} is exiting chase state to attack state");
                    stateMachine.SwitchState(new EnemyAttackingState(stateMachine));
                    return;
                }
                else
                {
                    Move(deltaTime);
                    stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0);
                    if (stateMachine.LastAttacker == null)
                    {
                        stateMachine.SwitchState(new EnemyIdleState(stateMachine));
                    }
                    else
                    {
                        FaceTarget(stateMachine.LastAttacker.transform.position, deltaTime);
                    }
                    return;
                }
            }
            else
            {
                MoveToTarget(deltaTime);
            }

            UpdateMovementAnimation(deltaTime);
        }

        public override void Exit()
        {
            stateMachine.Agent.ResetPath();
            stateMachine.Agent.velocity = Vector3.zero;
        }

        private bool IsInAttackingRange()
        {
            if (stateMachine.LastAttacker == null)
            {
                return Vector3.SqrMagnitude(stateMachine.Player.transform.position - stateMachine.transform.position) <= attackingRange;
            }
            else
            {
                return Vector3.SqrMagnitude(stateMachine.LastAttacker.transform.position - stateMachine.transform.position) <= attackingRange;
            }
        }

        private void MoveToTarget(float deltaTime)
        {
            if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;

            Vector3 destination = stateMachine.LastAttacker != null ? stateMachine.LastAttacker.transform.position : stateMachine.Player.transform.position;

            stateMachine.Agent.stoppingDistance = 1.5f;

            if (CanMoveTo(destination))
            {
                stateMachine.Agent.SetDestination(destination);

                Vector3 desiredVelocity = stateMachine.Agent.desiredVelocity;
                if (desiredVelocity.sqrMagnitude > 0)
                {
                    stateMachine.Agent.velocity = desiredVelocity;
                    stateMachine.Agent.nextPosition = stateMachine.transform.position;
                    Move(desiredVelocity.normalized * stateMachine.MovementSpeed, deltaTime);
                    FaceMovementDirection(desiredVelocity, deltaTime);
                }
            }
            else
            {
                Debug.Log($"{stateMachine.gameObject.name} cannot move to {destination}, no valid path found.");
                stateMachine.SwitchState(new EnemyIdleState(stateMachine));
            }
        }

        private bool HasLineOfSight()
        {
            Debug.Log($"HasLineOfSight has been called");
            Ray ray = new Ray(stateMachine.transform.position + Vector3.up * 1.6f, stateMachine.LastAttacker.transform.position - stateMachine.transform.position);
            if (Physics.Raycast(ray, out RaycastHit hit, Vector3.Distance(stateMachine.transform.position, stateMachine.LastAttacker.transform.position), stateMachine.LayerMask))
            {
                return false;
            }
            return true;
        }

        private bool CanMoveTo(Vector3 destination)
        {
            var path = new NavMeshPath();
            var hasPath = NavMesh.CalculatePath(stateMachine.transform.position, destination, NavMesh.AllAreas, path);
            if (!hasPath) return false;
            if (path.status != NavMeshPathStatus.PathComplete) return false;
            if (GetPathLength(path) > stateMachine.maxNavPathLength) return false;
            return true;
        }

        private float GetPathLength(NavMeshPath path)
        {
            float total = 0;
            if (path.corners.Length < 2) return total;

            for (int i = 0; i < path.corners.Length - 1; i++)
            {
                total += Vector3.Distance(path.corners[i], path.corners[i + 1]);
            }
            return total;
        }

        private void FaceMovementDirection(Vector3 forward, float deltaTime)
        {
            if (forward == Vector3.zero) return;
            Quaternion desiredRotation = Quaternion.LookRotation(forward, Vector3.up);
            stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, desiredRotation, stateMachine.FreeLookRotationSpeed * deltaTime);
        }

        private void UpdateMovementAnimation(float deltaTime)
        {
            // Updating the Movement Animation, so it all acts out smoothly (ALL STUFF BELOW):
            Vector3 lastPosition = stateMachine.transform.position;
            Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
            float deltaMagnitude = deltaMovement.magnitude;

            if (deltaMagnitude > 0)
            {
                // if the game is not paused:
                FaceTarget(stateMachine.transform.position - deltaMovement, deltaTime);
                float grossSpeed = deltaMagnitude / deltaTime;
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, grossSpeed / stateMachine.MovementSpeed, stateMachine.AnimatorDampTime, deltaTime);
            }

            else
            {
                // if the game is paused:
                FaceTarget(stateMachine.Player.transform.position, deltaTime);
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
            }
        }
    }
}

(and by sliding animations, I mean the animation is probably set to NaN, because of a division by zero by accident, or it’s a straight zero, somewhere before setting it afloat)

Took a little bit of extra tweaking, but yeah it’s fixed and running now :slight_smile: (albeit sometimes the AI gets a little confused with unusual surface heights, but for the most part it’s fine). Most of the tweaking was done in ‘MoveToTarget’, my equivalent to ‘MoveToPlayer’, but because I got NPCs fighting each other now, I had to change names

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

Privacy & Terms