NavMeshAgent obstacle avoidance whilst Patrolling

Hi friends. I’m currently developing an algorithm to detect obstacles and trying to get my animals to move around them. So far, here’s my best attempt, and then I’ll mention what sort of issues I’m dealing with:

using RPG.Animals;
using RPG.Control;
using RPG.States.Animals;
using RPG.Statics;
using UnityEngine;
using UnityEngine.AI;

public class AnimalPatrolState : AnimalBaseState
{
    private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";

    public AnimalPatrolState(AnimalStateMachine stateMachine) : base(stateMachine) { }

    private float movementSpeed = 0.5f;
    private float acceptanceRadius = 3f;
    private float dwellTime = 2f;
    private Vector3 targetPatrolPoint;

    private float stuckTimer = 0f;
    private float maxStuckTime = 2f;
    private Vector3 lastPosition;

    private bool hasRecalculatedPath = false;

    public override void Enter()
    {
        if (stateMachine.PatrolPath == null)
        {
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
            return;
        }

        int index;
        if (stateMachine.Blackboard.ContainsKey(NextPatrolPointIndexKey))
        {
            index = stateMachine.Blackboard.GetValueAsInt(NextPatrolPointIndexKey);
        }
        else
        {
            index = stateMachine.PatrolPath.GetNearestIndex(stateMachine.transform.position);
        }

        targetPatrolPoint = stateMachine.PatrolPath.GetWaypoint(index);
        PatrolPoint patrolPoint = stateMachine.PatrolPath.GetPatrolPoint(index);

        if (patrolPoint)
        {
            movementSpeed = stateMachine.MovementSpeed * patrolPoint.SpeedModifier;
            acceptanceRadius = patrolPoint.AcceptanceRadius;
            dwellTime = patrolPoint.DwellTime;
        }
        else
        {
            movementSpeed = stateMachine.MovementSpeed;
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 1);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 0);
            ActivateNonMalbersLayerWeight(5, 1);
        }

        acceptanceRadius *= acceptanceRadius;
        stateMachine.Blackboard[NextPatrolPointIndexKey] = stateMachine.PatrolPath.GetNextIndex(index);
        stateMachine.Agent.SetDestination(targetPatrolPoint);
        stateMachine.Animator.CrossFadeInFixedTime(HorseFreeLookBlendTreeHash, stateMachine.CrossFadeDuration);

        AnimalMountManager.OnAnimalMounted += RemoveAnimalPatrolPath;
    }

    public override void Tick(float deltaTime)
    {
        if (IsInAcceptanceRange())
        {
            stateMachine.SwitchState(new AnimalDwellState(stateMachine, dwellTime));
            return;
        }

        // Check if there's any obstacle in the path of the NPC
        if (HasObstacleInPath() && !hasRecalculatedPath)
        {
            AttemptToFindNewPath(deltaTime);
            hasRecalculatedPath = true;
            Debug.Log($"NPC has an obstacle, attempting to find a new path");
            return; // stick to one finding attempt, otherwise the animals will keep searching for a long time (performance) and eventually get the wrong path
        }

        // Check if the NPC is stuck
        lastPosition = stateMachine.transform.position;

        if (Vector3.Distance(stateMachine.transform.position, lastPosition) < 0.1f)
        {
            stuckTimer += Time.deltaTime;
            if (stuckTimer > maxStuckTime && !hasRecalculatedPath)
            {
                AttemptToFindNewPath(deltaTime);
                stuckTimer = 0f;
                hasRecalculatedPath = true;
                Debug.Log($"NPC is stuck, finding a new path");
                return; // stick to one finding attempt, otherwise the animals will keep searching for a long time (performance) and eventually get the wrong path
            }
        }
        else
        {
            stuckTimer = 0f; // Reset the stuck timer if the NPC is moving
            hasRecalculatedPath = false;
        }

        MoveToWayPoint(deltaTime);
        Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
        float deltaMagnitude = deltaMovement.magnitude;

        Vector3 direction = stateMachine.Agent.desiredVelocity.normalized;

        if (deltaMagnitude > 0) 
        {
            FaceMovementDirection(direction, deltaTime);
            float grossSpeed = deltaMagnitude / deltaTime;
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0.7f, stateMachine.AnimatorDampTime, deltaTime);
        }
        else 
        {
            FaceMovementDirection(direction, deltaTime);
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0.7f);
        }
    }

    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;
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 0);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 1);
            ActivateNonMalbersLayerWeight(5, 0);
        }

        AnimalMountManager.OnAnimalMounted -= RemoveAnimalPatrolPath;
    }

    private bool IsInAcceptanceRange()
    {
        return (stateMachine.transform.position - targetPatrolPoint).sqrMagnitude < acceptanceRadius;
    }

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

        stateMachine.Agent.updatePosition = false;
        stateMachine.Agent.updateRotation = false;

        Vector3 direction = stateMachine.Agent.desiredVelocity;
        direction.y = 0;
        direction.Normalize();

        Move(direction * movementSpeed, deltaTime);
        stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
        stateMachine.Agent.nextPosition = stateMachine.transform.position;

        stateMachine.Agent.updatePosition = true;
        stateMachine.Agent.updateRotation = true;

        stateMachine.Agent.destination = targetPatrolPoint;
    }

    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 RemoveAnimalPatrolPath()
    {
        stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
        stateMachine.AssignPatrolPath(null);
        stateMachine.SwitchState(new AnimalIdleState(stateMachine));
    }

    private bool HasObstacleInPath()
    {
        NavMeshHit hit;
        if (NavMesh.Raycast(stateMachine.transform.position, targetPatrolPoint, out hit, NavMesh.AllAreas))
        {
            return true;
        }
        return false;
    }

    private void AttemptToFindNewPath(float deltaTime)
    {
        // stateMachine.Agent.ResetPath();
        Debug.Log($"Agent path reset");

        // Attempt to recalculate the path to the current target patrol point
        NavMeshPath path = new NavMeshPath();
        stateMachine.Agent.CalculatePath(targetPatrolPoint, path);
        Debug.Log($"Calculated path to target patrol point. Path status: {path.status}");

        if (path.status != NavMeshPathStatus.PathComplete)
        {
            Debug.Log("Path to target patrol point is incomplete. Attempting to find a random nearby position");

            // If the path is not complete, find a random nearby position
            Vector3 randomDirection = Random.insideUnitSphere * 5f;
            randomDirection += stateMachine.transform.position;
            Debug.Log($"Random direction for new position: {randomDirection}");

            NavMeshHit hit;
            if (NavMesh.SamplePosition(randomDirection, out hit, 5f, NavMesh.AllAreas))
            {
                Debug.Log($"Found a valid nearby position: {hit.position}, setting it as new destination");
                stateMachine.Agent.SetDestination(hit.position);
            }
            else
            {
                Debug.LogWarning("Failed to find a new valid position for the NPC.");
            }
        }
        else 
        {
            MoveToWayPoint(deltaTime);
        }
    }
}

