Distance Matching Locomotion : Giving it a shot!

Been a long time coming, but here is my distance matching code. The way I used it was I put it in a BP Lib and made calls in the animBP to trigger the functions based of off anim state or state transition notifies. Use it as you will, I am not officially supporting it. The plan was to add this to my AnimWarp+IK plugin but life took a turn and I don’t have time to develop it any further for the foreseeable future.


// Copyright Patrick Stancu 2021


#include "BP_Lib_AnimWarpIK.h"

#include "Engine/Engine.h" 
#include "EngineUtils.h"	


void UBP_Lib_AnimWarpIK::CalcWarpFactorAndPlayRate(float CurrentMovementSpeed, const float AnimAuthordMoveSpeed, float& WarpFactor, float& PlayRate, const float WarpFactorCap, const float PlayRateRatioFaster, const float PlayRateRatioSlower)
{
	float SpeedChangeRatio = FMath::GetMappedRangeValueUnclamped(FVector2D(0.0f, AnimAuthordMoveSpeed), FVector2D(0.0f, 1.0f), CurrentMovementSpeed);
	float SpeedChangeReductionRatio = FMath::Abs(1.0f - SpeedChangeRatio);
	//if (SpeedChangeRatio > 0)
	//{
	//	//SpeedChangeRatio = FMath::Clamp(SpeedChangeRatio, 0.0f, WarpFactorCap);
	//	WarpFactor = FMath::Clamp(FMath::Clamp(SpeedChangeRatio, 0.0f, WarpFactorCap), 0.0f, WarpFactorCap);
	//}
	//else
	//{
	//	//SpeedChangeRatio = FMath::Clamp(1.0f - SpeedChangeRatio - 1.0f, 0.0f, WarpFactorCap);
	//	WarpFactor = FMath::Clamp(1.0f - (FMath::Clamp(1.0f - SpeedChangeRatio - 1.0f, 0.0f, WarpFactorCap)) - 1.0f, 0.0f, WarpFactorCap);
	//}

 //   if (FMath::IsNearlyEqual(WarpFactor, WarpFactorCap, 0.001f))
	//{
	//	PlayRate = (SpeedChangeRatio - WarpFactor + 1.0f);
	//}
	//else
	//{
	//	PlayRate = 1.0f;
	//}
	
	if (SpeedChangeRatio > 1.0f)
	{
		WarpFactor = ((SpeedChangeReductionRatio * (1.0f - PlayRateRatioFaster)) + 1.0f);
		
		if (WarpFactor > WarpFactorCap)
		{
			PlayRate = ((SpeedChangeReductionRatio * PlayRateRatioFaster) + 1.0f) + (WarpFactor - WarpFactorCap);
			WarpFactor = WarpFactorCap;
		}
		else
		{
			PlayRate = (SpeedChangeReductionRatio * PlayRateRatioFaster) + 1.0f;
		}
	}
	else
	{
		
		PlayRate = 1.0f - (SpeedChangeReductionRatio * PlayRateRatioSlower);

		WarpFactor = (1.0f + (SpeedChangeReductionRatio * PlayRateRatioSlower)) * SpeedChangeRatio;

	}

}

void UBP_Lib_AnimWarpIK::DetermineStopLocation(FVector& StopLocation, FVector InitPos, FVector InitVel, float Friction, FVector BrakeAccel, float DeltaTime)
{

	FVector PredicVel = InitVel;
	FVector PredicLoc = InitPos;
	float PredictTime = 0;

	while (PredictTime < 2.0f)
	{
		PredicVel -= (Friction * PredicVel + BrakeAccel) * DeltaTime;
		if (FVector::DotProduct(PredicVel, InitVel) <= 0.0f)
		{
			break;
		}
		PredicLoc += PredicVel * DeltaTime;

		PredictTime += DeltaTime;

	}

	StopLocation = PredicLoc;

}

