【UE】类GTA第三人称基础移动:Rotating | Strafing

复制代码
Rotating模式下判断是否需原地转身
	UFUNCTION(BlueprintCallable, Category = "CharacterRotation", meta = (HideSelfPin = "true"))
	void TurnInPlace_Rotating();

	//当前为手柄输入
	UPROPERTY(BlueprintReadWrite, Category = "Input")
	bool bIsGamepadInput = false;

void UBaseAnimInstance::UpdateLeftFootUp()
{
	USkeletalMeshComponent* OwningComponent = GetOwningComponent();
	APawn* PawnOwner = TryGetPawnOwner();
	if (OwningComponent == nullptr || PawnOwner == nullptr)
	{
		bIsLeftFootUp = false;
		return;
	}

	static const FName LeftFootSocketName(TEXT("foot_l"));
	static const FName RightFootSocketName(TEXT("foot_r"));

	const FVector LeftFootDirection =
		OwningComponent->GetSocketLocation(LeftFootSocketName).GetSafeNormal2D(0.0001f);
	const FVector RightFootDirection =
		OwningComponent->GetSocketLocation(RightFootSocketName).GetSafeNormal2D(0.0001f);
	const FVector VelocityDirection =
		PawnOwner->GetVelocity().GetSafeNormal2D(0.0001f);
	//局部速度方向
	const FVector LocalVelocityDirection =
		OwningComponent->GetComponentRotation().UnrotateVector(VelocityDirection);

	const float LeftFootDot =
		FVector::DotProduct(LeftFootDirection, LocalVelocityDirection);
	const float RightFootDot =
		FVector::DotProduct(LocalVelocityDirection, RightFootDirection);

	bIsLeftFootUp = LeftFootDot > RightFootDot;
}

Part 01 自定义数据类型:枚举和结构体

C++ 复制代码
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "BaseControllerData.generated.h"

/*
 *移动方向枚举
 */
UENUM(BlueprintType)
enum class ECardinalDirection : uint8
{
	Backward	UMETA(DisplayName = "Backward"),
	Forward		UMETA(DisplayName = "Forward"),
	Left		UMETA(DisplayName = "Left"),
	Right		UMETA(DisplayName = "Right")
};

/*
 *步态枚举
 */
UENUM(BlueprintType)
enum class EGroundGait : uint8
{
	Idle	UMETA(DisplayName = "Idle"),
	Walk	UMETA(DisplayName = "Walk"),
	Run		UMETA(DisplayName = "Run")
};

/*
 *步态参数
 */
USTRUCT(BlueprintType)
struct FSGaitSettings
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gait")
	float StandingSpeed = 0;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gait")
	float Acceleration = 0;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gait")
	float BrakingDeceleration = 0;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gait")
	float BrakingFrictionFactor = 0;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gait")
	bool bUseSeparateBrakingFriction = false;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gait")
	float BrakingFriction = 0;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gait")
	float GroundFriction = 0;
};

/*
 *旋转模式枚举
 */
UENUM(BlueprintType)
enum class ERotationMode : uint8
{
	Rotating	UMETA(DisplayName = "Rotating"),
	Strafing	UMETA(DisplayName = "Strafing")
};

Part02 设置角色初始步态和旋转模式

用到的变量及默认值

C++ 复制代码
	//步态
	UPROPERTY(BlueprintReadWrite, Category = Gait)
	TMap<EGroundGait, FSGaitSettings> GaitSettings;
	UPROPERTY(BlueprintReadWrite, Category = Gait)
	EGroundGait CurrentGroundGait;
	UPROPERTY(BlueprintReadWrite, Category = Gait)
	EGroundGait PreviousGroundGait;
	//旋转模式
	UPROPERTY(BlueprintReadWrite, Category = RotationMode)
	ERotationMode CurrentRotationMode;
	UPROPERTY(BlueprintReadWrite, Category = RotationMode)
	ERotationMode PreviousRotationMode;

	//Anim实例
	UPROPERTY(BlueprintReadWrite, Category = Anim)
	UAnimInstance* MainAnimInstance;

从RootMotion动画分析器可以看到,WalkLoop速度169.92,RunLoop速度386.19,因此可以设置一个接近的MaxWalkSpeed

C++ 复制代码
void ABaseController::InitialGaitSettings()
{
	FSGaitSettings WalkSettings;
	WalkSettings.MaxWalkSpeed = 175.0f;
	WalkSettings.MaxAcceleration = 350.0f;
	WalkSettings.BrakingDeceleration = 500.0f;
	WalkSettings.BrakingFrictionFactor = 1.0f;
	WalkSettings.bUseSeparateBrakingFriction = true;
	WalkSettings.BrakingFriction = 0.0f;
	WalkSettings.GroundFriction = 8.0f;
	GaitSettings.Add(EGroundGait::Walk, WalkSettings);

	FSGaitSettings RunSettings;
	RunSettings.MaxWalkSpeed = 375.0f;
	RunSettings.MaxAcceleration = 800.0f;
	RunSettings.BrakingDeceleration = 1200.0f;
	RunSettings.BrakingFrictionFactor = 1.0f;
	RunSettings.bUseSeparateBrakingFriction = true;
	RunSettings.BrakingFriction = 0.0f;
	RunSettings.GroundFriction = 8.0f;
	GaitSettings.Add(EGroundGait::Run, RunSettings);
}

// Sets default values
ABaseController::ABaseController()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	InitialGaitSettings();
}

BeginPlay

