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

Git仓库:github.com/EanoJiang/ThirdPersonControl
演示视频:www.bilibili.com/video/BV19CpX6AEXn

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

// 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 设置角色初始步态和旋转模式

用到的变量及默认值

	//步态
	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

1788488074605

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

1788168092090

void ABaseController::SetMainAnimInstance()
{
	MainAnimInstance = GetMesh()->GetAnimInstance();
}
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;
}
void ABaseController::SetRotationMode(ERotationMode NewRotationMode)
{
	if (CurrentRotationMode == NewRotationMode)
	{
		return;
	}

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

Part03 更新步态数据+状态机

动画蓝图

1788244861505

1788244875571

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();
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();
	}

}

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

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来实现

状态机

1788259131256

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

1788259176495

较暗的条件是勾选了Disabled

1788259160694

Part04 移动停止+选择器表

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

	UPROPERTY(BlueprintReadWrite, Category = "CharacterState")
	bool bIsLeftFootUp = false;
	//更新移动方向上的领先脚
	UFUNCTION(BlueprintCallable, Category = "UpdateData")
	void UpdateLeftFootUp();
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曲线修改器

1788252857284

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

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

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

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

CT_StopState

1788770297642

Stop State

1788487681799

AnimSetup_Stop

1788493445480

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

	//根据 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;
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

1788490100942

	UPROPERTY(BlueprintReadWrite, Category = "Curve")
	FName DistanceCurveName = FName("Distance");

效果

1788490445874

Part05 ALS的旋转框架

角色蓝图

InputGraph.StrafeAction

1788512581664

动画蓝图

Event BlueprintUpdateAnimation

1788512535827

1788512723146

MovingRotation

1788512728127

	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;
	//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);
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

1788512736385

效果

1788513376408

Part06 Start资产选择

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

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

1788514721937

1788514522495

确保RotateAlpha曲线从1->0
1代表有转向

1788514984711

CT_StartState.Run.RunRotating

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

1788516152927

选择器表Debug

1788768839725

Start State

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

1788748167289

AnimSetup_Start

1788748558731

AnimUpdate_Start

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

1788748551013

效果

1788748754207

Part07 曲线过渡和伪回转运动

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

引入DeadBlending节点

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

1788749885721

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

1788750789389

效果

1788765420733

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

1788765673634

对需要用到的Start WalkRotating资产进行和Start RunRotating同样的操作(添加左右脚标记、RotateAlpha曲线修改器)
1788766399528
操作详见【动画序列添加左右脚标记、RotateAlpha曲线修改器】

CT_StartState的步态切换

1788769200565

CT_StartState.Walk.WalkRotating

1788769338166

CT_StopState的步态切换

对需要用到的Stop WalkRotating资产进行和Stop RunRotating同样的操作(添加Distance修改器)
1788769493488
操作详见【动画序列添加Distance曲线修改器】

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

1788773310398

效果

1788774311595

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

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

角色蓝图

引入死区

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

1788839013796

	//根据阈值过滤手柄输入的 X/Y 轴
	UFUNCTION(BlueprintPure, Category = "Input", meta = (HideSelfPin = "true"))
	FVector2D FilterGamepadValue(const FVector2D& InputActionValue, float LowerThreshold) const;
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输入加上死区修改器

1788785817420

动画蓝图

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

	//动画播放速率
	UPROPERTY(BlueprintReadWrite, Category = "AnimPlayRate")
	float AnimPlaySpeed = 1.0f;
	UPROPERTY(BlueprintReadWrite, Category = "Curve")
	FName SpeedCurveName = FName("MotionSpeed");
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(防止起步动画开头顿一下)

1788840327011

动画序列添加MotionSpeed曲线

1788838687400

1788838735056

效果

1788853907749

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

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

角色蓝图

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

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

InputGraph.AnyKey

1788858563173

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

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

1788861722064

1788861749549

效果

1788866650314

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

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

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

1788867712025

加上StrideWarping节点

优化滑步

1788869441095

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

1789977714707

Part11 Cycle和同步组的设置

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

17889435504171788943559403

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

CT_CycleState

1788943538739

1788943585454

1788943575618

Cycle State

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

1789373150919

1788943969782

1788943983642

设置同步组

Cycle

1788944051580

Start

1788944080340

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

1789373079029

效果

1788948157586

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

待办

Part13 Rotating模式下的原地转身

TurnInPlace

动画蓝图

UpdateCharacterRotation.NoMovingRotation

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

1789530446453

	//旋转模式下的原地转身
	UPROPERTY(BlueprintReadWrite, Category = "CharacterState")
	bool bIsShouldTurnInPlace = false;
	/原地转身的角度
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation")
	double TurnInPlaceAngle = 0.0;
	//Rotating模式下,计算原地转身State需要的数据
	UFUNCTION(BlueprintCallable, Category = "CharacterRotation", meta = (HideSelfPin = "true"))
	void UpdateStateData_TurnInPlace();
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

1789373026727

紫色:WantToTurnInPlaceimg

把原先的Cycle->Start的跳转条件WantToStop改为连到Cycle->Stop,否则这里会出现bug:
当角色进入CycleState,这时候松开移动输入键会进入StartState,而StartState相关联的AnimSetup_Start函数会更新PrimaryRotation,导致TurnInPlaceAngle = PrimaryRotation-Actor.Rotation > 60,触发ShouldTurnInPlace==true而进入TurnInPlaceState