void UBP_Lib_AnimWarpIK::GetAnimCurveValue(float& FrameTime, UAnimSequence* AnimationSequence, FName CurveName, float CurveValue)
{
	float time = 0;

	checkf(AnimationSequence != nullptr, TEXT("Invalid Animation Sequence ptr"));
	const FName ContainerName = RetrieveContainerNameForCurve(AnimationSequence, CurveName);

	if (ContainerName != NAME_None)
	{
		const FSmartName CurveSmartName = RetrieveSmartNameForCurve(AnimationSequence, CurveName, ContainerName);
		time = AnimationSequence->EvaluateCurveData(CurveSmartName.UID, CurveValue, false);
	}

	FrameTime = time;
}

void UBP_Lib_AnimWarpIK::DetermineStartLocation(FVector& StartLocation, FVector InitPos, FVector InitVel, float Friction, FVector Accel, float DeltaTime)
{
	FVector PredicVel = FVector::ZeroVector;
	FVector CurrentVel = InitVel;
	FVector PredicLoc = FVector::ZeroVector;
	FVector CurrentLoc = InitPos;
	float PredictTime = 0;

	while (PredictTime < 2.0f)
	{

		PredicVel = PredicVel - (PredicVel - Accel.GetSafeNormal() * PredicVel.Size()) * FMath::Min(DeltaTime * Friction, 1.f);
		PredicVel += Accel * DeltaTime;

		if (PredicVel.Size() >= InitVel.Size())
		{
			StartLocation = InitPos + (InitVel.GetSafeNormal() * -1.0f * FVector::Dist(FVector::ZeroVector, PredicLoc));

			return;
		}

		PredicLoc += PredicVel * DeltaTime;

		PredictTime += DeltaTime;

	}

	StartLocation = PredicLoc;
}

void UBP_Lib_AnimWarpIK::DeterminePivotLocation(FVector& PivotLocation, FVector InitPos, FVector InitVel, float Friction, FVector Accel, float DeltaTime)
{
	FVector PredicVel = InitVel;
	FVector PredicLoc = InitPos;
	float PredictTime = 0;

	while (PredictTime < 2.0f)
	{
		/*Velocity = Velocity - (Velocity - AccelDir * VelSize) * FMath::Min(DeltaTime * Friction, 1.f);
		Velocity += Acceleration * DeltaTime;*/

		PredicVel = PredicVel - (PredicVel - Accel.GetSafeNormal() * PredicVel.Size()) * FMath::Min(DeltaTime * Friction, 1.f);
		PredicVel += Accel * DeltaTime;

		if (FVector::DotProduct(PredicVel, InitVel) <= 0.0f)
		{
			break;
		}
		PredicLoc += PredicVel * DeltaTime;

		PredictTime += DeltaTime;

	}

	PivotLocation = PredicLoc;

}

																						
//CODE TAKEN FROM: AnimationBlueprintLibrary.h										

FName UBP_Lib_AnimWarpIK::RetrieveContainerNameForCurve(const UAnimSequence* AnimationSequence, FName CurveName)
{
	checkf(AnimationSequence != nullptr, TEXT("Invalid Animation Sequence ptr"));
	for (int32 Index = 0; Index < (int32)Enum_SmartNameContainerType::SNCT_MAX; ++Index)
	{
		const FSmartNameMapping* CurveMapping = AnimationSequence->GetSkeleton()->GetSmartNameContainer(SmartContainerNames[Index]);
		if (CurveMapping && CurveMapping->Exists(CurveName))
		{
			return SmartContainerNames[Index];
		}
	}

	return NAME_None;
}

const FName UBP_Lib_AnimWarpIK::SmartContainerNames[(int32)Enum_SmartNameContainerType::SNCT_MAX] = { USkeleton::AnimCurveMappingName, USkeleton::AnimTrackCurveMappingName };

bool UBP_Lib_AnimWarpIK::RetrieveSmartNameForCurve(const UAnimSequence* AnimationSequence, FName CurveName, FName ContainerName, FSmartName& SmartName)
{
	checkf(AnimationSequence != nullptr, TEXT("Invalid Animation Sequence ptr"));
	return AnimationSequence->GetSkeleton()->GetSmartNameByName(ContainerName, CurveName, SmartName);
}