1: In ‘Tick()’, The ‘stuck’ checking algorithm (the one that compares the ‘stuckTimer’ with the ‘maxStuckTime’), which is supposed to get the animal to be unstuck from any sort of obstacles that he might be stuck in, does not work… like, at all. The animal is incapable of finding it’s way out of any sticky positions it finds itself in, and that’s a huge problem for me

  1. It takes corners a little too sharp, essentially acting like a drift car that’s trying to be as close to the wall as possible. I want the animal to have a little bit of breathing room, so it acts more naturally when moving around obstacles

  2. How can I clean this algorithm up a little bit to make it neater?

@Brian_Trotter @bixarrio just letting you both know that this is here :slight_smile:

If you have any questions about anything, please ask

Just a heads-up, that ‘AttemptToFindNewPath’ is… USELESS. It absolutely fails to find a new path around the NavMesh (OR OBSTACLES!) that can be walked over by the NPC. If he’s stuck, there’s no way out the NavMeshAgent for him, and this is troubling me

This is my biggest problem right now. If there’s a way to get NPCs out of navigation problems, that would honestly be amazing!

oh, and this thing too:


Edit: I asked around, and was told to use Reciprocal Velocity Obstacles (RVO) and A* to help with the stuck issues… Again, I’d rather wait for Brian or bixarrio on this one first


I’ll leave my current code below as I go test other stuff:

using RPG.Animals;
using RPG.Control;
using RPG.States.Animals;
using RPG.Statics;
using UnityEngine;
using UnityEngine.AI;

