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
-
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
-
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
If you have any questions about anything, please ask