FSmartName UBP_Lib_AnimWarpIK::RetrieveSmartNameForCurve(const UAnimSequence* AnimationSequence, FName CurveName, FName ContainerName)
{
	checkf(AnimationSequence != nullptr, TEXT("Invalid Animation Sequence ptr"));
	FSmartName SmartCurveName;
	AnimationSequence->GetSkeleton()->GetSmartNameByName(ContainerName, CurveName, SmartCurveName);
	return SmartCurveName;
}
				
//CODE TAKEN FROM:AnimationBlueprintLibrary.h

Referencing the code @postman09 provided, I was inspired to look more into the physics of the CMC calculations. I’m a physics guy, so the locomotion code doesn’t make sense unless I view it through the lens of physics. Perhaps there are others like me. I’ve attached a pdf document outlining the physics for the stop prediction calculation that @Slavical put into code for our small team. (@Slavical is our code guy. I’m our physics guy.)
DistanceMatchingNotes.pdf (181.5 KB)

Our stop prediction now works for any input parameters. (Our doesn’t yet work for multiplayer yet though.)

I hope you all find the notes helpful!

You don’t need to do anything server side for distance matching since its all cosmetic and not effecting capsule movement. It will work fine networked without having to do anything.

1 Like

Hey guys, so below is the code to get the time based on the distance. This works in both dev and shipping builds and functions perfectly in single player. However, I still have one minor issue with multiplayer - this function returns zero for client simulated proxies. So if you’re a client who’s also hosting the server (listen server), you can start and stop just fine, your buddies can start and stop just fine on their local machines, you can even see your buddies start and stop just fine, but your buddies don’t see your or anyone else’s starting/stopping animations . Could I get some help figuring out why this is happening?

float UMyAnimInstance::CalculateDistanceCurveTime(UAnimSequenceBase* Sequence, float Distance)
{
	// We have to use smart names and set our animations' compression settings to "Uniform Indexable"
	// in order to get curve data for development and shipping builds.
	FSmartName CurveSmartName;
	const bool bCurveExists = Sequence->GetSkeleton()->GetSmartNameByName(USkeleton::AnimCurveMappingName, DistanceCurveName, CurveSmartName);

	if (bCurveExists)
	{
		// Get an array of keys from the DistanceCurve.
		const FAnimCurveBase* CurveData = Sequence->GetCurveData().GetCurveData(CurveSmartName.UID);
		const FFloatCurve* DistanceCurve = static_cast<const FFloatCurve*>(CurveData);
		TArray<FRichCurveKey> DistanceCurveKeys = DistanceCurve->FloatCurve.Keys;
		
		for (int32 i = 0; i < DistanceCurveKeys.Num(); i++)
		{
			// Look for the first key that has a value greater than or equal to the Distance.
			if (DistanceCurveKeys[i].Value >= Distance)
			{
				// The first key found is the primary key, and the secondary key
				// is initially set to zero to prevent any divide by zero errors.
				const float PrimaryKeyValue = DistanceCurveKeys[i].Value;
				const float PrimaryKeyTime = DistanceCurveKeys[i].Time;
				float SecondaryKeyValue = 0;
				float SecondaryKeyTime = 0;

				if (i > 0)
				{
					// This is not the first iteration of the loop, so secondary key will be
					// the key before the primary key.
					SecondaryKeyValue = DistanceCurveKeys[i - 1].Value;
					SecondaryKeyTime = DistanceCurveKeys[i - 1].Time;
				}

				// Calculate a delta value and a delta time between the two keys.
				const float DeltaValue = SecondaryKeyValue - PrimaryKeyValue;
				const float DeltaTime = SecondaryKeyTime - PrimaryKeyTime; 
				
				return ((DeltaTime / DeltaValue) * (Distance - PrimaryKeyValue)) + PrimaryKeyTime;
			}
		}
	}

	return 0.f;
}

If i understand correctly, time calculation function always execute on client, but it result should be replicated to server. A you’re sure this is working in builded project? P.S.: Use binary search for finding times and lerp to find desired time between to values. See FRichCurve::Eval in engine source.