C++ 复制代码
void ABaseController::SetMainAnimInstance()
{
	MainAnimInstance = GetMesh()->GetAnimInstance();
}
C++ 复制代码
void ABaseController::SetGroundGait(EGroundGait NewGroundGait)
{
	if (CurrentGroundGait == NewGroundGait)
	{
		return;
	}

	//更新步态
	PreviousGroundGait = CurrentGroundGait;
	CurrentGroundGait = NewGroundGait;

	//更新CharacterMovement组件属性
	const FSGaitSettings* GaitSetting = GaitSettings.Find(CurrentGroundGait);
	UCharacterMovementComponent* MovementComponent = GetCharacterMovement();
	if (GaitSetting == nullptr || MovementComponent == nullptr)
	{
		return;
	}

	MovementComponent->MaxWalkSpeed = GaitSetting->MaxWalkSpeed;
	MovementComponent->MaxAcceleration = GaitSetting->MaxAcceleration;
	MovementComponent->BrakingDecelerationWalking = GaitSetting->BrakingDeceleration;
	MovementComponent->BrakingFrictionFactor = GaitSetting->BrakingFrictionFactor;
	MovementComponent->bUseSeparateBrakingFriction = GaitSetting->bUseSeparateBrakingFriction;
	MovementComponent->BrakingFriction = GaitSetting->BrakingFriction;
	MovementComponent->GroundFriction = GaitSetting->GroundFriction;
}
C++ 复制代码
void ABaseController::SetRotationMode(ERotationMode NewRotationMode)
{
	if (CurrentRotationMode == NewRotationMode)
	{
		return;
	}

	//更新旋转模式
	PreviousRotationMode = CurrentRotationMode;
	CurrentRotationMode = NewRotationMode;
}

Part03 更新步态数据+状态机

动画蓝图

C++ 复制代码
public:
	UPROPERTY(BlueprintReadWrite, Category = "Reference")
	ABaseController* AsBaseController = nullptr;
	UPROPERTY(BlueprintReadWrite, Category = "Reference")
	UCharacterMovementComponent* MovementComponent = nullptr;
	UPROPERTY(BlueprintReadWrite, Category = "Reference")
	float DeltaTimeX = 0.0f;

	UPROPERTY(BlueprintReadWrite, Category = "CharacterMovement")
	float GroundSpeed = 0.0f;
	UPROPERTY(BlueprintReadWrite, Category = "CharacterState")
	bool bIsMoving = false;
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation")
	FRotator LastVelocityRotation = FRotator::ZeroRotator;
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation")
	FRotator LastInputRotation = FRotator::ZeroRotator;
	UPROPERTY(BlueprintReadWrite, Category = "CharacterState")
	bool bHasInput = false;

	UPROPERTY(BlueprintReadWrite, Category = "CharacterMovement")
	EGroundGait GroundGait = EGroundGait::Idle;

	//计算动画蓝图需要的参数
	UFUNCTION(BlueprintCallable, Category = "UpdateData")
	void UpdateEssentialData();

	//更新地面步态
	UFUNCTION(BlueprintCallable, Category = "UpdateData")
	void UpdateGroundGait();
C++ 复制代码
void UBaseAnimInstance::UpdateEssentialData()
{
	if (MovementComponent == nullptr)
	{
		GroundSpeed = 0.0f;
		bIsMoving = false;
		bHasInput = false;
		return;
	}

	//Sequence 0:
	const FVector Velocity = MovementComponent->Velocity;
	GroundSpeed = Velocity.Size2D();
	bIsMoving = GroundSpeed > 1.0f;
	if (bIsMoving)
	{
		LastVelocityRotation = Velocity.Rotation();
	}

	//Sequence 1:
	bHasInput = MovementComponent->GetAnalogInputModifier() > 0.0f;
	if (bHasInput)
	{
		LastInputRotation = MovementComponent->GetLastInputVector().Rotation();
	}

}

适配手柄:摇杆推进自动切换步态

C++ 复制代码
void UBaseAnimInstance::UpdateGroundGait()
{
	if (MovementComponent == nullptr)
	{
		return;
	}

	//速度与加速度反向时应该转身
	const FVector NormalizedVelocity =
		MovementComponent->Velocity.GetSafeNormal(0.0001f);
	const FVector NormalizedAcceleration =
		MovementComponent->GetCurrentAcceleration().GetSafeNormal(0.0001f);
	const bool bShouldTurn =
		FVector::DotProduct(NormalizedVelocity, NormalizedAcceleration) < -0.5f;


	if (MovementComponent->IsFalling())
	{
		GroundGait = EGroundGait::Idle;
		return;
	}
  
	//根据移动速度和推入系数判断是否可以切换到Run
	const bool bCanRun =
		bIsMoving
		&& MovementComponent->GetMaxSpeed() > 250.0f
		&& MovementComponent->GetAnalogInputModifier() > 0.6f;

	if (bCanRun)
	{
		GroundGait = bShouldTurn ? EGroundGait::Idle : EGroundGait::Run;
		return;
	}

	const bool bCanWalk =
		bIsMoving
		&& MovementComponent->GetMaxSpeed() > 50.0f
		&& MovementComponent->GetAnalogInputModifier() > 0.01f;

	GroundGait = bCanWalk && !bShouldTurn
		? EGroundGait::Walk
		: EGroundGait::Idle;
}

UpdateGroundGait函数中:

切换为移动时转向的逻辑,用move-idle-move来实现

状态机

灰色条件就是播完自动过渡

较暗的条件是勾选了Disabled

Part04 移动停止+选择器表

判断左脚是否抬起(也就是左脚是否在前)