CT_TurnInPlaceState

1789044625015

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

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

1789031623684

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

Begin——设置RootMotion From Everything

1789044913067

End——恢复为RootMotion From Montages Only

1789044919400

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

1789044923580

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

1789045157828

效果

1789045568124

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

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

Skeleton中添加DisableSwitchGroundGait曲线

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

1789123838351

利用RotateAlpha曲线来赋值DisableSwitchGroundGait曲线

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

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

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

1789123963256

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

1789124007972

效果

1789124866728

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

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

1789124821663

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

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

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

17893560462691789356195455

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

CT_GaitTransition

1789356442569

Cycle动画状态机重构

CycleStateWithGaitTransition

1789379868757

1789380458465

跳转条件:

Cycle_Walk和Cycle_Run

1789380379935

Cycle_WalkToRun和Cycle_RunToWalk

1789380555569

1789380536097

设置SequencePlayer的起始播放时间

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

Cycle_WalkToRun

1789376247245

1789380652796

Cycle_RunToWalk

1789375789131

1789380659778

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

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

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

1789386962677

设置同步组

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

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

1789387534151

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

1789370730920

限制步态切换时机

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

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

WalkToRun

1789377810536

RunToWalk

1789377826646

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

效果

1789392023671

Part17 Strafing模式的原地转身

1789444311841

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

1789444404041

TurnRotationAnimModifier修改器
1789454928667

1789455667005

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

CT_TurnInPlace_Montage

1789529241219

角色蓝图基类

	UPROPERTY(BlueprintReadWrite, Category = "Input")
	bool bShouldAim = false;

1789118088241

1789118069930

动画蓝图

UpdateCharacterRotation.NoMovingRotation

1789529980164

	//蒙太奇原地转身角度
	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;
	//播放原地转身动画蒙太奇并应用旋转曲线控制实际角色转身角度
	UFUNCTION(BlueprintCallable, Category = "CharacterRotation", meta = (HideSelfPin = "true"))
	void TurnInPlace_Montage(TSoftObjectPtr<UChooserTable> CT_TurnInPlace_Montage);
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用来控制蒙太奇的播放速率

1789530103756

状态机

IdleState

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

1789530153810

效果

均能正确转向

1789478504755

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

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

1789530651233

效果

1789530959502

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

选择器表

动画蓝图

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

CT_StartState

添加左右脚标记、MotionSpeed曲线

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

Walk

1789544372485

1789546468825

1789546537328

Run

1789546642759

1789546676143

1789546682804

CT_CycleState

添加左右脚标记、MotionSpeed曲线

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

Walk

1789547314859

1789547106506

1789547179126

Run

1789547302726

1789547245925

1789547254203

CT_StopState

添加Distance曲线

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

Walk

1789547464014

1789547440994

1789548112377

Run

1789548158796

1789548095561

1789548232965

运动逻辑

	//Strafing模式下的Locomotion角度
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation|Strafing")
	float VelocityLocomotionAngle = 0.0f;
	//根据移动角度选择方向,并扩大当前前后方向的死区。
	UFUNCTION(BlueprintPure, Category = "CharacterRotation|Strafing", meta = (HideSelfPin = "true"))
	ECardinalDirection SelectCardinalDirection(
		ECardinalDirection CurrentDirection,
		float CurrentAngle,
		float DeadZone,
		bool bUseCurrentDirection) const;
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

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分布如图

1789638059931

添加OrientationWarping

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

1789639473920

1789639542072

效果

1789644777892

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

角色蓝图

角色蓝图新增标志位bIsSwitchToRotating

	//是否切换到Rotating模式
	UPROPERTY(BlueprintReadWrite, Category = "RotationMode")
	bool bIsSwitchToRotating = false;

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

1789963387541

动画蓝图

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

	//Start状态下的RotationMode
	UPROPERTY(BlueprintReadWrite, Category = "CharacterRotation|StartState")
	ERotationMode RotationMode_StartState = ERotationMode::Rotating;

1790234684758

AnimUpdate_Start中,

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

	//快速切换到Cycle
	UPROPERTY(BlueprintReadWrite, Category = "CharacterState")
	bool bRotatingStartSwitchToStrafingCycle;

1790235678118

AnimSetup_Cycle中重置bRotatingStartSwitchToStrafingCycle为false

1790235721791

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

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

Start->Cycle:bRotatingStartSwitchToStrafingCycle

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

1790237913103

效果

1790241722251

Part20 Jump状态机拆分

CT_JumpStartState

1791601364731

1791601374742

1791601389132

动画蓝图UpdateEssentialData

	//角色胶囊体底部到地面的高度
	UPROPERTY(BlueprintReadWrite, Category = "CharacterMovement")
	float DistanceToFloor;
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

1791600532566

FallLoop / JumpStart -> FallLand:WantToFallLand

1791600628162

FallLand -> ToIdle:(Inertiaization)

1791600664226

剩余条件:Automatic

JumpState

1791600780889

1791600788525

FallLoopState

勾选LoopAnimation

1791600808150

1791600813674

FallLandState

FallLand_Light资产

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

1791601179690

1791601081359

1791601230481

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

FallLand_Heavy资产

设置为Additive Anim:

1791601107893

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

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

1791601867777

效果

1791602261423

Part999_1 移动时的身体倾斜

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

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

1789380171128

1789380167630

posted @ 2026-10-10 11:25  EanoJiang  阅读(54)  评论(0)    收藏  举报