I’m trying to replicate your formula, but I’m always getting a difference of ~3 units from the actual stop location. I’m not so good with math but I guess that my formula is following what you said.

// vi = vi−1 − (aB + βvi−1)∆t
// apply friction and braking
SimulatedVelocity = OldVelocity + (RevAccel + (-Friction * OldVelocity)) * dt;
// xi = xi−1 + vi−1∆t − (aB + βvi−1)(∆t)^2
StopLocation = OldLocation + OldVelocity * DeltaTime - (SimulatedVelocity * DeltaTime * DeltaTime);

I looked back at my notes and noticed a typo—or, at least, a lack of clarity. In the way I’ve written things, the aB should be the magnitude of the BrakingDeceleration. (I like to keep my negative signs outside the parameters, when possible.) I’ve made the clarification in my notes below. (See after equation (3).)

That said, your RevAccel variable is probably already negative, if I’m understanding correctly, so the signs you and I have should be the same. I’m not sure what could be causing your error. I do, however, notice you use dt in one instance while using DeltaTime in another. I’m not sure whether you’ve defined those variables differently. Perhaps try coding in the following fashion:

SimulatedVelocity = SimulatedVelocity + (RevAccel + (-Friction * SimulatedVelocity )) * DeltaTime ;
StopLocation = StopLocation + SimulatedVelocity * DeltaTime;

DistanceMatchingNotes.pdf (174.6 KB)

Actually I was using what the CMC does in the ApplyVelocityBraking() function to calculate the stop location and after reading your notes, I tried to adapt.

That said, your RevAccel variable is probably already negative, if I’m understanding correctly, so the signs you and I have should be the same. I’m not sure what could be causing your error.

Yes, RevAccel is the BrakingDeceleration multiplied by the Velocity normal

const FVector RevAccel = (bZeroBraking ? FVector::ZeroVector : (-BrakingDeceleration * SimulatedVelocity.GetSafeNormal()));

I do, however, notice you use dt in one instance while using DeltaTime in another.

Since I was using what CMC does, the Braking function does a substep to calculate deceleration.

const float dt = ((RemainingTime > MaxTimeStep && !bZeroFriction) ? FMath::Min(MaxTimeStep, RemainingTime * 0.5f) : RemainingTime);

I’m already using StopLocation = OldLocation + (SimulatedVelocity * DeltaTime); and got better results, something like 0.1 sometimes 0.2. I don’t know if is possible to get the exactly location.

Now I have a question, is vi−1 supposed to be the previous value right, in my case the OldVelocity right?

Good job referring to the CMC. The CMC does something we haven’t implemented, which has to do with lag, I think. (Again, I’m not the programmer of the group, so take my word with a grain of salt.) When we implemented the stop prediction, we used DeltaTime throughout, and our prediction is nearly exact. But, of course, we haven’t tested much beyond the singleplayer scenario. (We have, at least, the issue Slavical mentioned above for multiplayer.)

You’re correct in saying vi-1 is the previous value, but in code the new value overwrites the old, so writing something such as SimulatedVelocity = SimulatedVelocity + (RevAccel + (-Friction * SimulatedVelocity )) * DeltaTime is effectively the same as what I wrote in my pdf notes.

The reason the calculation is not the same (and I totally forgot) it’s because when the while loop runs it only uses the value of one frame.

So, after the loop if the frame value changes, the actual deceleration values that CMC calculates will change, unless you’re using a fixed framerate or doing the math in real time.

Like this

LogTemp: Warning: Delta Seconds: 0.017827
LogTemp: Warning: Delta Seconds: 0.014507
LogTemp: Warning: Delta Seconds: 0.008334
LogTemp: Warning: Delta Seconds: 0.008334

I’m feeling dumb for not realizing this early :sweat_smile:

1 Like

With the help of many who’ve commented on this thread as well as a lot of learning on my (small) team’s end, the basic skeleton of a distance matching setup has been achieved. (See below video.) However, there are many issues that I have not been able to resolve, one of which is highlighted in the below video.