public class AnimalPatrolState : AnimalBaseState
{
    private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";

    public AnimalPatrolState(AnimalStateMachine stateMachine) : base(stateMachine) {}

    private float movementSpeed = 0.5f;
    private float acceptanceRadius = 3f;
    private float dwellTime = 2f;
    private Vector3 targetPatrolPoint;

    private float stuckTimer = 0f;
    private float maxStuckTime = 2f;
    private Vector3 lastPosition;

    private bool hasRecalculatedPath = false;

    public override void Enter()
    {
        if (stateMachine.PatrolPath == null)
        {
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
            return;
        }

        int index;
        if (stateMachine.Blackboard.ContainsKey(NextPatrolPointIndexKey))
        {
            index = stateMachine.Blackboard.GetValueAsInt(NextPatrolPointIndexKey);
        }
        else
        {
            index = stateMachine.PatrolPath.GetNearestIndex(stateMachine.transform.position);
        }

        targetPatrolPoint = stateMachine.PatrolPath.GetWaypoint(index);
        PatrolPoint patrolPoint = stateMachine.PatrolPath.GetPatrolPoint(index);

        if (patrolPoint)
        {
            movementSpeed = stateMachine.MovementSpeed * patrolPoint.SpeedModifier;
            acceptanceRadius = patrolPoint.AcceptanceRadius;
            dwellTime = patrolPoint.DwellTime;
        }
        else
        {
            movementSpeed = stateMachine.MovementSpeed;
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 1);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 0);
            ActivateNonMalbersLayerWeight(5, 1);
        }

        acceptanceRadius *= acceptanceRadius;
        stateMachine.Blackboard[NextPatrolPointIndexKey] = stateMachine.PatrolPath.GetNextIndex(index);
        stateMachine.Agent.SetDestination(targetPatrolPoint);
        stateMachine.Animator.CrossFadeInFixedTime(HorseFreeLookBlendTreeHash, stateMachine.CrossFadeDuration);

        AnimalMountManager.OnAnimalMounted += RemoveAnimalPatrolPath;
    }

    public override void Tick(float deltaTime)
    {
        if (IsInAcceptanceRange())
        {
            stateMachine.SwitchState(new AnimalDwellState(stateMachine, dwellTime));
            return;
        }

        // Check if the NPC is stuck (by checking the distance between the position of the last frame, and the current position)
        float distanceMoved = Vector3.Distance(stateMachine.transform.position, lastPosition);

        // Check if there's any obstacle in the path of the NPC
        if (HasObstacleInPath() && !hasRecalculatedPath)
        {
            AttemptToFindNewPath(deltaTime);
            hasRecalculatedPath = true;
            Debug.Log($"NPC has an obstacle, attempting to find a new path");
            return; // stick to one finding attempt, otherwise the animals will keep searching for a long time (performance) and eventually get the wrong path
        }

        if (distanceMoved < 0.1f)
        {
            stuckTimer += deltaTime;
            Debug.Log($"Stuck Timer: {stuckTimer}");
            Debug.Log($"HasRecalculatedPath: {hasRecalculatedPath}");
            if (stuckTimer > maxStuckTime)
            {
                AttemptToFindNewPath(deltaTime);
                stuckTimer = 0f;
                hasRecalculatedPath = true;
                Debug.Log($"NPC is stuck, finding a new path");
                return; // stick to one finding attempt, otherwise the animals will keep searching for a long time (performance) and eventually get the wrong path
            }
        }
        else
        {
            stuckTimer = 0f; // Reset the stuck timer if the NPC is moving
            hasRecalculatedPath = false;
        }

        lastPosition = stateMachine.transform.position;
        MoveToWayPoint(deltaTime);
        Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
        float deltaMagnitude = deltaMovement.magnitude;

        Vector3 direction = stateMachine.Agent.desiredVelocity.normalized;

        if (deltaMagnitude > 0) 
        {
            FaceMovementDirection(direction, deltaTime);
            float grossSpeed = deltaMagnitude / deltaTime;
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0.7f, stateMachine.AnimatorDampTime, deltaTime);
        }
        else 
        {
            FaceMovementDirection(direction, deltaTime);
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0.7f);
        }
    }

    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;
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 0);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 1);
            ActivateNonMalbersLayerWeight(5, 0);
        }

        AnimalMountManager.OnAnimalMounted -= RemoveAnimalPatrolPath;
    }

    private bool IsInAcceptanceRange()
    {
        return (stateMachine.transform.position - targetPatrolPoint).sqrMagnitude < acceptanceRadius;
    }

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

        stateMachine.Agent.updatePosition = false;
        stateMachine.Agent.updateRotation = false;

        Vector3 direction = stateMachine.Agent.desiredVelocity;
        direction.y = 0;
        direction.Normalize();

        Move(direction * movementSpeed, deltaTime);
        stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
        stateMachine.Agent.nextPosition = stateMachine.transform.position;

        stateMachine.Agent.updatePosition = true;
        stateMachine.Agent.updateRotation = true;

        stateMachine.Agent.destination = targetPatrolPoint;
    }

    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 RemoveAnimalPatrolPath()
    {
        stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
        stateMachine.AssignPatrolPath(null);
        stateMachine.SwitchState(new AnimalIdleState(stateMachine));
    }

    private bool HasObstacleInPath()
    {
        NavMeshHit hit;
        if (NavMesh.Raycast(stateMachine.transform.position, targetPatrolPoint, out hit, NavMesh.AllAreas))
        {
            return true;
        }
        return false;
    }

    private void AttemptToFindNewPath(float deltaTime)
    {
        // Attempt to recalculate the path to the current target patrol point
        NavMeshPath path = new NavMeshPath();
        stateMachine.Agent.CalculatePath(targetPatrolPoint, path);
        Debug.Log($"Calculated path to target patrol point. Path status: {path.status}");

        if (path.status != NavMeshPathStatus.PathComplete)
        {
            Debug.Log("Path to target patrol point is incomplete. Attempting to find a random nearby position");

            // If the path is not complete, find a random nearby position
            Vector3 randomDirection = Random.insideUnitSphere * 5f;
            randomDirection += stateMachine.transform.position;
            Debug.Log($"Random direction for new position: {randomDirection}");

            NavMeshHit hit;
            if (NavMesh.SamplePosition(randomDirection, out hit, 5f, NavMesh.AllAreas))
            {
                Debug.Log($"Found a valid nearby position: {hit.position}, setting it as new destination");
                stateMachine.Agent.SetDestination(hit.position);
            }
            else
            {
                Debug.LogWarning("Failed to find a new valid position for the NPC.");
            }
        }
        else 
        {
            MoveToWayPoint(deltaTime);
        }
    }
}

