Off-The-NavMesh Patrol Paths

Hello @Brian_Trotter @bixarrio and everyone else

Currently I’m trying something a little different. I’m trying to develop patrol paths for a Raven so it can fly around the map, something to make the game feel more organic and natural. Whilst it’s not mandatory, I personally believe it would be an awesome little addon to the game, but there’s a problem: For Flying Patrol States, as far as I know, you need to turn off the NavMesh Agent and rely on manual controls, and that’s what I’m trying to do

HOWEVER, It’s not going so well. If anyone has anything they can add to help me make this work, please let me know. So far, I added a boolean called ‘CanFly’, which allows flying animals to take a different patrol state than what non-flying animals would take, and that leads to this new (not-so-working) code:

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

public class AnimalFlyingPatrolState : AnimalBaseState
{
    private static readonly int RavenFlyFreeLookBlendTreeHash = Animator.StringToHash("RavenFlyFreeLookBlendTree");
    private static readonly int flyHorizontal = Animator.StringToHash("FlyHorizontal");
    private static readonly int flyVertical = Animator.StringToHash("FlyVertical");

    private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";

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

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

    public override void Enter()
    {
        Debug.Log($"{stateMachine.gameObject.name} has entered Flying Patrol State");
        if (stateMachine.Agent.enabled) stateMachine.Agent.enabled = false;

        if (stateMachine.PatrolPath == null) // Safety procedure to ensure we actually have a path to follow
        {
            stateMachine.SwitchState(new AnimalPatrolState(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 != null) 
        {
            movementSpeed = stateMachine.MovementSpeed * patrolPoint.SpeedModifier;
            acceptanceRadius = patrolPoint.AcceptanceRadius;
            dwellTime = patrolPoint.DwellTime;
        }
        else 
        {
            movementSpeed = stateMachine.MovementSpeed;
        }

        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);
        PlayCorrectBlendTreeHash();
    }

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

        if (IsInAcceptanceRange()) 
        {
            // stateMachine.SwitchState(new AnimalFlyingDwellState(stateMachine, dwellTime));
            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 Vector3 here, without relying on the NavMeshAgent

        if (deltaMagnitude > 0) 
        {
            PlayCorrectFloatValue();
        }
    }

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

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

    private void PlayCorrectBlendTreeHash() 
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Raven) 
        {
            stateMachine.Animator.CrossFadeInFixedTime(RavenFlyFreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
        }
    }

    private void PlayCorrectFloatValue()
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            Vector3 deltaMovement = targetPatrolPoint - stateMachine.transform.position;
            float horizontalSpeed = new Vector2(deltaMovement.x, deltaMovement.z).magnitude;
            float verticalSpeed = deltaMovement.y;

            stateMachine.Animator.SetFloat(flyHorizontal, horizontalSpeed);
            stateMachine.Animator.SetFloat(flyVertical, verticalSpeed);
        }
    }

    private void FaceMovementDirection(Vector3 direction, float deltaTime)
    {
        if (direction == Vector3.zero) return;
        Quaternion targetRotation = Quaternion.LookRotation(direction);
        stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, targetRotation, deltaTime * movementSpeed);
    }

    private void MoveToWaypoint(float deltaTime)
    {
        Vector3 direction = (targetPatrolPoint - stateMachine.transform.position).normalized;
        Move(stateMachine.transform.position += direction * movementSpeed, deltaTime);
        FaceMovementDirection(direction, deltaTime);
    }
}

I’m not exactly sure what my code does, if I’m being honest, I was just trying to modify ‘AnimalPatrolState.cs’ (my original code) to make flying off the NavMeshAgent work, but this is one of those heavy mathematical topics that can really baffle me

I also created this Blend Tree for the bird, for cases where it’s flying off the surface

Here’s what it looks like:

The blend tree is controlled by two variables, ‘flyVertical’ (Y-axis values of the image above) and ‘flyHorizontal’ (X-axis values of the image above), and it’s a 2D Freeform Cartesian Tree

Any sort of help to make patrol paths for NPCs that move off the NavMeshAgent would be very helpful, thank you very much! :slight_smile:

My idea was simple: when the animal first spawns, it should bump the ‘flyVertical’ value to 0.5 to take off, and once that animation is done, then push both x and y axis accordingly so it can play the correct animations to fly to wherever the closest patrol point it can find will be

(I can also use the same formula for swimming NPCs, so that would be a really nice addon)

OK so since that blend tree seemed a little too complicated, I decided to start simple. I created a variant of the patrol state, named it ‘AnimalFlyingPatrolState.cs’ and decided to remove anything relevant to the NavMeshAgent and just work on moving the character controller to wherever it has to go. Here’s the current code:

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

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

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

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

    public override void Enter()
    {
        // Disable NavMesh agent and gravity
        if (stateMachine.Agent.enabled) stateMachine.Agent.enabled = false;

        if (stateMachine.PatrolPath == null)
        {
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
            return;
        }

        int index; // Index of the Patrol Point

        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.Raven)
        {
            // Handle Raven-specific setup
            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);
        PlayCorrectBlendTreeHash();

        AnimalMountManager.OnAnimalMounted += RemoveAnimalPatrolPath;
    }

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

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

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

        Vector3 direction = (targetPatrolPoint - stateMachine.transform.position).normalized;

        if (deltaMagnitude > 0)
        {
            FaceMovementDirection(direction, deltaTime);
            PlayCorrectFloatValue(deltaTime);
        }
        else
        {
            FaceMovementDirection(direction, deltaTime);
            PlayCorrectFloatValue(deltaTime);
        }
    }

    public override void Exit()
    {
        stateMachine.Agent.enabled = true;

        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 void PlayCorrectBlendTreeHash()
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            stateMachine.Animator.CrossFadeInFixedTime(RavenFlyFreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
        }
    }

    private void PlayCorrectFloatValue(float deltaTime)
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0.5f, stateMachine.AnimatorDampTime, deltaTime);
        }
    }

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

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

    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.StartCoroutine(RemovePatrolPathDelayed(0.05f));
    }

    private IEnumerator RemovePatrolPathDelayed(float delay)
    {
        yield return new WaitForSeconds(delay);
        if (stateMachine.PlayerOnAnimalStateMachine.GetAnimal().GetUniqueID() == stateMachine.GetComponent<Animal>().GetUniqueID())
        {
            stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
            stateMachine.AssignPatrolPath(null);
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
        }
    }
}

All I did was essentially get rid of anything NavMesh-related, and deactivated it when entering and re-activated it when exiting (and get rid of anything relevant to non-flying animals)

The problem I have is, is that in terms of the y-axis, it will not get off the NavMesh. It’ll just remain stuck there, and I’m baffled as to why (and naturally when it gets to the patrol point, but it doesn’t make a move on the y-axis, it goes nuts (mainly because I didn’t create a flying dwelling state yet)).

So… How do I get the bird to move on the y-axis?

You might also find the ‘AnimalBaseState.cs’, which is extremely similar to the Enemy’s base state script, useful:

using RPG.Animals;
using RPG.Attributes;
using UnityEngine;
using UnityEngine.AI;

// This script is based off 'EnemyBaseState.cs'

namespace RPG.States.Animals
{

public abstract class AnimalBaseState : State
{
    // Free Look Speed
    protected static readonly int FreeLookSpeedHash = Animator.StringToHash("FreeLookSpeed");

    // Horse
    protected static readonly int HorseFreeLookBlendTreeHash = Animator.StringToHash("HorseFreeLookBlendTree");
    protected static readonly int HorseIdleHash = Animator.StringToHash("HorseIdle");

    // Raven
    protected static readonly int RavenFlyFreeLookBlendTreeHash = Animator.StringToHash("RavenFlyFreeLookBlendTree");
    protected static readonly int RavenFreeLookBlendTreeHash = Animator.StringToHash("RavenFreeLookBlendTree");
    protected static readonly int RavenIdleHash = Animator.StringToHash("RavenIdle");

    protected float AnimatorDampTime = 0.1f;

    protected AnimalStateMachine stateMachine;

