Game Animation Sample: Strafe facing a specific direction

Hello all !

I’m trying to lock a character’s orientation in the Game Animation sample so it always moves facing the same direction. By default this is possible only when facing towards the view direction.

I saw this post (no real solution though):

https://forums.unrealengine.com/t/game-animation-sample-5-5-orient-rotation/2269499

By default, PoseSearchGenerateTransformTrajectory uses Character->GetViewRotation(). I overrode it in the AnimBP to lock the orientation instead. The issues I’m hitting:

  • I want the locked orientation to be the mesh root at the moment I lock it but that often doesn’t match the actor rotation.

  • If the actor rotation is different than locked yaw then the character “ice skates” (the animations are nor correctly chosen by the motion matching).

  • If I snap the actor rotation to the root rotation then I get a one-frame glitch where the character changes its orientation.

Here is my override of PoseSearchGenerateTransformTrajectory for reference:

void ADefaultCharacter::PoseSearchGenerateTransformTrajectoryWithYaw(const UObject* InContext, const FPoseSearchTrajectoryData& InTrajectoryData,
	float InDeltaTime, FTransformTrajectory& InOutTrajectory, bool LockedOrientation, float LockedOrientationYaw, float& InOutDesiredControllerYawLastUpdate, FTransformTrajectory& OutTrajectory,
	float InHistorySamplingInterval, int32 InTrajectoryHistoryCount, float InPredictionSamplingInterval, int32 InTrajectoryPredictionCount)
{
	FPoseSearchTrajectoryData::FSampling TrajectoryDataSampling;
	TrajectoryDataSampling.NumHistorySamples = InTrajectoryHistoryCount;
	TrajectoryDataSampling.SecondsPerHistorySample = InHistorySamplingInterval;
	TrajectoryDataSampling.NumPredictionSamples = InTrajectoryPredictionCount;
	TrajectoryDataSampling.SecondsPerPredictionSample = InPredictionSamplingInterval;

	FPoseSearchTrajectoryData::FState TrajectoryDataState;
	TrajectoryDataState.DesiredControllerYawLastUpdate = InOutDesiredControllerYawLastUpdate;

	FPoseSearchTrajectoryData::FDerived TrajectoryDataDerived;
	UpdateDataWithYaw(InTrajectoryData, InDeltaTime, InContext, LockedOrientation, LockedOrientationYaw, TrajectoryDataDerived, TrajectoryDataState);
	UPoseSearchTrajectoryLibrary::InitTrajectorySamples(InOutTrajectory, TrajectoryDataDerived.Position, TrajectoryDataDerived.Facing, TrajectoryDataSampling, InDeltaTime);
	UPoseSearchTrajectoryLibrary::UpdateHistory_TransformHistory(InOutTrajectory, TrajectoryDataDerived.Position, TrajectoryDataDerived.Velocity, TrajectoryDataSampling, InDeltaTime);
	UPoseSearchTrajectoryLibrary::UpdatePrediction_SimulateCharacterMovement(InOutTrajectory, InTrajectoryData, TrajectoryDataDerived, TrajectoryDataSampling, InDeltaTime);

	InOutDesiredControllerYawLastUpdate = TrajectoryDataState.DesiredControllerYawLastUpdate;
	
	OutTrajectory = InOutTrajectory;
}