The NavMesh should already be performing obstacle avoidance.

Your HasObstacleInPath() routine returns true if there is something between the character and the target, but the entire point of the NavMesh system is to enable characters to walk around those obstacles. For example: If there is a wall between you and me, but you can go through the doorway, then the agent will try to go through the doorway.

This is likely a problem with the turn speed/movement speed of the character… if the turn speed is too low, then using our CharacterController solution, it’s possible to walk right off the NavMesh. Once it’s walked or been knocked off the NavMesh, the Agent can’t find a solution to any location. (You’ll usually see an error because of this that the Agent is not on a NavMesh). One reason I actually recommend putting hard colliders everywhere things arent’ supposed to go… Helps prevent this very sort of thing.

Put your animals on their own NavMeshSurface (you can have multiple surfaces, one for each Agent Type). Give them their own agent type. In the Agent Type, increase the radius for that Agent Type.

By letting the NavMeshAgent do it’s job…

One big issue with the Third Person system in general is that while a character with proper turning speed (fast is best) using the CharacterController and ForceReceiver for movement should stay on the NavMesh, if the character is knocked off the NavMesh by the ForceReceiver, we get problems. I presented a “solution” to this in my original tutorial, but even that can fail. Ultimately, colliders and good blocking solutions (buildings, for example) are the prevantitive for this phoenomenon.

Won’t this be heavy performance-wise on the long run?

I tried baking that too (the horse with the NavMesh Surface), and my project crashed. Not bad :slight_smile:

And with this simple crash comes a ton of questions. If I’m building a variety of biomes, what will happen to performance then? As in, how do you take care of multiple NavMesh Surfaces in the scene?

Other than that, increasing the radius of the horse’s new Agent does not seem to affect the fact that it takes corners too sharply, and I’m certain it’s because I didn’t bake it, which causes a crash… You get the idea now