C++ 复制代码
	UPROPERTY(BlueprintReadWrite, Category = "CharacterState")
	bool bIsLeftFootUp = false;
C++ 复制代码
	//更新移动方向上的领先脚
	UFUNCTION(BlueprintCallable, Category = "UpdateData")
	void UpdateLeftFootUp();
C++ 复制代码
void UBaseAnimInstance::UpdateLeftFootUp()
{
	USkeletalMeshComponent* OwningComponent = GetOwningComponent();
	APawn* PawnOwner = TryGetPawnOwner();
	if (OwningComponent == nullptr || PawnOwner == nullptr)
	{
		bIsLeftFootUp = false;
		return;
	}

	static const FName LeftFootSocketName(TEXT("foot_l"));
	static const FName RightFootSocketName(TEXT("foot_r"));

	//局部左脚方向
	const FVector LeftFootDirection =
		OwningComponent
			->GetSocketTransform(LeftFootSocketName, RTS_Component)
			.GetLocation()
			.GetSafeNormal2D(0.0001f);
	//局部右脚方向
	const FVector RightFootDirection =
		OwningComponent
			->GetSocketTransform(RightFootSocketName, RTS_Component)
			.GetLocation()
			.GetSafeNormal2D(0.0001f);
	const FVector VelocityDirection =
		PawnOwner->GetVelocity().GetSafeNormal2D(0.0001f);
	//局部速度方向
	const FVector LocalVelocityDirection =
		OwningComponent->GetComponentRotation().UnrotateVector(VelocityDirection);

	const float LeftFootDot =
		FVector::DotProduct(LeftFootDirection, LocalVelocityDirection);
	const float RightFootDot =
		FVector::DotProduct(LocalVelocityDirection, RightFootDirection);

	bIsLeftFootUp = LeftFootDot > RightFootDot;
}

动画序列添加Distance曲线修改器

创建Uniform Indexable曲线压缩设置资产并分配到这些动画序列

如果打包后的游戏要运行:

  • Distance Match to Target
  • Advance Time by Distance Matching
  • 其他依赖 FAnimCurveBufferAccess 的距离匹配功能

那么相关动画的曲线必须使用 Uniform Indexable 压缩。否则编辑器里可能正常,打包后距离曲线查询可能失效,出现动画时间为 0、距离匹配不正确等问题。

CT_StopState

Stop State

AnimSetup_Stop

根据不同速度决定Stop动画的起始播放时间,而不是依赖同步组(参考Lyra,Stop和Locomotion并不是同一个同步组)

C++ 复制代码
	//根据 GroundSpeed 计算Stop动画起始播放时间
	UFUNCTION(BlueprintPure, Category = "Animation", meta = (BlueprintThreadSafe, HideSelfPin = "true"))
	float SetStopAnimStartTime(
		float LowSpeedStartTime = 1.8f,
		float MediumSpeedStartTime = 1.5f,
		float HighSpeedStartTime = 0.0f) const;
C++ 复制代码
float UBaseAnimInstance::SetStopAnimStartTime(
	const float LowSpeedStartTime,
	const float MediumSpeedStartTime,
	const float HighSpeedStartTime) const
{
	if (GroundSpeed < 300.0f)
	{
		return LowSpeedStartTime;
	}

	if (GroundSpeed <= 400.0f)
	{
		return MediumSpeedStartTime;
	}

	return HighSpeedStartTime;
}

AnimUpdate_Stop

C++ 复制代码
	UPROPERTY(BlueprintReadWrite, Category = "Curve")
	FName DistanceCurveName = FName("Distance");

效果

Part05 ALS的旋转框架

角色蓝图

InputGraph.StrafeAction

动画蓝图

Event BlueprintUpdateAnimation

MovingRotation

C++ 复制代码
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation")
	ERotationMode RotationMode = ERotationMode::Rotating;
	UPROPERTY(BlueprintReadWrite, Category = "CharacterState")
	bool bIsStrafing = false;

	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation")
	FRotator PrimaryRotation = FRotator::ZeroRotator;

	UPROPERTY(BlueprintReadWrite, Category = "Curve")
	FName RotateCurveName = FName("RotateAlpha");
	//需要修正的总旋转角度
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation")
	float StartAngle = 0.0f;
C++ 复制代码
	//Rotating模式下的旋转逻辑
	UFUNCTION(BlueprintCallable, Category = "CharacterRotation", meta = (HideSelfPin = "true"))
	void SmoothVelocityRotation(float TargetInterpSpeed, float ActorInterpSpeed);

	//Strafing模式下的旋转逻辑
	UFUNCTION(BlueprintCallable, Category = "CharacterRotation", meta = (HideSelfPin = "true"))
	void SmoothControlRotation(float TargetInterpSpeed, float ActorInterpSpeed);
C++ 复制代码
void UBaseAnimInstance::SmoothVelocityRotation(const float TargetInterpSpeed, const float ActorInterpSpeed)
{
	//PrimaryRotation = LastVelocityRotation
	PrimaryRotation = FMath::RInterpConstantTo(
		PrimaryRotation,
		LastVelocityRotation,
		DeltaTimeX,
		TargetInterpSpeed);

	if (AsBaseController == nullptr)
	{
		return;
	}

	//抵消:动画曲线RotateCurve带来的旋转偏移量
	const float RotateAlpha = GetCurveValue(RotateCurveName);
	FRotator TargetActorRotation = PrimaryRotation;
	TargetActorRotation.Yaw =
		PrimaryRotation.Yaw - RotateAlpha * StartAngle;

	const FRotator NewActorRotation = FMath::RInterpTo(
		AsBaseController->GetActorRotation(),
		TargetActorRotation,
		DeltaTimeX,
		ActorInterpSpeed);

	AsBaseController->SetActorRotation(NewActorRotation);
}