    public AnimalBaseState(AnimalStateMachine stateMachine)
    {
        this.stateMachine = stateMachine;
    }

    protected bool IsInChaseRange()
    {
        // If the last attacker was an NPC, go for that NPC, under the condition that he's closer than the 'PlayerChasingRangedSquared'
        if (stateMachine.LastAttacker != null 
        && !stateMachine.LastAttacker.CompareTag("Player") 
        && stateMachine.LastAttacker.GetComponent<Health>() != null 
        && !stateMachine.LastAttacker.GetComponent<Health>().IsDead() 
        && Vector3.SqrMagnitude(stateMachine.transform.position - stateMachine.GetLastAttacker().transform.position) <= stateMachine.PlayerChasingRangedSquared) return true;
        // If the last attacker was the Player, go for the player, under the condition that he's nearby
        return Vector3.SqrMagnitude(stateMachine.transform.position - stateMachine.PlayerStateMachine.transform.position) <= stateMachine.PlayerChasingRangedSquared;
    }

    protected bool IsAggrevated()
    {
        // Returns true if the enemy has both a Last Attacker, and has an 'AnimalAggro' Cooldown Timer
        return stateMachine.CooldownTokenManager.HasCooldown("AnimalAggro") && stateMachine.LastAttacker != null;
    }

    // We will also get to the 'ShouldPursue()' system when we also get to the chasing system

    // We will get to the vision-based 'CanSeePlayer()' when we get it to work on our regular enemies first

    // We will also get to 'RaycastAllSorted()' when we get to the vision system

    // We will also get to the 'GetMouseRay()' when we get to the vision system

    protected void Move(float deltaTime)
    {
        Move(Vector3.zero, deltaTime);
    }

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

        if (SampleNavMesh(stateMachine.transform.position + intendedMovement))
        {
            stateMachine.CharacterController.Move(intendedMovement);
        }
        else
        {
            stateMachine.CharacterController.Move(new Vector3(0, stateMachine.ForceReceiver.Movement.y, 0));
        }
    }

    protected virtual void FaceTarget(Vector3 target, float deltaTime)
    {
        Vector3 directionToTarget = target - stateMachine.transform.position;
        directionToTarget.y = 0;
        stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, Quaternion.LookRotation(directionToTarget), stateMachine.FreeLookRotationSpeed * deltaTime);
    }

    protected float GetNormalizedTime(string tag = "Attack")
    {
        // Get the Player's Layer Index for each animal, rather than what Malbers has to offer for this one
        int layerIndex = GetAnimalLayerIndex();

        var currentInfo = stateMachine.Animator.GetCurrentAnimatorStateInfo(layerIndex);
        var nextInfo = stateMachine.Animator.GetNextAnimatorStateInfo(layerIndex);

        if (stateMachine.Animator.IsInTransition(layerIndex) && nextInfo.IsTag(tag))
        {
            return nextInfo.normalizedTime;
        }
        else if (!stateMachine.Animator.IsInTransition(layerIndex) && currentInfo.IsTag(tag))
        {
            return currentInfo.normalizedTime;
        }

        return layerIndex;
    }

    private int GetAnimalLayerIndex() 
    {
        // From the Layers of Animation on each individual animal,
        // This function will return the layer where the player has
        // assigned his animations to use for each animal individually

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse) 
        {
            return 4; // HORSE
        }

        // Implement Other animals here

        else return 5; // RAVEN
    }

    protected bool SampleNavMesh(Vector3 position)
    {
        if (!stateMachine.CharacterController.isGrounded)
        {
            RaycastHit hit;
            bool hasHit = Physics.Raycast(position, Vector3.down, out hit, 2, 1<<LayerMask.NameToLayer("Terrain"));
            if (hasHit)
            {
                position = hit.point;
            }
        }

        NavMeshHit navMeshHit;
        bool hasCastToNavMesh = NavMesh.SamplePosition(position, out navMeshHit, 1f, NavMesh.AllAreas);

        if (!hasCastToNavMesh)
        {
            Debug.Log($"{stateMachine.gameObject.name} is attempting to move off the NavMesh");
            return false;
        }

        return true;
    }

    protected void SetLayerWeight(int layerIndex, float weight)
    {
        if (stateMachine.Animator != null)
        {
            stateMachine.Animator.SetLayerWeight(layerIndex, weight);
            Debug.Log($"Set Animation Layer: {stateMachine.Animator.GetLayerName(layerIndex)} to weight: {weight}");
        }
        else
        {
            Debug.Log($"Animator on the animal does not exist");
        }
    }

    protected void ActivateNonMalbersLayerWeight(int layerIndex, float weight)
    {
        SetLayerWeight(layerIndex, weight);
    }
}

}

