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