void UBaseAnimInstance::SmoothControlRotation(const float TargetInterpSpeed, const float ActorInterpSpeed)
{
	if (AsBaseController == nullptr)
	{
		return;
	}

	//PrimaryRotation = ControlRotation.Yaw
	const FRotator ControlRotation = AsBaseController->GetControlRotation();
	const FRotator TargetRotation(0.0f, ControlRotation.Yaw, 0.0f);
	PrimaryRotation = FMath::RInterpConstantTo(
		PrimaryRotation,
		TargetRotation,
		DeltaTimeX,
		TargetInterpSpeed);

	//ActorRotation => PrimaryRotation
	const FRotator NewActorRotation = FMath::RInterpTo(
		AsBaseController->GetActorRotation(),
		PrimaryRotation,
		DeltaTimeX,
		ActorInterpSpeed);

	AsBaseController->SetActorRotation(NewActorRotation);
}

为什么计算用VelocityRotation的PrimaryRotation用RInterpConstantTo:

起步瞬间假设:

复制代码
ActorRotation       = 0°
LastInputRotation   = 90°
LastVelocityRotation= 0°

AnimSetup_Start 先计算:

复制代码
PrimaryRotation = 90°
StartAngle      = 90°

原来的 RInterpTo(..., 800) 随后让 PrimaryRotation 一帧跳向旧速度方向:

复制代码
PrimaryRotation:90° → 0°

Start 开头 RotateAlpha = 1:

复制代码
TargetActorYaw
= PrimaryRotation - RotateAlpha × StartAngle
= 0° - 1 × 90°
= -90°

Actor 自己使用 RInterpTo(..., 25)。60 FPS 下:

复制代码
Alpha = 25 / 60 ≈ 0.4167
ActorYaw = 0° + (-90°) × 0.4167
         ≈ -37.5°

所以首帧角色被反向拉到大约 -37.5°。随后速度方向更新到新输入方向,PrimaryRotation 又跳回去,Actor 再往正确方向回拉,这就是"抽身"。

改成 BaseAnimInstance.cpp (line 148) 中的 RInterpConstantTo 后:

复制代码
PrimaryRotation:90° → 76.67°
TargetActorYaw:76.67° - 90° = -13.33°
Actor 首帧:约 -5.56°

反向误差被限制在很小的范围,不再产生明显抽身。

严格来说,RInterpConstantTo 也可能一帧到达目标,例如剩余角度小于 800 × DeltaTime。但它不会因为 800 > 帧率 就无条件吸附;这是两者最根本的区别。

NoMovingRotation

效果

Part06 Start资产选择

动画序列添加左右脚标记、RotateAlpha曲线修改器

一个前向起步,四个reface转向起步

确保RotateAlpha曲线从1->0

1代表有转向

CT_StartState.Run.RunRotating

添加动画序列,范围区间参考【UE】角色偏航角Yaw.Rotation分布规则

选择器表Debug

Start State

对比 Sequence Player Sequence Evaluator
时间控制 节点内部自动累加时间 外部通过Explicit Time控制
默认表现 动画会持续播放,可循环 时间不变就停在某一帧
常见用途 Idle、Walk、Run 等循环动画 距离匹配、落地、停止、按曲线或游戏逻辑控制动画进度
使用难度 简单,设置动画和 Play Rate 即可 需要自己更新时间或传入计算结果

AnimSetup_Start

AnimUpdate_Start

由于要适配手柄操作,随着摇杆推进会自动切换步态,因此需要把资产选择的ChooserTable放在Update里面

效果

Part07 曲线过渡和伪回转运动

出现问题:Start状态下开始转身前会有个抽身的动作

原因:动画的转向角度是死的,角色实际转向角度是活的,因此需要利用RotateAlpha曲线对StartAngle值进行缩放。曲线是从1开始衰减到0的,但是状态机过渡时会对曲线值也进行过渡操作,为了解决这个问题需要引入DeadBlending节点过滤掉不需要blend的动画曲线

引入DeadBlending节点

打印RotateAlpha曲线值可以发现,RotateAlpha是从0开始变化到1再衰减为0的,而不是直接从1开始衰减,这就会导致前半段出现抽身的动作

因此在动画蓝图中需要在最后加一个DeadBlending节点,用于屏蔽状态机过渡时的曲线值过渡

效果

Part08 Start和Stop时的步态切换逻辑

对需要用到的Start WalkRotating资产进行和Start RunRotating同样的操作(添加左右脚标记、RotateAlpha曲线修改器)

操作详见【动画序列添加左右脚标记、RotateAlpha曲线修改器】

CT_StartState的步态切换

CT_StartState.Walk.WalkRotating

CT_StopState的步态切换

对需要用到的Stop WalkRotating资产进行和Stop RunRotating同样的操作(添加Distance修改器)

操作详见【动画序列添加Distance曲线修改器】

由于Stop状态时的EGroundGait已经更新为Idle,不能用EGroundGait来区分WalkStop和RunStop,因此用GroundSpeed区间来判定

效果

由于走路时只会有一只脚抬起,因此走路停步可以用判断哪只脚抬起来区分

Part09 解决手柄输入不满时的滑步问题

角色蓝图

引入死区

InputGraph.IA_Move中根据阈值过滤手柄输入的 X/Y 轴

C++ 复制代码
	//根据阈值过滤手柄输入的 X/Y 轴
	UFUNCTION(BlueprintPure, Category = "Input", meta = (HideSelfPin = "true"))
	FVector2D FilterGamepadValue(const FVector2D& InputActionValue, float LowerThreshold) const;
C++ 复制代码
FVector2D ABaseController::FilterGamepadValue(const FVector2D& InputActionValue, float LowerThreshold) const
{
	return FVector2D(
		FMath::Abs(InputActionValue.X) > LowerThreshold ? InputActionValue.X : 0.0f,
		FMath::Abs(InputActionValue.Y) > LowerThreshold ? InputActionValue.Y : 0.0f);
}

IMC_Default中的手柄Move输入加上死区修改器

动画蓝图

MotionSpeed曲线控制动画播放速率AnimPlayRate

C++ 复制代码
	//动画播放速率
	UPROPERTY(BlueprintReadWrite, Category = "AnimPlayRate")
	float AnimPlaySpeed = 1.0f;
	UPROPERTY(BlueprintReadWrite, Category = "Curve")
	FName SpeedCurveName = FName("MotionSpeed");
C++ 复制代码
void UBaseAnimInstance::UpdateEssentialData()
{
	//Sequence 0:	RotationMode
	if (AsBaseController != nullptr)
	{
		RotationMode = AsBaseController->CurrentRotationMode;
	}

	if (MovementComponent != nullptr)
	{
		//Sequence 1:	bIsMoving	LastVelocityRotation
		const FVector Velocity = MovementComponent->Velocity;
		GroundSpeed = Velocity.Size2D();
		bIsMoving = GroundSpeed > 1.0f;
		if (bIsMoving)
		{
			LastVelocityRotation = Velocity.Rotation();
		}

		//Sequence 2:	bHasInput	LastInputRotation
		bHasInput = MovementComponent->GetAnalogInputModifier() > 0.0f;
		if (bHasInput)
		{
			LastInputRotation = MovementComponent->GetLastInputVector().Rotation();
		}
	}

	//Sequence 3:	bIsStrafing
	if (AsBaseController != nullptr)
	{
		bIsStrafing = RotationMode != ERotationMode::Rotating;
	}

	//Sequence 4:	AnimPlaySpeed
	const double SpeedRatio = UKismetMathLibrary::SafeDivide(
		GroundSpeed,
		GetCurveValue(SpeedCurveName));
	AnimPlaySpeed = static_cast<float>(
		FMath::Clamp(SpeedRatio, 0.8, 1.0)
		);
}

Start State的序列播放器PlayRate绑定AnimPlaySpeed参数传入,并且设置播放起点为0.1(防止起步动画开头顿一下)

动画序列添加MotionSpeed曲线

效果

Part10 解决Walk转身起步时肩膀卡顿问题

复现条件:Walk转身起步时突然摇杆往同一侧推到底

角色蓝图

区分一下当前是否是手柄输入

C++ 复制代码
	//当前为手柄输入
	UPROPERTY(BlueprintReadWrite, Category = "Input")
	bool bIsGamepadInput = false;

InputGraph.AnyKey

C++ 复制代码
	//当前为手柄输入
	UPROPERTY(BlueprintReadWrite, Category = "Input")
	bool bIsGamepadInput = false;
C++ 复制代码
void UBaseAnimInstance::UpdateEssentialData()
{
	//Sequence 0:	RotationMode
	if (AsBaseController != nullptr)
	{
		RotationMode = AsBaseController->CurrentRotationMode;
		bIsGamepadInput = AsBaseController->bIsGamepadInput;
	}

如果是手柄输入,只用90度的Start动画

效果

解决突然45度转向时的角色突然转向

原因:前向Start动画序列没有RotateAlpha信息

在用到的前向Start动画序列中的rotatealpha加一小段

加上StrideWarping节点

优化滑步

开启Clamp Result,勾上InterpResult可以消除起步时的warping导致的掰腿卡顿现象

Part11 Cycle和同步组的设置

对需要用到的Cycle Walk和Cycle Run 添加左右脚标记、MotionSpeed曲线修改器

添加左右脚标记详见:动画序列添加左右脚标记、RotateAlpha曲线修改器

MotionSpeed曲线修改器详见:动画序列添加MotionSpeed曲线

CT_CycleState

Cycle State

Cycle->Idle的跳转条件优先级设置低一级,防止出现直接跳转为Idle

设置同步组

Cycle

Start

Start->Cycle的过渡时间改为0.5s,确保同步组正确匹配

效果

Part12 改善步态切换和回转运动效果

待办

Part13 Rotating模式下的原地转身

TurnInPlace

动画蓝图

UpdateCharacterRotation.NoMovingRotation

也就是不移动时的旋转逻辑中加入Rotating模式下的原地转身:

C++ 复制代码
	//旋转模式下的原地转身
	UPROPERTY(BlueprintReadWrite, Category = "CharacterState")
	bool bIsShouldTurnInPlace = false;
	/原地转身的角度
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation")
	double TurnInPlaceAngle = 0.0;
C++ 复制代码
	//Rotating模式下,计算原地转身State需要的数据
	UFUNCTION(BlueprintCallable, Category = "CharacterRotation", meta = (HideSelfPin = "true"))
	void UpdateStateData_TurnInPlace();
C++ 复制代码
void UBaseAnimInstance::UpdateStateData_TurnInPlace()
{
	bShouldTurnInPlace_Rotating = false;

	if (!IsValid(AsBaseController) || GroundGait != EGroundGait::Idle)
	{
		return;
	}

	FRotator TargetRotation = (AsBaseController->bShouldAim) ? AsBaseController->GetControlRotation() : PrimaryRotation;


	TurnInPlaceAngle_Rotating = UKismetMathLibrary::NormalizedDeltaRotator(
		TargetRotation,
		AsBaseController->GetActorRotation()
		).Yaw;

	bShouldTurnInPlace_Rotating = FMath::Abs(TurnInPlaceAngle_Rotating) > 60.0;
}

TurnInPlace State

紫色:WantToTurnInPlace
把原先的Cycle->Start的跳转条件WantToStop改为连到Cycle->Stop,否则这里会出现bug:

当角色进入CycleState,这时候松开移动输入键会进入StartState,而StartState相关联的AnimSetup_Start函数会更新PrimaryRotation,导致TurnInPlaceAngle = PrimaryRotation-Actor.Rotation > 60,触发ShouldTurnInPlace==true而进入TurnInPlaceState

CT_TurnInPlaceState

Part14 解决TurnInPlaceState播放动画RootMotion不起作用

原因:动画蓝图的默认设置中RootMotion是仅蒙太奇启用

因此需要让原地转身动画在旋转期间回调动画通知对RootMotion进行设置:

Begin------设置RootMotion From Everything

End------恢复为RootMotion From Montages Only

Tick------如果有输入,就立刻恢复为RootMotion From Montages Only,从而打断原地转身动画

在原地转身动画资产中添加该AnimNotifyState(最好是在root的z轴旋转不再变化时结束):

效果

Part15 键盘输入时转身Start对步态切换的限制

转身Start的时候切换步态的动作衔接问题:

Skeleton中添加DisableSwitchGroundGait曲线

在Skeleton中添加曲线DisableSwitchGroundGait,只有当DisableSwitchGroundGait==0的时候才能够切换步态

利用RotateAlpha曲线来赋值DisableSwitchGroundGait曲线

因为是StartState出现的步态切换动作衔接问题

因此在StartState中添加ModifyCurve节点,并且用转身起步动画资产中的RotateAlpha曲线来赋值DisableSwitchGroundGait曲线:

RotateAlpha有值的时候禁止切换步态,即DisableSwitchGroundGait=1

InputGraph.GaitAction:只有DisableSwitchGroundGait==0的时候才能切换步态

效果

解决键盘切换步态为Walk时手柄无法自动切回Run

InputGraph.AnyKey:手柄输入时设置GroundGait为Run

或者在IMC_Default.IA_Walk中添加手柄切换步态的按键

Part16 CycleState的步态过渡动画------垫步

对步态过渡动画添加左右脚标记、MotionSpeed曲线

添加左右脚标记详见:动画序列添加左右脚标记、RotateAlpha曲线修改器

MotionSpeed曲线修改器详见:动画序列添加MotionSpeed曲线

CT_GaitTransition

Cycle动画状态机重构

CycleStateWithGaitTransition

跳转条件:

Cycle_Walk和Cycle_Run

Cycle_WalkToRun和Cycle_RunToWalk

设置SequencePlayer的起始播放时间

步态过渡动画的前后帧片段较长,因此需要根据MotionSpeed曲线值的变化,设置他们State的SequencePlayer的起始播放时间

Cycle_WalkToRun

Cycle_RunToWalk

删去步态过渡动画的后续冗余帧片段

不能删去前序动画帧,因为状态之间的crossfade需要前序动画帧才能比较完美地融合

同样地,可以参考motionspeed曲线,也就是删去motionspeed已经稳定的帧片段

设置同步组

删去SequencePlayer起始播放时间前面的左右脚标记点,可以根据实际表现保留最近的那一个标记

(并注意这个时间点是哪只脚抬起而决定是否要在选择器表CT_CycleGaitTransition中修改资产的条件)

将WalkToRun和RunToWalk的SequencePlayer的同步组都设置为Transition Leader

限制步态切换时机

骨骼添加DisableSwitchGroundGait曲线详见:Skeleton添加DisableSwitchGroundGait曲线

根据MotionSpeed曲线手动设置DisableSwitchGroundGait曲线(稍微比MotionSpeed曲线滞后一点)

WalkToRun

RunToWalk

加一个跳转条件保证:只有不在禁止步态切换期间动画 、且步态还没切换到目标步态 才会切过去,并且该条件的优先级需要低一档

效果

Part17 Strafing模式的原地转身

先全部取消rootmotion,添加TurnRotationAnimModifier修改器

TurnRotationAnimModifier修改器

为了和Rotating下的TurnInPlace区分,将之前的加上后缀_Rotating

CT_TurnInPlace_Montage

角色蓝图基类

C++ 复制代码
	UPROPERTY(BlueprintReadWrite, Category = "Input")
	bool bShouldAim = false;

动画蓝图

UpdateCharacterRotation.NoMovingRotation

C++ 复制代码
	//蒙太奇原地转身角度
	UPROPERTY(BlueprintReadWrite, Category = "TurnInPlace_Montage")
	float TurnInPlaceAngle_Montage = 0.0;

	//限制TurnInPlace的播放速度
	UPROPERTY(BlueprintReadWrite, Category = "TurnInPlace_Montage")
	float ScaledPlayRate_Montage = 1.0f;
	//限制TurnInPlace的旋转修正倍率
	UPROPERTY(BlueprintReadWrite, Category = "TurnInPlace_Montage")
	float TurnInPlaceAngleModifier_Montage;
C++ 复制代码
	//播放原地转身动画蒙太奇并应用旋转曲线控制实际角色转身角度
	UFUNCTION(BlueprintCallable, Category = "CharacterRotation", meta = (HideSelfPin = "true"))
	void TurnInPlace_Montage(TSoftObjectPtr<UChooserTable> CT_TurnInPlace_Montage);
C++ 复制代码
void UBaseAnimInstance::TurnInPlace_Montage(TSoftObjectPtr<UChooserTable> CT_TurnInPlace_Montage)
{
	static const FName EnableTurnInPlaceCurveName(TEXT("EnableTurnInPlace"));
	static const FName RotationAmountCurveName(TEXT("RotationAmount"));
	static const FName SlotName(TEXT("TurnInPlace_Montage"));

	// Sequence Then 0: Chooser 同时更新 ScaledPlayRate,再使用该值播放蒙太奇。
	if (GetCurveValue(EnableTurnInPlaceCurveName) == 1.0f && AsBaseController->bShouldAim)
	{
		TurnInPlaceAngle_Montage = UKismetMathLibrary::NormalizedDeltaRotator(
			AsBaseController->GetControlRotation(), AsBaseController->GetActorRotation()).Yaw;

		if (FMath::Abs(TurnInPlaceAngle_Montage) > 60.0f)
		{
			if (UChooserTable* ChooserTable = CT_TurnInPlace_Montage.LoadSynchronous())
			{
				UAnimSequenceBase* Animation = Cast<UAnimSequenceBase>(
					UChooserFunctionLibrary::EvaluateChooser(
						this, 
						ChooserTable, 
						UAnimSequenceBase::StaticClass()));
				if (IsValid(Animation) && !IsPlayingSlotAnimation(Animation, SlotName))
				{
					PlaySlotAnimationAsDynamicMontage(
						Animation, 
						SlotName,
						0.2f, 
						0.25f, 
						ScaledPlayRate_Montage, 
						1, 
						0.0f, 
						0.0f);

					const float AnimationAngle = FMath::Sign(TurnInPlaceAngle_Montage)
						* ( (FMath::Abs(TurnInPlaceAngle_Montage) < 130.0f ) ? 90.0f : 180.0f);
					//	(实际旋转角度 / 动画的旋转角度) * 播放速率 = 旋转倍率 * 播放速率 = 修正后的旋转倍率
					TurnInPlaceAngleModifier_Montage = (TurnInPlaceAngle_Montage / AnimationAngle) * ScaledPlayRate_Montage;
				}
			}
		}
	}

	// Sequence Then 1 独立执行,不受上面的动画触发条件影响。
	const float RotationAmount = GetCurveValue(RotationAmountCurveName);
	if (FMath::Abs(RotationAmount) > 0.0f)
	{
      //乘上DeltaTime用来适应不同帧率
		AsBaseController->AddActorWorldRotation(FRotator(
			0.0f, 
			RotationAmount * 45.0f * DeltaTimeX, 
			0.0f));
	}
}

回到CT_TurnInPlaceStrafing,返回ScaledPlayRate用来控制蒙太奇的播放速率

状态机

IdleState

  • 让EnableTurnInPlace=1,也就是只有在Idle状态才会触发Strafing模式的原地转身
  • 添加专属Slot
  • 限制TurnInPlace的旋转修正倍率TurnInPlaceAngleModifier赋值给控制角色旋转的参数RotationAmount

效果

均能正确转向

Aim时两种旋转模式共享原地转身逻辑

把这个Aim时播放原地转身蒙太奇的节点也放在Rotating模式下

效果

Part18 Strafing模式下的动画蓝图配置

选择器表

动画蓝图

C++ 复制代码
	//Strafing模式下的人物方向
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation|Strafing")
	ECardinalDirection VelocityCardinalDirection = ECardinalDirection::Forward;

CT_StartState

添加左右脚标记、MotionSpeed曲线

添加左右脚标记详见:动画序列添加左右脚标记、RotateAlpha曲线修改器

MotionSpeed曲线修改器详见:动画序列添加MotionSpeed曲线

Walk

Run

CT_CycleState

添加左右脚标记、MotionSpeed曲线

添加左右脚标记详见:动画序列添加左右脚标记、RotateAlpha曲线修改器

MotionSpeed曲线修改器详见:动画序列添加MotionSpeed曲线

Walk

Run

CT_StopState

添加Distance曲线

添加Distance曲线操作详见:【动画序列添加Distance曲线修改器】

Walk

Run

运动逻辑

C++ 复制代码
	//Strafing模式下的Locomotion角度
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation|Strafing")
	float VelocityLocomotionAngle = 0.0f;
C++ 复制代码
	//根据移动角度选择方向,并扩大当前前后方向的死区。
	UFUNCTION(BlueprintPure, Category = "CharacterRotation|Strafing", meta = (HideSelfPin = "true"))
	ECardinalDirection SelectCardinalDirection(
		ECardinalDirection CurrentDirection,
		float CurrentAngle,
		float DeadZone,
		bool bUseCurrentDirection) const;
C++ 复制代码
ECardinalDirection UBaseAnimInstance::SelectCardinalDirection(
	const ECardinalDirection CurrentDirection,
	const float CurrentAngle,
	const float DeadZone,
	const bool bUseCurrentDirection) const
{
	const float AbsoluteAngle = FMath::Abs(CurrentAngle);
	float FwdDeadZone = DeadZone;
	float BwdDeadZone = DeadZone;

	if (bUseCurrentDirection)
	{
		switch (CurrentDirection)
		{
		case ECardinalDirection::Backward:
			BwdDeadZone *= 2.0f;
			break;
		case ECardinalDirection::Forward:
			FwdDeadZone *= 2.0f;
			break;
		default:
			break;
		}
	}

	//[-45 - 死区, 45 + 死区] 
	if (AbsoluteAngle <= 45.0f + FwdDeadZone)
	{
		return ECardinalDirection::Forward;
	}

	//[-∞, -135 - 死区]∪[130 + 死区, ∞]
	if (AbsoluteAngle >= 135.0f - BwdDeadZone)
	{
		return ECardinalDirection::Backward;
	}

	//剩下的<0就是左,>0就是右
	return CurrentAngle > 0.0f ? ECardinalDirection::Right : ECardinalDirection::Left;
}

在UpdateEssentialData中设置VelocityLocomotionAngle和VelocityCardinalDirection

C++ 复制代码
void UBaseAnimInstance::UpdateEssentialData()
{
	//Sequence 0:	RotationMode
		//Sequence 1:	bIsMoving	LastVelocityRotation
		//Sequence 2:	bHasInput	LastInputRotation


	//Sequence 3:	bIsStrafing
	if (AsBaseController != nullptr)
	{
		bIsStrafing = RotationMode != ERotationMode::Rotating;

		VelocityLocomotionAngle = UKismetAnimationLibrary::CalculateDirection(MovementComponent->Velocity, AsBaseController->GetActorRotation());

		VelocityCardinalDirection = SelectCardinalDirection(VelocityCardinalDirection, VelocityLocomotionAngle, 10, bIsMoving);
	}

	//Sequence 4:	AnimPlaySpeed
}

死区取10,VelocityCardinalDirection分布如图

添加OrientationWarping

只在StartState和CycleState中添加:Start、Cycle_Walk、Cycle_Run、Cycle_WalkToRun、Cycle_RunToWalk

效果

Part19 切换旋转模式时的伪Spine效果

角色蓝图

角色蓝图新增标志位bIsSwitchToRotating

C++ 复制代码
	//是否切换到Rotating模式
	UPROPERTY(BlueprintReadWrite, Category = "RotationMode")
	bool bIsSwitchToRotating = false;

IA_Strafe中切换到Rotating时为true,下一tick再重置为false

动画蓝图

AnimSetup_Start中,记录Start状态时的旋转模式RotationMode_StartState

C++ 复制代码
	//Start状态下的RotationMode
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation|StartState")
	ERotationMode RotationMode_StartState = ERotationMode::Rotating;

AnimUpdate_Start中,

在Set Sequence之后声明bRotatingStartSwitchToStrafingCycle变量:Start状态时是Rotating模式&当前Strafing模式

C++ 复制代码
	//快速切换到Cycle
	UPROPERTY(BlueprintReadWrite, Category = "CharacterState")
	bool bRotatingStartSwitchToStrafingCycle;

AnimSetup_Cycle中重置bRotatingStartSwitchToStrafingCycle为false

状态机Start<->Cycle跳转条件

Cycle->Start:bIsSwitchToRotating==true & 当前状态已完全过渡到Cycle&不是前进方向(否则前进时切换到Rotating也会触发TurnInPlace_Montage)

Start->Cycle:bRotatingStartSwitchToStrafingCycle

解决start状态时从strafing切换到rotating的姿势过渡生硬

效果

Part20 Jump状态机拆分

CT_JumpStartState

动画蓝图UpdateEssentialData

C++ 复制代码
	//角色胶囊体底部到地面的高度
	UPROPERTY(BlueprintReadWrite, Category = "CharacterMovement")
	float DistanceToFloor;
C++ 复制代码
void UBaseAnimInstance::UpdateEssentialData()
{
	//Sequence 0:	RotationMode

		//Sequence 1:	bIsMoving	LastVelocityRotation


		//Sequence 2:	bHasInput	LastInputRotation


	//Sequence 3:	bIsStrafing


	//Sequence 4:	AnimPlaySpeed


	//Sequence 5:	DistanceToFloor(仅在下降时更新,其余情况保留原值)
	if (AsBaseController != nullptr && MovementComponent != nullptr
		&& MovementComponent->MovementMode == MOVE_Falling
		&& MovementComponent->Velocity.Z > -20.0f)
	{
		FFindFloorResult FloorResult;
		MovementComponent->ComputeFloorDist(
			AsBaseController->GetActorLocation(), 0.0f, 10000.0f, FloorResult, 35.0f);
		DistanceToFloor = FloorResult.FloorDist;
	}
}

Jump状态机

JumpEntry / FallLand ->JumpStart:WantToJumpStart

FallLoop / JumpStart -> FallLand:WantToFallLand

FallLand -> ToIdle:(Inertiaization)

剩余条件:Automatic

JumpState

FallLoopState

勾选LoopAnimation

FallLandState

FallLand_Light资产

添加ANS_RootMotionMode,并设置bInterruptRoorMotion=false,也就是不能被移动输入打断:

注意:考虑到原地转身动画需要被移动输入打断,把CT_TurnInPlaceState_Rotating用到资产中的ANS_RootMotionMode设置bInterruptRoorMotion=true

FallLand_Heavy资产

设置为Additive Anim:

让跳跃落地动画播完才进行下一次跳跃

只有当没有在播放RootMotion时才会触发Jump(这个判定条件需要根据具体项目来,以防止对于其他RootMotion在播放时不能跳跃)

效果

Part999_1 移动时的身体倾斜

参考Lyra:www.cnblogs.com/eanojiang/p/19364383#10-lean-animations-intermediate

Part999_2 不同方向起步的时机限制