And to differentiate between flying and non-flying animals, here’s what I use in ‘AnimalStateMachine.cs’:

    [field: Tooltip("Can this animal Fly?")]
    [field: SerializeField] public bool CanFly {get; private set;}

And… the Force Receiver is here as well, because I suspect that the vertical velocity in here plays a role with how the bird reacts to all of this:

using UnityEngine;
using UnityEngine.AI;
using System.Collections;

namespace RPG.Movement
{
    public class ForceReceiver : MonoBehaviour
    {
        [SerializeField] private float drag = 0.3f;
        [SerializeField] private float minimumImpactVelocity = 0.1f;
        [SerializeField] private float divingSpeed = 3.0f;
        
        private CharacterController controller;
        private NavMeshAgent agent;

        public event System.Action <Vector3> OnForceApplied;
        public event System.Action OnForceCompleted;

        private AnimalStateMachine animalStateMachine;

        bool forceActive;

        // Swimming and Diving booleans:
        bool isSwimming;
        bool isDiving;
        bool isReviving;

        private void Awake()
        {
            controller = GetComponent<CharacterController>();
            agent = GetComponent<NavMeshAgent>();
            animalStateMachine = GetComponent<AnimalStateMachine>();
        }

        private float verticalVelocity;

        private Vector3 impact;
        private Vector3 dampingVelocity;

        public Vector3 Movement => impact + Vector3.up * verticalVelocity;

        private void Update()
        {
            if (isSwimming)
            {
                verticalVelocity = 0f;
            }

            if (isDiving)
            {
                verticalVelocity = -divingSpeed;
            }

            if (isReviving)
            {
                verticalVelocity = divingSpeed;
            }

            else if (verticalVelocity < 0f && controller.isGrounded)
            {
                verticalVelocity = Physics.gravity.y * Time.deltaTime;
            }
            
            else
            {
                verticalVelocity += Physics.gravity.y * Time.deltaTime;
            }
            
            impact = Vector3.SmoothDamp(impact, Vector3.zero, ref dampingVelocity, drag);

            // if the squared magnitude of the impact is below the 'minimumImpactVelocity',
            // reset the impact and enable the agent (i.e: he took the impact, now get back to normal)
            if (forceActive) 
            {
                if (impact.sqrMagnitude < minimumImpactVelocity) 
                {
                    impact = Vector3.zero;
                    forceActive = false;
                    OnForceCompleted?.Invoke();
                    if (agent) agent.enabled = false;
                }
            }
        }

        public void AddForce(Vector3 force, bool triggerKnockbackEvent = false)
        {
            impact += force;
            if (agent) agent.enabled = false;
            if (triggerKnockbackEvent) OnForceApplied?.Invoke(force);
            forceActive = true;
        }

        public void Jump(float jumpForce)
        {
            verticalVelocity += jumpForce;
        }

        public void SetIsSwimming(bool isSwimming)
        {
            this.isSwimming = isSwimming;
        }

        public void SetIsDiving(bool isDiving) 
        {
            this.isDiving = isDiving;
        }

        public void SetIsReviving(bool isReviving) 
        {
            this.isReviving = isReviving;
        }

        /// <summary>
        /// This function resets the force applied to the player, when he exits the "PlayerBoatDrivingState.cs" State
        /// </summary>
        public void ResetForce() 
        {
            impact = Vector3.zero;
            verticalVelocity = 0f;
            dampingVelocity = Vector3.zero;
            forceActive = false;
            if (agent) agent.enabled = false;
        }