I have a question that regards orientation warping in the context of pivot/plants. (I’m using the Strider plugin for warping.) When my (test) Paragon character performs a pivot animation, the Orientation Warp node is set to rotate the hips based on a CardinalDirection variable, which is determined by the relative direction of the velocity vector. However, because pivots are performed when drastic changes of the velocity vector occur, the angle driving the orientation changes dramatically, causing annoying over-rotation behavior, as shown in the below video.

I can’t seem to think through what type of logic I should apply to remedy the issue. My guess is it’s something small I’m overlooking. I’ve attempted to use the relative acceleration direction (white arrow in vid) rather than the relative velocity direction (teal arrow in vid) to drive the orientation warp (not shown), and that helped, but it didn’t completely resolve the issue, and I also don’t know if that’s the best approach.

Does anyone have any suggestions? (I’m currently using UE4.27.)

I’ve finally released my implementation of Distance Matching plugin!

Distance matching plugin
How to use Distance Matching plugin

Hello @, thanks for your release !

I followed your tutorial installing the Distance matching plugin on 4.27.1
But when I try to apply the uniform index codec in the curve compression settings, Unreal crashes.

Have you ever encountered this problem ?
Even if I create a new projet on 4.27.1, and juste create a curve compression settings asset to apply the uniform index codec, it still crashes. But working on UE5 and older versions.
Thanks!

Yep, this is are known issue, but it is not on my side. Sorry but I can’t help you with this issue. Try to use UE4.25-4.26 or UE5.

Thanks for the info @, you helped me a lot !

I’ve managed to make quite a bit of progress with translational distance matching, as shown in this video. But as I begin to approach turn-in-place animations, I’m realizing I don’t understand how to apply distance matching to rotations.

For those of you in this thread who have successfully applied distance matching to rotations, how did you do it?

I’m familiar with the standard turn-in-place logic, as first publicly shown in this livestream by Jay at Epic, and I’ve seen many others copy his same approach. But that approach does not use distance matching, and I presume distance matching adds some benefit I’m not aware of.

For translational distance matching, the input drives how the animation plays via the distance from some dropped marker. However, for rotational distance matching, the angle drives the character movement, which I presume is done by some editing of the yaw offset input into the RotateRootBone node.

Anyway, I’m confused and would appreciate any help on this.

Rotation matching is not like distance matching . you don’t use rotation to drive the animation, you play the animations and you use its rotation curve to drive the root rotation.
here’s someone showing how to use root offset and rotation curve with turn in place. should be a good start for you.

Thanks for the response. The standard way of doing turn-in-place is exactly as you mention. My question was whether there was another way I’m not aware of that somehow makes use of distance matching.

I got my character to turn in place using the standard approach, but then I edited that approach a bit to make the turn-in-place more accurate, as shown in the below video. I show two “steps.” In the first step, I turn the character purely using the animated angle. In the second step, I turn the character to the exact position at which the turn-in-place was triggered.

this is the way that uses “Distance Matching” .this is how paragon did turn in place there’s no other way. well there are other ways to do turn in place but you already did what you are asking about.

1 Like

For turn in place “rotation matchin” you could consider the same situation that a normal Jump takes.

Is character turning? Yes, play start turn.
Transition to idle.
Is character done turning? Yes, play end turn animation.

Now, it’s important to note that there is nothing idle about the idle state.
This will have to he an animation that starts at 0, and gets truncated ahead of its full course by the end turn.

The end trun itself, needs to contain the whole turning “ide” animation, and start at the appropriate frame for a clean transition.
Yes, you could leave thing up to the blend too. As such you just play the end animation perhaps.

The thing is, if you try turning in place yourself, you’ll notice that you start and end differently based on how far you Transition.
So you may also have to set up alternate animation sets to use for start and end based on overall distance.

This is probably going to mean making things overly complicated, when just letting a full animation drive the turn will give you perfect animation driven rotation.

But since it is possible, you can make it complicated for no reason at all other than to be able to use cheap animations you cobble together (almost single frames with a lot of blend) :stuck_out_tongue_winking_eye: