Hi!
I will try to describe my problem in great detail because I’m probably not the only one who will benefit from the answer. Thanks in advance
I want to integrate FollowerEntity into the Ultimate Character Controller (UCC) asset. UCC already has built-in integration with other components implementing IAstarAI (RichAI, AILerp, etc.), which is based on the steps in this order:
- Awake() →
canMove = false
- Update() → calling
IAstarAI.MovementUpdate()
, modifying the returned values and assigning them to the variablesVector2 m_inputVector
andVector3 m_DeltaRotation
.
/// <summary>
/// Updates the ability.
/// </summary>
public override void Update()
{
m_IAstarAIAgent.MovementUpdate(Time.fixedDeltaTime, out m_NextPosition, out m_NextRotation);
var localVelocity = m_Transform.InverseTransformDirection((m_NextPosition - m_CharacterLocomotion.Position) / Time.fixedDeltaTime);
localVelocity.y = 0;
// Only move if a path exists.
m_InputVector = Vector2.zero;
if (m_IAstarAIAgent.hasPath && localVelocity.sqrMagnitude > 0.01f && m_IAstarAIAgent.remainingDistance > 0.01f) {
// Only normalize if the magnitude is greater than 1. This will allow the character to walk.
if (localVelocity.sqrMagnitude > 1) {
localVelocity.Normalize();
}
m_InputVector.x = localVelocity.x;
m_InputVector.y = localVelocity.z;
}
var rotation = m_NextRotation * Quaternion.Inverse(m_Transform.rotation);
m_DeltaRotation.y = Utility.MathUtility.ClampInnerAngle(rotation.eulerAngles.y);
base.Update();
}
- Some internal UCC logic, takes previously cached m_inputVector and m_DeltaRotation, and modifies them, and saves the final movement in
m_CharacterLocomotion.Position
andm_CharacterLocomotion.Rotation
. - LateUpdate() → calling
IAstarAI.FinalizeMovement()
withm_CharacterLocomotion.Position
andm_CharacterLocomotion.Rotation
.
/// <summary>
/// Notify IAStarAI of the final position and rotation after the movement is complete.
/// </summary>
public override void LateUpdate()
{
m_IAstarAIAgent.FinalizeMovement(m_CharacterLocomotion.Position, m_CharacterLocomotion.Rotation);
}
From the documentation, it seems that FollowerEntity does not support MovementUpdate or FinalizeMovement functions, instead it gives the ability to modify the movement using MovementOverrides callbacks. I tried to somehow logically split the existing logic, but unfortunately, it does not work out. I also took inspiration from the MecanimBridge component from A* Pathfinding Project examples.
MecanimBridge code:
void MovementOverride (Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings, ref MovementControl movementControl, ref ResolvedMovement resolvedMovement) {
var desiredVelocity = math.normalizesafe(resolvedMovement.targetPoint - localTransform.Position) * resolvedMovement.speed;
var currentRotation = movementPlane.value.ToPlane(localTransform.Rotation);
var deltaRotationSpeed = AstarMath.DeltaAngle(currentRotation, resolvedMovement.targetRotation);
deltaRotationSpeed = Mathf.Sign(deltaRotationSpeed) * Mathf.Clamp01(Mathf.Abs(deltaRotationSpeed) / math.max(0.001f, dt * resolvedMovement.rotationSpeed));
deltaRotationSpeed = -deltaRotationSpeed * resolvedMovement.rotationSpeed;
smoothedRotationSpeed = Mathf.Lerp(smoothedRotationSpeed, deltaRotationSpeed, angularVelocitySmoothing > 0 ? dt / angularVelocitySmoothing : 1);
// Calculate the desired velocity relative to the character (+Z = forward, +X = right)
var localDesiredVelocity = localTransform.InverseTransformDirection(desiredVelocity);
localDesiredVelocity.y = 0;
smoothedVelocity = Vector3.Lerp(smoothedVelocity, localDesiredVelocity, velocitySmoothing > 0 ? dt / velocitySmoothing : 1);
if (smoothedVelocity.magnitude < 0.4f) {
smoothedVelocity = smoothedVelocity.normalized * 0.4f;
}
var normalizedRotationSpeed = movementSettings.follower.maxRotationSpeed > 0 ? Mathf.Rad2Deg * Mathf.Abs(resolvedMovement.rotationSpeed) / movementSettings.follower.maxRotationSpeed : 0;
var normalizedSpeed = movementSettings.follower.speed * naturalSpeed > 0 ? resolvedMovement.speed / naturalSpeed : 0;
// Combine the normalized rotation speed and normalized speed such that either of them being large, results in the input magnitude being large.
// This is to ensure that even if the agent wants to almost rotate on the spot, the input magnitude will still be large.
var inputMagnitude = Mathf.Min(1, Mathf.Sqrt(normalizedSpeed*normalizedSpeed + normalizedRotationSpeed*normalizedRotationSpeed));
anim.SetFloat(InputMagnitudeKeyHash, inputMagnitude);
anim.SetFloat(XAxisKeyHash, smoothedRotationSpeed);
anim.SetFloat(YAxisKeyHash, smoothedVelocity.z);
// Calculate how much the agent should rotate during this frame
var nextPosition = localTransform.Position;
var nextRotation = localTransform.Rotation;
// Apply rotational root motion
nextRotation = anim.deltaRotation * nextRotation;
nextPosition += (float3)anim.deltaPosition;
resolvedMovement.targetPoint = nextPosition;
resolvedMovement.targetRotation = movementPlane.value.ToPlane(nextRotation);
// target rotation speed?
resolvedMovement.speed = math.length(nextPosition - localTransform.Position) / math.max(0.001f, dt);
}
I currently have this:
private Vector2 _inputVector;
private Vector3 _deltaRotation;
public float velocitySmoothing = 1;
public float angularVelocitySmoothing = 1;
float smoothedRotationSpeed;
Vector3 smoothedVelocity;
private Vector3 newDeltaRotation;
private Vector2 newInputVector;
private bool newValues;
private Vector3 locomotionPosition;
private Quaternion locomotionRotation;
/// <summary>
/// Initialize the default values.
/// </summary>
public override void Awake()
{
base.Awake();
if (_astarAI is not FollowerEntity entity)
{
_astarAI.canMove = false;
}
else
{
_isFollowerEntityAI = true;
_followerEntity = entity;
_followerEntity.movementOverrides.AddBeforeMovementCallback(OverrideMovement);
}
}
/// <summary>
/// Updates the ability.
/// </summary>
public override void Update()
{
if (_isFollowerEntityAI)
{
newValues = false;
_inputVector = newInputVector;
_deltaRotation = newDeltaRotation;
// newPositionVector = float3.zero;
// newRotationValue = 0f;
base.Update();
return;
}
// The rest of the code that supports other AIMovement than FollowerEntity.
// ...
}
/// <summary>
/// Notify IAStarAI of the final position and rotation after the movement is complete.
/// </summary>
public override void LateUpdate()
{
if (_isFollowerEntityAI)
{
locomotionPosition = m_CharacterLocomotion.Position;
locomotionRotation = m_CharacterLocomotion.Rotation;
syncMovement = true;
return;
}
_astarAI.FinalizeMovement(m_CharacterLocomotion.Position, m_CharacterLocomotion.Rotation);
}
private void OverrideMovement(Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings, ref MovementControl movementControl, ref ResolvedMovement resolvedMovement)
{
float3 desiredVelocity = math.normalizesafe(resolvedMovement.targetPoint - localTransform.Position) * resolvedMovement.speed;
float currentRotation = movementPlane.value.ToPlane(localTransform.Rotation);
float deltaRotationSpeed = AstarMath.DeltaAngle(currentRotation, resolvedMovement.targetRotation);
deltaRotationSpeed = Mathf.Sign(deltaRotationSpeed) * Mathf.Clamp01(Mathf.Abs(deltaRotationSpeed) / math.max(0.001f, dt * resolvedMovement.rotationSpeed));
deltaRotationSpeed = -deltaRotationSpeed * resolvedMovement.rotationSpeed;
smoothedRotationSpeed = Mathf.Lerp(smoothedRotationSpeed, deltaRotationSpeed, angularVelocitySmoothing > 0 ? dt / angularVelocitySmoothing : 1);
// Calculate the desired velocity relative to the character (+Z = forward, +X = right)
float3 localDesiredVelocity = localTransform.InverseTransformDirection(desiredVelocity);
localDesiredVelocity.y = 0;
smoothedVelocity = Vector3.Lerp(smoothedVelocity, localDesiredVelocity, velocitySmoothing > 0 ? dt / velocitySmoothing : 1);
if (smoothedVelocity.magnitude < 0.4f) {
smoothedVelocity = smoothedVelocity.normalized * 0.4f;
}
float rotationChange = smoothedRotationSpeed * dt;
newDeltaRotation.y = rotationChange;
newInputVector = new Vector2(smoothedRotationSpeed, smoothedVelocity.z);
newValues = true;
if (syncMovement)
{
// ... ?
resolvedMovement.targetPoint = (float3)locomotionPosition;
resolvedMovement.targetRotation = movementPlane.value.ToPlane(locomotionRotation);
resolvedMovement.speed = math.length(locomotionPosition - (Vector3)localTransform.Position) / math.max(0.001f, dt);
syncMovement = false;
}
}
Result (enable fullscreen):
https://i.imgur.com/0yBEIcC.mp4
It doesn’t work properly. I tried to split it logically like this:
- Awake() →
canMove = true
- Update() → updated values
_inputVector
and_deltaRotation
based on the values calculated in the last OverrideMovement() function call. - OverrideMovement() → calculates values
newInputVector
andnewDeltaRotation
, which will then be used in point 1. and updates resolvedMovement with the values cached in the last LateUpdate() call - LateUpdate() → caches
locomotionPosition
andlocomotionRotation
, so that OverrideMovement can use them to update its position.
Maybe I should break it down differently or maybe I’m close to a solution, but I’m just assigning the wrong values? I’m very interested in the answer, as I’m sure anyone who wants to use FollowerEntity with a UCC asset will.
Thanks again