        public void ResetVerticalVelocity()
        {
            // Used in 'PlayerFallingState.Enter()' to ensure the vertical velocity doesn't go wild
            verticalVelocity = 0f;
        }

        public float GetVerticalVelocity()
        {
            return verticalVelocity;
        }

        // BOTH ARE TEST FUNCTIONS BELOW - Done to ensure that the Player can safely transition to
        // 'PlayerFreeLookState.cs' after impact, by triggering the 'OnForceCompleted' attached to the function
        // in 'PlayerImpactState.cs' which is triggered by the coroutine in the function below:
        public void StartImpactEffect(float duration)
        {
            StartCoroutine(ImpactEffectCoroutine(duration));
        }

        private IEnumerator ImpactEffectCoroutine(float duration)
        {
            yield return new WaitForSeconds(duration);
            OnForceCompleted?.Invoke(); // That way, 'PlayerImpactState.cs' when coming from 'PlayerFallingState.cs' will safely transition to the free look state, which is attached to 'OnForceCompleted' over there
        }
    }
}

Edit 1: I found a way, in ‘ForceReceiver.cs’ to block the NPC from receiving any vertical force, if ‘AnimalStateMachine.CanFly’ is turned on:

        private void Update()
        {
            if (isSwimming)
            {
                verticalVelocity = 0f;
            }

            if (isDiving)
            {
                verticalVelocity = -divingSpeed;
            }

            if (isReviving)
            {
                verticalVelocity = divingSpeed;
            }

            else if (verticalVelocity < 0f && controller.isGrounded)
            {
                verticalVelocity = Physics.gravity.y * Time.deltaTime;
            }
            
            else
            {
                // THE FOLLOWING IF STATEMENT, ADDED BY BAHAA, MAKES SURE ANY
                // FLYING ANIMAL DOES NOT ATTEMPT TO FALL TO THE GROUND WITHOUT CONSENT
                if (GetComponent<AnimalStateMachine>() != null && GetComponent<AnimalStateMachine>().CanFly)
                {
                    verticalVelocity = 0f;
                }

                else verticalVelocity += Physics.gravity.y * Time.deltaTime;
            }
            
            impact = Vector3.SmoothDamp(impact, Vector3.zero, ref dampingVelocity, drag);

            // if the squared magnitude of the impact is below the 'minimumImpactVelocity',
            // reset the impact and enable the agent (i.e: he took the impact, now get back to normal)
            if (forceActive) 
            {
                if (impact.sqrMagnitude < minimumImpactVelocity) 
                {
                    impact = Vector3.zero;
                    forceActive = false;
                    OnForceCompleted?.Invoke();
                    if (agent) agent.enabled = false;
                }
            }
        }

WHICH… DID NOT WORK FOR LONG! (The Force Receiver is incredibly challenging for me…)

Please help me out, I’m severely baffled here


Edit 2: This has to do with MalberS and the physics applied to the bird. it’s what has been causing me so much trouble recently. Honestly, I think it’s best that this idea is put on hold for the time being, because this just sounds like a lot of problems waiting to happen

Once he gives me an answer on how to control the y-axis without hurting the falling state in his code, I will get back to this post

In the meanwhile I’ll go work on Equestrianism

Once again, the NavMesh can be useful in this case, but it requires playing around with the offset. Since a bird can fly anywhere, you can create a new layer “FlyingObstacle”, and put NavMeshModifierVolumes in areas you don’t want the Raven to fly with the Agent Type on the Modifier set to the Raven’s agent type. Put a plane on the map that covers the flyable area of the game. Give it a NavMeshModifier (not volume) and set it to just the Raven layer, and make it Walkable. Once you’ve baked the Surface, you can disable the plane in the inspector. Then a new NavMeshSurface for the Raven can be baked.

Now regardless of where the Agent is in the Y dimension, you can use the X and Z components of the desired velocity to move the character, adding in the true Y dimension.

We’ll return to this in a day or two, as soon as the current equestrianism implementation is done :slight_smile: (It’s honestly a bit of a headache, and I need some help)

Privacy & Terms