Off-the-NavMesh Patrol Paths (Part 2)

Hey @Brian_Trotter @bixarrio and other friends

Currently I’m working on an “Off-the-NavMesh” AI Controller, something to essentially allow NPCs, like patrolling boats for my particular example, or maybe flying planes even, to essentially move off the NavMesh in patrolling states. Problem is, I’m facing a lot of challenges with the code as our code relies heavily on NavMeshAgents, so I seek some help here today. Here’s my current ‘BoatPatrollingState.cs’ script, which I’m currently trying to develop:

using System.Collections;
using System.Collections.Generic;
using RPG.Control;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

public class BoatPatrolState : BoatBaseState
{
    private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";

    public BoatPatrolState(BoatStateMachine 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.GetBoatLastAttacker() != null && !stateMachine.CooldownTokenManager.HasCooldown("BoatAggro")) 
        {
            stateMachine.ClearLastBoatAttacker();
        }

        if (stateMachine.PatrolPath == null) 
        {
            stateMachine.SwitchState(new BoatIdleState(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;
        }

        acceptanceRadius *= acceptanceRadius;
        stateMachine.Blackboard[NextPatrolPointIndexKey] = stateMachine.PatrolPath.GetNextIndex(index);
    }

    public override void Tick(float deltaTime)
    {
        if (IsAggrevated()) 
        {
            stateMachine.Blackboard.Remove(NextPatrolPointIndexKey);
            stateMachine.SwitchState(new BoatChasingState(stateMachine));
            return;
        }

        if (IsInAcceptanceRange()) 
        {
            stateMachine.SwitchState(new BoatDwellState(stateMachine));
            return;
        }

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

        // Find a way to get the direction without using the NavMeshAgent

        if (deltaMagnitude > 0) 
        {
            // Find a way to face the movement direction, without using the direction that relies on the NavMeshAgent's desired velocity
        }
        else 
        {
            // Find a way to face the movement direction, without using the direction that relies on the NavMeshAgent's desired velocity
        }
    }

    public override void Exit()
    {
        // Without a NavMeshAgent, there's not much to write here
    }

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

    private void MoveToWayPoint(float deltaTime)
    {
        // You'll need to find a way here to get the direction, without using the NavMeshAgent
    }

    // Face Movement Direction here, but without the NavMeshAgent

    // Work on a Boat Dismounting State in here, something to invoke in 'PlayerBoatDrivingState.Exit()'
}

As you can see, it’s missing a few things here and there. Any idea what modifications will need to be done to get patrol paths that are independent of the NavMeshAgent to work? Thanks in advance :slight_smile:

Also does it make sense to have a Character Controller on a ship?

I got the Patrolling to work, but the boat is fighting some unknown force on the y-axis. Here’s where I suspect things may go wrong:

  1. I turned off the character controller’s collision detection to try combat this in ‘Start()’:
    private void Start()
    {
        Health.onDie.AddListener(() => 
        {
            SwitchState(new BoatDeathState(this));
        });

        Health.OnTakenHit += OnTakenHit;

        SwitchState(new BoatIdleState(this));

        Blackboard["Level"] = BaseStats.GetLevel();

        PlayerBoatChasingRangedSquared = PlayerBoatChasingRange * PlayerBoatChasingRange;

        ForceReceiver.OnForceApplied += HandleForceApplied;

        CharacterController.detectCollisions = false;
    }
  1. The water asset itself comes with a buoy controller for floating objects. I tried eliminating that, but that wasn’t the source of the issue either. All it does is adjust the y-value based on the water height:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Pinwheel.Poseidon;

[ExecuteInEditMode]
public class SimpleBuoyController : MonoBehaviour
{
    public PWater water;
    public bool applyRipple;

    public void Update()
    {
        if (water == null)
            return;
        Vector3 localPos = water.transform.InverseTransformPoint(transform.position);
        localPos.y = 0;
        localPos = water.GetLocalVertexPosition(localPos, applyRipple);

        Vector3 worldPos = water.transform.TransformPoint(localPos);
        transform.position = worldPos;
    }
}
  1. In ‘BoatBaseState.cs’, I eliminated any y-axis sort of movement:
    protected void Move(float deltaTime)
    {
        Move(Vector3.zero, deltaTime);
    }

    protected void Move(Vector3 direction, float deltaTime)
    {
        Vector3 intendedMovement = (direction + stateMachine.ForceReceiver.Movement) * deltaTime;

        stateMachine.CharacterController.Move(new Vector3(intendedMovement.x, 0, intendedMovement.z));
    }
  1. Here comes the screenshots of my edits, maybe it’s in there:



Anything suspiciously wrong that makes the boat fight the y-axis? When it gets to dwelling, the boat is half sunk, and I have absolutely no idea why

Here’s also the patrol and dwell states so far:

using RPG.Control;
using UnityEngine;

public class BoatPatrolState : BoatBaseState
{
    private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";

    public BoatPatrolState(BoatStateMachine stateMachine) : base(stateMachine) {}

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

    public override void Enter()
    {
        int boatLayer = LayerMask.NameToLayer("Boat");
        int swimLimiterLayer = LayerMask.NameToLayer("SwimLimiter");
        Physics.IgnoreLayerCollision(boatLayer, swimLimiterLayer, true);

        if (stateMachine.GetBoatLastAttacker() != null && !stateMachine.CooldownTokenManager.HasCooldown("BoatAggro")) 
        {
            stateMachine.ClearLastBoatAttacker();
        }

        if (stateMachine.PatrolPath == null) 
        {
            stateMachine.SwitchState(new BoatIdleState(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;
        }

        acceptanceRadius *= acceptanceRadius;
        stateMachine.Blackboard[NextPatrolPointIndexKey] = stateMachine.PatrolPath.GetNextIndex(index);
    }

    public override void Tick(float deltaTime)
    {
        if (IsAggrevated())
        {
            stateMachine.Blackboard.Remove(NextPatrolPointIndexKey);
            stateMachine.SwitchState(new BoatChasingState(stateMachine));
            return;
        }

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

        MoveToWayPoint(deltaTime);
        FaceTarget(targetPatrolPoint, deltaTime);
    }

    public override void Exit()
    {
        int boatLayer = LayerMask.NameToLayer("Boat");
        int swimLimiterLayer = LayerMask.NameToLayer("SwimLimiter");
        Physics.IgnoreLayerCollision(boatLayer, swimLimiterLayer, false);
    }

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

    private void MoveToWayPoint(float deltaTime)
    {
        Vector3 direction = (targetPatrolPoint - stateMachine.transform.position).normalized;
        Vector3 velocity = direction * movementSpeed;

        Move(velocity, deltaTime);
    }

    private void FaceTarget(Vector3 target, float deltaTime)
    {
        Vector3 direction = (target - stateMachine.transform.position).normalized;

        if (direction != Vector3.zero)
        {
            Quaternion targetRotation = Quaternion.LookRotation(direction);
            stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, targetRotation, deltaTime * stateMachine.RotationSpeed);
        }
    }

    // Work on a Boat Dismounting State in here, something to invoke in 'PlayerBoatDrivingState.Exit()'
}
using UnityEngine;

public class BoatDwellState : BoatBaseState
{
    private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";
    private float dwellTime;

    public BoatDwellState(BoatStateMachine stateMachine, float dwellTime) : base(stateMachine)
    {
        this.dwellTime = dwellTime;
    }

    public override void Enter()
    {
        int boatLayer = LayerMask.NameToLayer("Boat");
        int swimLimiterLayer = LayerMask.NameToLayer("SwimLimiter");
        Physics.IgnoreLayerCollision(boatLayer, swimLimiterLayer, true);
    }

    public override void Tick(float deltaTime)
    {
        Move(deltaTime);

        if (IsAggrevated())
        {
            stateMachine.Blackboard.Remove(NextPatrolPointIndexKey);
            stateMachine.SwitchState(new BoatChasingState(stateMachine));
            return;
        }

        dwellTime -= deltaTime;
        if (dwellTime <= 0)
        {
            stateMachine.SwitchState(new BoatIdleState(stateMachine));
        }
    }

    public override void Exit()
    {
        int boatLayer = LayerMask.NameToLayer("Boat");
        int swimLimiterLayer = LayerMask.NameToLayer("SwimLimiter");
        Physics.IgnoreLayerCollision(boatLayer, swimLimiterLayer, false);
    }
}

The Swim Limiter Layer was something I created to avoid the player from reviving his way to space, ignored by pretty much anything else except the player (and now that I think of it, the enemies will have the same problem too)

  1. I also tried removing the Force Receiver, but to no avail

Any other ideas? Essentially, when the boat goes into dwelling state, it literally goes half-sunk (and I’m guessing it’s something to do with the patrolling, because it does that just at the end of patrolling, before dwelling, if I’m not mistaken), before it acts like nothing happened when we get back to patrolling


Edit: The acceptance radius of each waypoint had to be cranked up extremely high, to 8 units. I think it has something to do with the fact that I’m testing patrolling on a SHIP, hence why it was sinking (it was trying to place it’s position on the spot of the patrol point, by sinking…

Should’ve seen the dance it did at 0 units. THE FABULOUS SNOW SHIP!)

Ordinarily, this would likely be an animation problem… if it was a human, for example, the animation has the wrong avatar or something… but… I see you’re playing with the Physics layer… I would start there… Make sure that what you’re dwelling on didn’t suddenly get ignored by the IgnoreLayerCollision

There’s an invisible horizontal wall barely above the water. The reason it’s there, is because when the player revives himself from underwater to the surface to catch his breath, that wall should exist to make sure he doesn’t take himself to space (i.e: it’s a hard limit to avoid bad bugs). Anything else that either has a different swimming functionality, or doesn’t swim, must completely ignore that layer

I’ll get back to the ship problem a little later today. Yesterday I was fixing a few bugs with other systems since the animals got introduced, and I’m currently dealing with a bit of a weird bug (where my arrows always aim at the feet of the Raven, although it’s not the heart of the character controller)

That one was a weird fix, but it had to do with ‘Projectile.GetAimLocation()’. For animals, I forced it to aim for the capsule collider at the heart (spine) of the animals

Privacy & Terms