should’ve seen what it does all on it’s own without a check and a re-path, followed by a return. The horse will occasionally go wild with the directions and get extremely undecisive for some reason. I know my solution isn’t the best one, but it’s better than the original tbh

no like… deleting or keeping the function makes zero difference to how the code works, literally

Each Agent Type needs it’s own baked surface… without it, it defaults to the only surface it can find, which will take the corners too tight for a horse…

No, because the Graphics Card is doing the hard physics collision detection. NavMesh is done strictly on the CPU… NVidia PhysX is FAST compared to anything done on the CPU. The reason Graphics cards are so bleeping expensive these days (and the reason why NVidia is now the richest company in the bleeping world) is because Video cards are screaming fast at computations, especially physics computations and other math (like Blockchain, which is why Bitcoin miners make computers with multiple high end video cards on em).

How big is your scene?

I suspect that the Malbers Horse system is interfering with your system… When you get too many things competing for movement (NavMeshAgent, CharacterController, Rigidbody, ForceReceiver, Whatever Malbers is using), things can get a bit weird…

Trust me, it’s a mid-sized GAIA Terrain scene (if you’re going to take a wild guess that I’ll be mixing GAIA 2021 Pro’s World Streaming (or SECTR 2019, whichever yields better results) with GPU Instancer to make manageable open worlds, you’d be right). I didn’t even start building the game world yet (and I just figured out I’m stuck with a middle-sized terrain (because I’m not willing to break any other systems just yet). Welp, time to make it look good I guess)

Moving forward, I’ll be building worlds that are somewhat levitated from the water otherwise I have to re-tune the Raven to stop him from falling into the water and… boy oh boy will this be a headache!

I also tried changing the game world a little and bringing it closer to the floor, and let’s just say this broke a few systems… Moving forward, I need the game world to be at least 3 units above the water so the bird can properly act

Time to go recover my backups

There’s a reason why I didn’t turn this into a massive game yet (i.e: testing and the constant modifications!)

Considering that this broke my project (today is one of these days where you just have to tough it out, and it’s always something to do with either the NavMesh or the Saving System for me!), I am PRETTY SURE we can come up with something else… talk about working on a GTX1650 laptop

sigh I’ll reverse to my original patrol algorithm and see what else can be done… I hate the NavMesh (with passion) for a reason :sweat_smile:

Just to keep track of things, I threw in a NavMeshObstacle on all the creatures in my scene, because I want them to intelligently avoid each other

AND… I have also reversed my code to the original variant, as follows:

using RPG.Animals;
using RPG.Control;
using RPG.States.Animals;
using RPG.Statics;
using UnityEngine;

public class AnimalPatrolState : AnimalBaseState
{
    private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";

    public AnimalPatrolState(AnimalStateMachine stateMachine) : base(stateMachine) { }

    private float movementSpeed = 0.5f;
    private float acceptanceRadius = 3f;
    private float dwellTime = 2f;
    private Vector3 targetPatrolPoint;

    public override void Enter()
    {
        if (stateMachine.PatrolPath == null)
        {
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
            return;
        }

        int index; // Index of the Patrol Point

        // Check the blackboard for the key, and set the index if the key is setup
        if (stateMachine.Blackboard.ContainsKey(NextPatrolPointIndexKey))
        {
            index = stateMachine.Blackboard.GetValueAsInt(NextPatrolPointIndexKey);
        }
        else
        {
            // If we are coming from chasing or somewhere without prior patrolling,
            // we will need to set it up for the first time, and that's what we are
            // doing here
            index = stateMachine.PatrolPath.GetNearestIndex(stateMachine.transform.position);
        }

        // Set our goal
        targetPatrolPoint = stateMachine.PatrolPath.GetWaypoint(index);
        PatrolPoint patrolPoint = stateMachine.PatrolPath.GetPatrolPoint(index);

        if (patrolPoint) // if you have a patrol point
        {
            movementSpeed = stateMachine.MovementSpeed * patrolPoint.SpeedModifier;
            acceptanceRadius = patrolPoint.AcceptanceRadius;
            dwellTime = patrolPoint.DwellTime;
        }
        else // if no patrol point, calculate the movement speed to be the state machine's movement speed
        {
            movementSpeed = stateMachine.MovementSpeed;
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            // in 'AnimalBaseState.cs'
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 1);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            // in 'AnimalBaseState.cs' (Will be implemented in the future)
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 0);
            ActivateNonMalbersLayerWeight(5, 1);
        }

        // Squaring the acceptance radius, to save on calculation time
        acceptanceRadius *= acceptanceRadius;
        // next waypoint index setup
        stateMachine.Blackboard[NextPatrolPointIndexKey] = stateMachine.PatrolPath.GetNextIndex(index);
        // Set the destination for the agent (since waypoints don't move)
        stateMachine.Agent.SetDestination(targetPatrolPoint);
        // Set the animation
        stateMachine.Animator.CrossFadeInFixedTime(HorseFreeLookBlendTreeHash, stateMachine.CrossFadeDuration);

        // When the animal is mounted, remove its patrol path so the player
        // can take control over it, and switch to idle state, where an idle animation
        // will be played for a moment before regular riding systems take over again
        AnimalMountManager.OnAnimalMounted += RemoveAnimalPatrolPath;
    }

    public override void Tick(float deltaTime)
    {
        // if you should pursue or you're aggrevated, remove the next point key index,
        // and switch to chasing state. We'll take care of that later

        if (IsInAcceptanceRange())
        {
            stateMachine.SwitchState(new AnimalDwellState(stateMachine, dwellTime));
            return;
        }

        // Patrolling code
        Vector3 lastPosition = stateMachine.transform.position;
        MoveToWayPoint(deltaTime);
        Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
        float deltaMagnitude = deltaMovement.magnitude;

        Vector3 direction = stateMachine.Agent.desiredVelocity.normalized; // TEST - DELETE IF FAILED

        if (deltaMagnitude > 0)
        {
            // When the game is not paused
            // FaceTarget(targetPatrolPoint, deltaTime);
            FaceMovementDirection(direction, deltaTime); // TEST - DELETE IF FAILED
            float grossSpeed = deltaMagnitude / deltaTime;
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0.7f /* grossSpeed / stateMachine.MovementSpeed */, stateMachine.AnimatorDampTime, deltaTime); // blend tree plays the horse walking animation
        }
        else
        {
            // When the game is paused
            // FaceTarget(targetPatrolPoint, deltaTime);
            FaceMovementDirection(direction, deltaTime); // TEST - DELETE IF FAILED
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0.7f);
        }
    }

    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;
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            // in 'AnimalBaseState.cs'
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 0);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            // in 'AnimalBaseState.cs' (Will be implemented in the future)
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 1);
            ActivateNonMalbersLayerWeight(5, 0);
        }

        // When the animal is mounted, remove its patrol path so the player
        // can take control over it, and switch to idle state, where an idle animation
        // will be played for a moment before regular systems take over again
        AnimalMountManager.OnAnimalMounted -= RemoveAnimalPatrolPath;
    }

    private bool IsInAcceptanceRange()
    {
        return (stateMachine.transform.position - targetPatrolPoint).sqrMagnitude < acceptanceRadius;
    }

    private void MoveToWayPoint(float deltaTime)
    {
        // turn on the NavMesh if it's not turned on
        if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;

        // TEST - DELETE IF FAILED (AVOIDS JITTERING WHILST PATROLLING OF THE HORSE)
        stateMachine.Agent.updatePosition = false;
        stateMachine.Agent.updateRotation = false;

        Vector3 direction = stateMachine.Agent.desiredVelocity/*.normalized*/;

        // TEST - DELETE IF FAILED (AVOIDS JITTERING WHILST PATROLLING OF THE HORSE)
        direction.y = 0;
        direction.Normalize();

        Move(direction * movementSpeed, deltaTime);
        stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
        stateMachine.Agent.nextPosition = stateMachine.transform.position;

        // get the destination, or you will struggle to find anywhere to go
        stateMachine.Agent.destination = targetPatrolPoint;
        return;
    }

    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 RemoveAnimalPatrolPath()
    {
        stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
        stateMachine.AssignPatrolPath(null);
        stateMachine.SwitchState(new AnimalIdleState(stateMachine));
    }
}

After the huge crash of my project because of the NavMeshSurface, I went and tuned my Terrain’s NavMesh, and here’s what worked for me:

He doesn’t necessarily take the shortest route, but he doesn’t get stuck either, so… considering how annoying this system was, I call this a win for now!

Time to go try do the same for the Raven and see what happens before moving on to other stuff (and to fix a few bugs I noticed around)

I want to know more about this thing too. What steps exactly should I take to make this happen?! (I can’t tell if baking was the right decision or not, I just know it almost broke my project)

Bahaa, all of Brian’s instructions above assume you are using the new navmesh. If you have a separate panel for adjusting settings and baking then you are still using the old system. Or maybe both. Your way forward would be a lot easier if you would take the time to learn the new system. It really is better.

I’m using the new system. I dumped the old system a while ago :slight_smile:

Just that I’ll not be creating massive islands that way again for this project, this is incredibly hard to replace. I got better ideas for the next upcoming biomes

Your screen shot above is for the old system. You should not be using that window at all. The adjustments and baking are in the regular inspector for the game object that has the navmesh surface on it. If you have an old system navmesh, you need to remove it and bake a new one. Then you can have multiple new system navmesh surfaces.

The screenshot above was me displaying the values that worked for me (I don’t know where else to modify the agents from tbh), but I think I’m slowly understanding what in the world is going on here… I tried adding a second NavMeshSurface to the terrain, and it doesn’t seem to be crashing. Now I just need to modify the agents a little. Pardon me guys, but this whole multi-surface thing is new to me and driving me a little crazy

I genuinely appreciate all the help I’m getting here, though. I wouldn’t know what to do otherwise

When you add a navmesh surface to a game object, it is like any other component. The settings are in the inspector for that object. If you are changing the settings in the Navigation window that is in your screen shot, then you are still using the old system. Using both will cause weird navmesh issue. There is even a warning at the bottom of your screen shot.

Well… Now I have a bit of a funny problem. Let’s get into the details:

  1. When the animal has it’s NavMeshAgent changed to the new agent, I called it ‘Horses’ for now, as follows (labelled in blue):

I get the error labelled in red

(For reference, changing the Horse’s NavMeshAgent type to ‘Humanoid’ again eliminates the problem

And for reference again, here’s what my agent (in the ‘old tab’) looks like:

And this is the function, with line 201 commented ‘line 201’ beside it:

    private void MoveToWayPoint(float deltaTime)
    {
        // turn on the NavMesh if it's not turned on
        if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;

        // TEST - DELETE IF FAILED (AVOIDS JITTERING WHILST PATROLLING OF THE HORSE)
        stateMachine.Agent.updatePosition = false;
        stateMachine.Agent.updateRotation = false;

        Vector3 direction = stateMachine.Agent.desiredVelocity;

        // TEST - DELETE IF FAILED (AVOIDS JITTERING WHILST PATROLLING OF THE HORSE)
        direction.y = 0;
        direction.Normalize();

        Move(direction * movementSpeed, deltaTime);
        stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
        stateMachine.Agent.nextPosition = stateMachine.transform.position;


        // get the destination, or you will struggle to find anywhere to go
        stateMachine.Agent.destination = targetPatrolPoint; // Line 201, the trouble-some line
        return;
    }

And I also made sure the agent was enabled in ‘Enter()’

Edit: Changing the ‘Include Layers’ for the ‘Horses’ on the ‘NavMeshSurface’ to ‘Everything’ seems to solve the problem, albeit I still want Brian’s opinion on this

Edit 2: Nuh-uh… it performs terribly on other forms of harsh surface :sweat_smile:

Side question, though. How do we handle placing NavMeshSurfaces if we are working with a non-terrain environment? Maybe a big city or something that relies on floor tiles as prefabs acting as the holder instead of a terrain?

Navmesh surfaces can be added to any game object. They are not tied to the terrain anymore.

so I have to do that to the tens or hundreds of floor objects I’m planning to place around, and can expect no problems?

You only need one. Multiple surfaces are only for when you need multiple agent types.

One surface, multiple floors… right?!

I have not built an interior level with the new navmesh system, but I have built outdoor levels with multiple heights and areas that are not connected and it built a good navmesh with one surface on the environment empty game object.

I’m not talking about interior levels. I’m talking about prefab-based islands for example. So let’s assume you got no terrain, but your level is made of blocks of some ceramic tiles or something, idk… I couldn’t think of a better example. We need a NavMeshSurface on each tile for that scenario?

I do not think so. Build a test level and see. The best way to learn is to do.

Privacy & Terms