bool ADefaultCharacter::UpdateDataWithYaw(	const FPoseSearchTrajectoryData& PoseSearchTrajectoryData,
											float DeltaTime,
											const UObject* Context,
											bool LockedOrientation,
											float LockedOrientationYaw,
											FPoseSearchTrajectoryData::FDerived& TrajectoryDataDerived,
											FPoseSearchTrajectoryData::FState& TrajectoryDataState)
{
	const ACharacter* Character = Cast<ACharacter>(Context);
	if (!Character)
	{
		if (const UAnimInstance* AnimInstance = Cast<UAnimInstance>(Context))
		{
			Character = Cast<ACharacter>(AnimInstance->GetOwningActor());
		}
		else if (const UActorComponent* AnimNextComponent = Cast<UActorComponent>(Context))
		{
			Character = Cast<ACharacter>(AnimNextComponent->GetOwner());
		}
		
		if (!Character)
		{
			return false;
		}
	}

	const UCharacterMovementComponent* MoveComp = Character->GetCharacterMovement();
	const USkeletalMeshComponent* MeshComp = Character->GetMesh();
	if (!MoveComp || !MeshComp)
	{
		return false;
	}

	TrajectoryDataDerived.MaxSpeed = FMath::Max(MoveComp->GetMaxSpeed() * MoveComp->GetAnalogInputModifier(), MoveComp->GetMinAnalogSpeed());
	TrajectoryDataDerived.BrakingDeceleration = FMath::Max(0.f, MoveComp->GetMaxBrakingDeceleration());
	TrajectoryDataDerived.BrakingSubStepTime = MoveComp->BrakingSubStepTime;
	TrajectoryDataDerived.bOrientRotationToMovement = MoveComp->bOrientRotationToMovement; // false when LockedOrientation is true

	TrajectoryDataDerived.Velocity = MoveComp->Velocity;
	TrajectoryDataDerived.Acceleration = MoveComp->GetCurrentAcceleration();
		
	TrajectoryDataDerived.bStepGroundPrediction = !MoveComp->IsFalling() && !MoveComp->IsFlying();

	if (TrajectoryDataDerived.Acceleration.IsZero())
	{
		TrajectoryDataDerived.Friction = MoveComp->bUseSeparateBrakingFriction ? MoveComp->BrakingFriction : MoveComp->GroundFriction;
		const float FrictionFactor = FMath::Max(0.f, MoveComp->BrakingFrictionFactor);
		TrajectoryDataDerived.Friction = FMath::Max(0.f, TrajectoryDataDerived.Friction * FrictionFactor);
	}
	else
	{
		TrajectoryDataDerived.Friction = MoveComp->GroundFriction;
	}

	const float DesiredYaw = LockedOrientation ? LockedOrientationYaw : Character->GetViewRotation().Yaw;
	
	const float DesiredYawDelta = DesiredYaw - TrajectoryDataState.DesiredControllerYawLastUpdate;
	TrajectoryDataState.DesiredControllerYawLastUpdate = DesiredYaw;
	
	if (DeltaTime > UE_SMALL_NUMBER)
	{
		// An AnimInstance might call this during an AnimBP recompile with 0 delta time, so we don't update ControllerYawRate
		TrajectoryDataDerived.ControllerYawRate = FRotator::NormalizeAxis(DesiredYawDelta) / DeltaTime;
		if (PoseSearchTrajectoryData.MaxControllerYawRate >= 0.f)
		{
			TrajectoryDataDerived.ControllerYawRate = FMath::Sign(TrajectoryDataDerived.ControllerYawRate) * FMath::Min(FMath::Abs(TrajectoryDataDerived.ControllerYawRate), PoseSearchTrajectoryData.MaxControllerYawRate);
		}
	}

	TrajectoryDataDerived.Position = MeshComp->GetComponentLocation();
	TrajectoryDataDerived.MeshCompRelativeRotation = MeshComp->GetRelativeRotation().Quaternion();

	if (TrajectoryDataDerived.bOrientRotationToMovement)
	{
		TrajectoryDataDerived.Facing = MeshComp->GetComponentRotation().Quaternion();
	}
	else
	{
		TrajectoryDataDerived.Facing = FQuat::MakeFromRotator(FRotator(0,TrajectoryDataState.DesiredControllerYawLastUpdate,0)) * TrajectoryDataDerived.MeshCompRelativeRotation;
	}
	return true;
}

Ideally I’d like to align the actor to the root orientation without the pop.

Or, if there’s a way to make motion matching still work with mismatched locked orientation vs actor rotation, that’d be awesome.

Any idea ?