《UE5_C++多人TPS完整教程》学习笔记44 ——《P45 倾斜与侧向移动(Leaning And Strafing)》
本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P45 倾斜与侧向移动(Leaning And Strafing)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author) Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
文章目录
- P45 倾斜与侧向移动(Leaning And Strafing)
- 45.1 获取瞄准和移动旋转
- 45.2 获取 YawOffset
- 45.3 获取 Leaning
- 45.4 平滑动画
- 45.5 Summary
P45 倾斜与侧向移动(Leaning And Strafing)
本节课我们将使用 C++ 变量关联奔跑混合空间中的坐标值,奔跑混合空间中的垂直轴与水平轴与人物角色的倾斜与侧向移动有关(Strafing,扫射,通常指的是玩家或角色在移动的同时可以进行横向移动或射击,这里笔者翻译成侧向移动)。
45.1 获取瞄准和移动旋转
-
在 Visual Studio 中打开 “
BlasterAnimInstance.h
”,在类构造函数中声明变量 “YawOffset
” 和 “Leaning
”,变量名与奔跑混合空间 “EquippedRun
” 中的水平轴和垂直轴名称保持一致。打开 “BlasterAnimInstance.cpp
”,在函数 “NativeUpdateAnimation()
” 中声明旋转体变量 “AimRotation
”,获取角色瞄准基旋转,并借助 “UE_LOG()
” 函数输出 “AimRotation
” 的 Yaw 值在 “输出日志”(Output Log)中。/*** BlasterAnimInstance.h ***/...UCLASS() class BLASTER_API UBlasterAnimInstance : public UAnimInstance {GENERATED_BODY()...private:...UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;bool bWeaponEquipped; // 是否装备了武器UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;bool bIsCrouched; // 是否在蹲伏/* P45 倾斜与侧向移动(Leaning And Strafing)*/UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;float YawOffset;UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;float Leaning;/* P45 倾斜与侧向移动(Leaning And Strafing)*/ };
/*** BlasterAnimInstance.cpp ***/...// 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画 void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) {...bWeaponEquipped = BlasterCharacter->IsWeaponEquipped(); // 调用 BlasterCharacter 的 IsWeaponEquipped() 函数判断人物角色是否装备了武器bIsCrouched = BlasterCharacter->bIsCrouched; // 访问 BlasterCharacter 的 bIsCrouched 变量值来判断人物角色是否在蹲伏bAiming = BlasterCharacter->IsAiming(); // 调用 BlasterCharacter 的 IsAiming() 函数来判断人物角色是否在瞄准/* P45 倾斜与侧向移动(Leaning And Strafing)*/// GetBaseAimRotation(): https://dev.epicgames.com/documentation/en-us/unreal-engine/BlueprintAPI/Pawn/GetBaseAimRotation?application_version=5.0// Return the aim rotation for the Pawn. // If we have a controller, by default we aim at the player's 'eyes' direction that is by default the Pawn rotation for AI, // and camera (crosshair) rotation for human playersFRotator AimRotation = BlasterCharacter->GetBaseAimRotation(); // 获取角色瞄准基旋转,主要用于控制角色视线方向与鼠标移动方向一致,同时避免角色模型旋转// 若存在控制器(如玩家控制器),则瞄准方向会跟随角色“眼睛”旋转;若为AI控制,则瞄准方向基于角色朝向。UE_LOG(LogTemp, Warning, TEXT("AimRotation Yaw %f: "), AimRotation.Yaw); // 在输出日志(Output Log)中输出 AimRotation 的 Yaw 值/* P45 倾斜与侧向移动(Leaning And Strafing)*/... }
-
编译后,打开关卡 “
BlasterMap
”。在菜单栏的 “窗口” 中选择 “输出日志”(Output Log),可以看到 “输出日志” 窗口被显示。我们将首先测试在服务器上的效果,因此在工具栏点击 “⋮\textbf{\vdots}⋮” 按钮,修改 “玩家数量”(Number of Players) 为 1。
-
点击工具栏的 “播放”(▶)按钮启动运行。移动鼠标使得摄像机视角向左旋转,转动到人物角色左侧再转动到人物角色的正面,可以看到输出日志中的 Yaw 值从 0 一直增大到 180;随后再向右旋转,从正面转动到右侧,可以看到 Yaw 值变为负值,从 -180 一直增大到 -0。这说明 “
GetBaseAimRotation()
” 获取的是一个以人物角色为基准的旋转,也是 摄像机相较于世界场景的 X 轴的旋转。
-
在 “
BlasterAnimInstance.cpp
” 的函数 “NativeUpdateAnimation()
” 中声明旋转体变量 “MovementRotation
”,获取人物角色速度相对于世界场景 X 轴的旋转,并借助 “UE_LOG()
” 函数输出 “MovementRotation
” 的 Yaw 值在 “输出日志”(Output Log)中。/*** BlasterAnimInstance.cpp ***/ .../* P45 倾斜与侧向移动(Leaning And Strafing)*/ #include "Kismet/KismetMathLibrary.h" /* P45 倾斜与侧向移动(Leaning And Strafing)*/...// 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画 void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) {...bWeaponEquipped = BlasterCharacter->IsWeaponEquipped(); // 调用 BlasterCharacter 的 IsWeaponEquipped() 函数判断人物角色是否装备了武器bIsCrouched = BlasterCharacter->bIsCrouched; // 访问 BlasterCharacter 的 bIsCrouched 变量值来判断人物角色是否在蹲伏bAiming = BlasterCharacter->IsAiming(); // 调用 BlasterCharacter 的 IsAiming() 函数来判断人物角色是否在瞄准/* P45 倾斜与侧向移动(Leaning And Strafing)*/// GetBaseAimRotation(): https://dev.epicgames.com/documentation/en-us/unreal-engine/BlueprintAPI/Pawn/GetBaseAimRotation?application_version=5.0// Return the aim rotation for the Pawn. // If we have a controller, by default we aim at the player's 'eyes' direction that is by default the Pawn rotation for AI, // and camera (crosshair) rotation for human playersFRotator AimRotation = BlasterCharacter->GetBaseAimRotation(); // 获取角色瞄准基旋转,主要用于控制角色视线方向与鼠标移动方向一致,同时避免角色模型旋转// UE_LOG(LogTemp, Warning, TEXT("AimRotation Yaw %f: "), AimRotation.Yaw); // 在输出日志(Output Log)中输出 AimRotation 的 Yaw 值FRotator MovementRotation = UKismetMathLibrary::MakeRotFromX(BlasterCharacter->GetVelocity()); // 获取人物角色相对于世界场景 X 轴移动的旋转体变量UE_LOG(LogTemp, Warning, TEXT("MovementRotation Yaw %f: "), MovementRotation.Yaw); // 在输出日志(Output Log)中输出 MovementRotation 的 Yaw 值/* P45 倾斜与侧向移动(Leaning And Strafing)*/... }
-
在人物角色保持静止的情况下,移动鼠标使得摄像机视角旋转,输出日志中的 Yaw 值并不会发生变化。当我们在世界场景的 X 轴方向上移动人物角色时,输出日志中的 Yaw 值为 ±0 或 ± 180;当我们不在世界场景的 X 轴方向上移动人物角色时,输出输出日志中的 Yaw 值为 0 ~ 180 或 -180 ~ -0 中的一个值,这取决于移动的方向。这说明 “
MovementRotation
” 是人物角色移动速度以世界场景 X 轴为基准的旋转。由于 “AimRotation
” 和 “MovementRotation
” 是 “同步”(Sync)的,所以我们可以计算任意时刻两者在 Yaw、Pitch 和 Roll 方向上的的差值。
-
接下来测试被控客户端上的效果,在 “
BlasterAnimInstance.cpp
” 的函数 “NativeUpdateAnimation()
” 中修改代码,只输出我们控制的客户端的 “AimRotation
” 和 “MovementRotation
” 的 Yaw 值在输出日志中。/*** BlasterAnimInstance.cpp ***/...// 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画 void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) {.../* P45 倾斜与侧向移动(Leaning And Strafing)*/// GetBaseAimRotation(): https://dev.epicgames.com/documentation/en-us/unreal-engine/BlueprintAPI/Pawn/GetBaseAimRotation?application_version=5.0// Return the aim rotation for the Pawn. // If we have a controller, by default we aim at the player's 'eyes' direction that is by default the Pawn rotation for AI, // and camera (crosshair) rotation for human playersFRotator AimRotation = BlasterCharacter->GetBaseAimRotation(); // 获取角色瞄准基旋转,主要用于控制角色视线方向与鼠标移动方向一致,同时避免角色模型旋转// 若存在控制器(如玩家控制器),则瞄准方向会跟随角色“眼睛”旋转;若为AI控制,则瞄准方向基于角色朝向。FRotator MovementRotation = UKismetMathLibrary::MakeRotFromX(BlasterCharacter->GetVelocity()); // 获取人物角色相对于世界场景 X 轴移动的旋转体变量if (!BlasterCharacter->HasAuthority()) // 如果是被控客户端{UE_LOG(LogTemp, Warning, TEXT("AimRotation Yaw %f: "), AimRotation.Yaw); // 在输出日志(Output Log)中输出 AimRotation 的 Yaw 值UE_LOG(LogTemp, Warning, TEXT("MovementRotation Yaw %f: "), MovementRotation.Yaw); // 在输出日志(Output Log)中输出 MovementRotation 的 Yaw 值}/* P45 倾斜与侧向移动(Leaning And Strafing)*/ }
-
编译后,在虚幻引擎中修改玩家数量为 2,然后运行。可以看到当我们控制客户端上的人物角色移动时,输出日志中客户端上 “
AimRotation
” 和 “MovementRotation
” 的 Yaw 值会发生变化,这说明虚幻引擎已经自动完成了相关属性的变量复制(如人物角色的速度),因此它们会在被控客户端进行更新。
-
接下来测试非被控客户端的效果,在 “
BlasterAnimInstance.cpp
” 的函数 “NativeUpdateAnimation()
” 中修改代码,只输出我们控制的客户端的 “AimRotation
” 和 “MovementRotation
” 的 Yaw 值在输出日志中。/*** BlasterAnimInstance.cpp ***/...// 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画 void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) {.../* P45 倾斜与侧向移动(Leaning And Strafing)*/// GetBaseAimRotation(): https://dev.epicgames.com/documentation/en-us/unreal-engine/BlueprintAPI/Pawn/GetBaseAimRotation?application_version=5.0// Return the aim rotation for the Pawn. // If we have a controller, by default we aim at the player's 'eyes' direction that is by default the Pawn rotation for AI, // and camera (crosshair) rotation for human playersFRotator AimRotation = BlasterCharacter->GetBaseAimRotation(); // 获取角色瞄准基旋转,主要用于控制角色视线方向与鼠标移动方向一致,同时避免角色模型旋转// 若存在控制器(如玩家控制器),则瞄准方向会跟随角色“眼睛”旋转;若为AI控制,则瞄准方向基于角色朝向。FRotator MovementRotation = UKismetMathLibrary::MakeRotFromX(BlasterCharacter->GetVelocity()); // 获取人物角色相对于世界场景 X 轴移动的旋转体变量// if (!BlasterCharacter->HasAuthority()) // 如果是被控客户端if (!BlasterCharacter->HasAuthority() && !BlasterCharacter->IsLocallyControlled()) { // 如果是非被控客户端UE_LOG(LogTemp, Warning, TEXT("AimRotation Yaw %f: "), AimRotation.Yaw); // 在输出日志(Output Log)中输出 AimRotation 的 Yaw 值UE_LOG(LogTemp, Warning, TEXT("MovementRotation Yaw %f: "), MovementRotation.Yaw); // 在输出日志(Output Log)中输出 MovementRotation 的 Yaw 值}/* P45 倾斜与侧向移动(Leaning And Strafing)*/ }
-
编译后,在虚幻引擎中修改玩家数量为 3,然后运行。可以看到当我们控制其中一个客户端上的人物角色移动时,输出日志中另一个非被控客户端上 “
AimRotation
” 和 “MovementRotation
” 的 Yaw 值会发生变化,这说明它们也会在非被控客户端中进行更新,因此相关属性在所有客户端上都进行了变量复制。
45.2 获取 YawOffset
-
编译后在虚幻引擎中打开动画蓝图 “
BlasterAnimBP
”,然后在 “AnimGraph
” 面板中打开状态机 “Equipped
” 的编辑界面,将 “Idle
” 节点重命名为 “Standing
”。
-
双击该节点进入编辑界面,在蓝图编辑面板中删除之前所有的蓝图节点,然后再右下角资产浏览器中拖拽动画蓝图 “
EquippedRun
” 至面板中生成蓝图节点,将该节点的 “Animation Pose
” 输出引脚与 “输出动画姿势”(Out Animation Pose)节点的 “Result
” 引脚连接。添加 “获取 YawOffset”(Get YawOffset)节点,将该节点的输出引脚与 “EquippedRun
” 节点的 “YawOffset
” 输入引脚连接。
-
编译后进行测试。我们的奔跑混合空间 “
EquippedRun
” 只有在人物角色装备了武器并进行奔跑的情况下才会被应用,否则混合空间 “Unequipped
” 被应用。
-
这里人物角色持枪奔跑时的旋转定向于移动(Orienting rotation to movement),即人物角色方向会与加速度方向一致,人物角色会跟随移动方向旋转,这并不是我们设计人物角色持枪时想要的,我们希望我们的人物角色在侧向移动的同时始终面向前方(Stay facing forward),因此我们需要在枪战功能组件源文件 “
CombatComponent.cpp
” 的 “EquipWeapon()
” 函数中设置人物角色移动组件的布尔值变量 “bOrientRotationToMovement
” 为 “false
”,禁止人物角色持枪时朝向其运动加速度的方向;并设置 “bUseControllerRotationYaw
” 为 “true
”,使得人物角色持枪时的 Yaw(偏航,左右转向) 旋转将始终与控制器的旋转保持一致。/*** CombatComponent.cpp ***/// Fill out your copyright notice in the Description page of Project Settings.#include "CombatComponent.h" // 原来自动生成的代码是 #include "BlasterComponents/CombatComponent.h",这里需要把 "BlasterComponents/" 去掉,否则找不到文件 "CombatComponent.h" #include "Blaster/Weapon/Weapon.h" #include "Blaster/Character/BlasterCharacter.h" #include "Engine/SkeletalMeshSocket.h" #include <Net/UnrealNetwork.h>/* P45 倾斜与侧向移动(Leaning And Strafing)*/ #include "GameFramework/CharacterMovementComponent.h" /* P45 倾斜与侧向移动(Leaning And Strafing)*/...void UCombatComponent::EquipWeapon(AWeapon* WeaponToEquip) {...// 设置武器所属并保证武器不能被拾取EquippedWeapon->SetOwner(Character); // 设置武器所属,通过查看 SetOwner() 的定义,然后查看定义中 Owner 的声明,可知该操作会被复制// EquippedWeapon->ShowPickupWidget(false); // 设置拾取组件不可见,被装备的武器将不能再被拾取,该操作不会被复制// EquippedWeapon->GetAreaSphere()->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 禁用武器球体碰撞/* P45 倾斜与侧向移动(Leaning And Strafing)*/Character->GetCharacterMovement()->bOrientRotationToMovement = false; // 禁止人物角色持枪时朝向其加速度/移动的方向Character->bUseControllerRotationYaw = true; // 人物角色持枪时的 Yaw(偏航,左右转向) 旋转将始终与控制器的旋转保持一致/* P45 倾斜与侧向移动(Leaning And Strafing)*/ }...
概述
角色的旋转可以分为两种情景,其一是受我们角色的加速度方向控制其方向,另一种是受其 Controller 控制,方向与 Controller 方向一致。
先说结论
我们只需更改以下5个参数即可。
“bOrientRotationToMovement
”:true 时角色方向与加速度和速度方向一致。
“bUseControllerDesiredRotation
”:true 时角色方向与控制器朝向一致。
“bUseControllerRotationPitch
”、“bUseControllerRotationYaw
”、“bUserControllerRotationRoll
”:当控制器 Rotation 更改时,根据这三个 bool 修改角色Pitch、Yaw、Roll值。这三个 bool 不能与 “bOrientRotationToMovement
” 同时为true,会出现角色抽搐问题。
—— 《虚幻引擎中单机游戏下角色的旋转逻辑》
-
在 “
BlasterAnimInstance.cpp
” 中借助标准化函数 “NormalizedDeltaRotator()
” 获取 “YawOffset
” 的值,它是 “MovementRotation
” 和 “AimRotation
” 在 Yaw 方向上的差值,注意。这里必须借助标准化函数计算差值,不能直接将两者在 Yaw 方向上的值进行相减,防止在-180 和 180 的过渡点时 “YawOffset
” 的值会发生突变。/*** BlasterAnimInstance.cpp ***/...// 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画 void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) {.../* P45 倾斜与侧向移动(Leaning And Strafing)*/// GetBaseAimRotation(): https://dev.epicgames.com/documentation/en-us/unreal-engine/BlueprintAPI/Pawn/GetBaseAimRotation?application_version=5.0// Return the aim rotation for the Pawn. // If we have a controller, by default we aim at the player's 'eyes' direction that is by default the Pawn rotation for AI, // and camera (crosshair) rotation for human playersFRotator AimRotation = BlasterCharacter->GetBaseAimRotation(); // 获取角色瞄准基旋转,主要用于控制角色视线方向与鼠标移动方向一致,同时避免角色模型旋转// 若存在控制器(如玩家控制器),则瞄准方向会跟随角色“眼睛”旋转;若为AI控制,则瞄准方向基于角色朝向。FRotator MovementRotation = UKismetMathLibrary::MakeRotFromX(BlasterCharacter->GetVelocity()); // 获取人物角色相对于世界场景 X 轴移动的旋转体变量// if (!BlasterCharacter->HasAuthority()) // 如果是被控客户端// if (!BlasterCharacter->HasAuthority() && !BlasterCharacter->IsLocallyControlled()) { // 如果是非被控客户端// UE_LOG(LogTemp, Warning, TEXT("AimRotation Yaw %f: "), AimRotation.Yaw); // 在输出日志(Output Log)中输出 AimRotation 的 Yaw 值// UE_LOG(LogTemp, Warning, TEXT("MovementRotation Yaw %f: "), MovementRotation.Yaw); // 在输出日志(Output Log)中输出 MovementRotation 的 Yaw 值// }YawOffset = UKismetMathLibrary::NormalizedDeltaRotator(MovementRotation, AimRotation).Yaw; // 标准化获取 MovementRotation 和 AimRotation 在 Yaw 方向的差值/* P45 倾斜与侧向移动(Leaning And Strafing)*/ }
函数用途
“NormalizedDeltaRotator
” 函数用于计算两个旋转角度之间的差异,并将结果进行标准化处理。标准化后的旋转角度范围在 [-180,180] 之间,可以方便地用于角度差值的计算和比较。
函数工作方式
“NormalizedDeltaRotator
” 函数的工作方式如下:- 首先,函数接受两个 FRotator 类型的参数 A 和 B,分别表示两个旋转角度。
- 函数计算A和B之间的旋转差值,得到一个新的 FRotator 类型的结果。计算方法如下:
- 首先,将 A 和 B 的 Yaw、Pitch 和 Roll 分量分别相减,得到新的 Yaw、Pitch和 Roll 分量。
- 然后,将新的 Yaw、Pitch 和 Roll 分量封装为一个新的FRotator对象,作为计算结果返回。
- 接下来,函数对计算结果进行标准化处理,确保旋转角度范围在[-180,180]之间。标准化的方式如下:
- 对 Yaw、Pitch 和 Roll分量分别进行标准化处理,确保它们的取值范围在 [-180, 180]之间。
- 如果某个分量的值超过了 180,将其减去 360,直到值在 [-180, 180] 之间。
- 如果某个分量的值小于 -180,将其加上 360,直到值在 [-180, 180] 之间。
- 最后,将标准化后的 Yaw、Pitch 和 Roll 分量封装为一个新的 FRotator 对象,作为最终的计算结果返回。
—— 百度文库《NormalizedDeltaRotator 函数详解》
-
编译后进行测试。我们操控视口(服务器)中人物角色拾取武器后进行左右移动,可以发现服务器上的人物角色无论是在服务器端还是其他两个客户端,都可以持枪面对前方侧向移动,这说明我们在服务器上改变了人物角色持枪奔跑时的朝向,虚幻引擎已经帮我们将与之相关的属性进行变量复制了。在测试中人物角色持枪奔跑时动画衔接有些抽搐(Kind of jerking),我们将在之后解决这个问题。
-
再次进行测试。我们操控其中一个客户端中的人物角色拾取武器后进行左右移动,在服务器和另一个客户端上可以看到这个客户端中的人物角色持枪面对前方进行侧向移动,但在客户端本地却看不到(下图左下),这说明我们只在服务器改变了改变了人物角色持枪奔跑时的朝向,自己的客户端没有,所有自己的客户端是按移动方向来控制旋转的,而另一个客户端是服务器复制过去的,所以和服务器一样都可以看到。
-
为解决上述问题,在 “
CombatComponent.h
” 中,声明 Repnotify 函数 “OnRep_EquippedWeapon()
”,然后在属性 “EquippedWeapon
” 的 “UPROPERTY
” 宏中使用 “ReplicatedUsing
” 说明符声明指定 Repnotify 函数为 “OnRep_EquippedWeapon()
”;随后,在 “CombatComponent.cpp
” 的 “OnRep_EquippedWeapon()
” 函数中设置人物角色持枪时的朝向。/*** CombatComponent.h ***/...UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class BLASTER_API UCombatComponent : public UActorComponent {GENERATED_BODY()...protected:// Called when the game startsvirtual void BeginPlay() override;void SetAiming(bool bIsAiming);UFUNCTION(Server, Reliable)void ServerSetAiming(bool bIsAiming);/* P45 倾斜与侧向移动(Leaning And Strafing)*/UFUNCTION()void OnRep_EquippedWeapon();/* P45 倾斜与侧向移动(Leaning And Strafing)*/private:class ABlasterCharacter* Character; // 声明人物角色类,避免反复 casting 到 ABlasterCharacter// UPROPERTY(Replicated)// class AWeapon* EquippedWeapon; // 保存当前装备的武器/* P45 倾斜与侧向移动(Leaning And Strafing)*/UPROPERTY(ReplicatedUsing = On)class AWeapon* EquippedWeapon; // 保存当前装备的武器/* P45 倾斜与侧向移动(Leaning And Strafing)*/UPROPERTY(Replicated) // 可复制变量bool bAiming; // 是否在瞄准 };
/*** CombatComponent.cpp ***/‘.../* P45 倾斜与侧向移动(Leaning And Strafing)*/ // 设置人物角色朝向的 Repnotify 函数 void UCombatComponent::OnRep_EquippedWeapon() {if (EquippedWeapon && Character) {Character->GetCharacterMovement()->bOrientRotationToMovement = false; // 禁止人物角色持枪时朝向其加速度/移动的方向Character->bUseControllerRotationYaw = true; // 人物角色持枪时的 Yaw(偏航,左右转向) 旋转将始终与控制器的旋转保持一致} } /* P45 倾斜与侧向移动(Leaning And Strafing)*/
-
编译后进行测试。我们操控其中一个客户端(下图左下)中的人物角色拾取武器后进行左右移动,在服务器和所有的客户端上都可以看到这个客户端中的人物角色持枪面对前方进行侧向移动。
45.3 获取 Leaning
-
在 “
BlasterAnimInstance.h
” 中声明旋转体变量 “CharacterRotationLastFrame
” 和 “CharacterRotation
”,分别表示人物角色上一帧的旋转和当前旋转。在 “BlasterAnimInstance.cpp
” 的 “NativeUpdateAnimation()
” 函数中保存人物角色上一帧旋转,借助 “GetActorRotation
” 函数获取人物角色当前旋转,标准化获取人物角色当前旋转与上一帧旋转的差量 “Delta
”,由于 “Delta
” 的 Yaw 分量值可能会很小,需要除以 “DeltaTime
”(动画更新每一帧的时间)进行缩放,获取目标 Yaw 值 “Target
(也就是 “Leaning
” 的最终目标值)。为了使得动画能够平滑过渡,我们使用 “FInterpTo()
” 实现 “Leaning
” 到 “Target
” 的插值(Interporlation),这样就可以获取动画更新每一帧的时间的插值 “Interp
”,随后借助 “Clamp()
” 限制 “Interp
” 数值范围,如果 “Interp
” 的值超过设定的最小值 -90.0 或最大值 90.0,就将 “Leaning
” 赋值为设定的最小值 -90.0 或最大值 90.0;否则直接将 “Interp
” 赋值给 “Leaning
。/*** BlasterAnimInstance.h ***/...UCLASS() class BLASTER_API UBlasterAnimInstance : public UAnimInstance {GENERATED_BODY().../* P45 倾斜与侧向移动(Leaning And Strafing)*/UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;float YawOffset;UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;float Leaning;FRotator CharacterRotationLastFrame; // 人物角色上一帧的旋转FRotator CharacterRotation; // 人物角色当前的旋转/* P45 倾斜与侧向移动(Leaning And Strafing)*/ };
/*** BlasterAnimInstance.cpp ***/...// 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画 void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) {.../* P45 倾斜与侧向移动(Leaning And Strafing)*/...YawOffset = UKismetMathLibrary::NormalizedDeltaRotator(MovementRotation, AimRotation).Yaw; // 标准化获取 MovementRotation 和 AimRotation 在 Yaw 方向的差值// 获取人物角色的倾斜度CharacterRotationLastFrame = CharacterRotation; // 保存人物角色上一帧的旋转CharacterRotation = BlasterCharacter->GetActorRotation(); // 获取当前人物角色的旋转const FRotator Delta = UKismetMathLibrary::NormalizedDeltaRotator(CharacterRotation, CharacterRotationLastFrame); // 标准化获取人物角色当前旋转与上一帧旋转的差量const float Target = Delta.Yaw / DeltaTime; // Delta 的 Yaw 值可能会很小,需要除以 DeltaTime(动画更新每一帧的时间)进行缩放,获取目标 Yaw 值const float Interp = FMath::FInterpTo(Leaning, Target, DeltaTime, 6.f); // FMath::FInterpTo() 实现 Lean 到 Target 插值(Interporlation),保证平滑过渡,这里最后一个参数 6.f 是插值速度Leaning = FMath::Clamp(Interp, -90.f, 90.f); // FMath::Clamp() 限制数值范围,如果 Interp 的值超过设定的最小值 -90.0 或最大值 90.0,就将 Lean 赋值为设定的最小值或最大值;否则直接将 Interp 赋值给 Lean。/* P45 倾斜与侧向移动(Leaning And Strafing)*/ }
-
在虚幻引擎中,进入 “
Standing
” 状态编辑界面,在蓝图编辑面板中添加 “获取 Leaning”(Get Leaning)节点,将该节点的输出引脚与 “EquippedRun
” 节点的 “Leaning
” 输入引脚连接。
-
编译后进行测试。可以看到当我们无论在服务器还是客户端上操控的人物角色持枪进行奔跑时,如果移动鼠标转动视角,都可以在服务器和所有客户端上看到人物角色身体发生了倾斜。
45.4 平滑动画
-
我们可以在测试中发现人物角色往左侧向移动时,如果立即让其向右侧向移动,两个人物角色的动画之间的衔接会非常不顺畅,处理这个问题的方法就是在持枪奔跑混合空间中使用插值。有两种方法进行插值,第一种直接在 “
EquippedRun
” 混合空间直接设置 “水平坐标”(Horizontal Axis)的插值时间,另一种方法是在 C++ 中进行插值再直接获取 “YawOffset
” 。 -
如果采用第一种方法,需要在虚幻引擎中打开 “
EquippedRun
” 混合空间编辑器,在左侧 “资产详情”(Asset Details)面板的 “Axis Settings
” 选项卡下展开 “水平坐标” 选项,设置 “插值时间”(Interpolation Time) 为 0.2。
-
编译后进行测试,可以发现这种方法可以使得人物角色往左侧向移动与向右侧向移动的动画衔接之间变得平滑,但是当人物角色向后移动时,人物角色会发生抽搐,这是因为在水平坐标中直接设置插值时间后,由于人物角色向后移动的动画播放时 “
YawOffset
” 的值为 -180.0 或 180.0,混合空间就会从 -180.0 平滑移到 180.0,因此人物角色向后移动的会同时播放水平坐标在 -180.0 ~ 180.0 的动画。 -
设置 “插值时间”(Interpolation Time) 为 0.0后,我们采用第二种方法解决上述问题,保证 “
YawOffset
” 从 -180.0 过渡到 180 时不会先增加到 0 再从 0 增加到 180(或从 180.0 过渡到 -180 时不会先减少到 0 再从 0 减少到 -180),而是直接采用从 -180.0 直接到 180 的 “最短路径”(It’ll take shortest path in that rotation going from negative 180 directly to postive 180)。
在 “BlasterAnimInstance.h
” 中声明旋转体变量 “DeltaRotation
”,表示需要进行插值的当前值;在 “BlasterAnimInstance.cpp
” 的 “NativeUpdateAnimation()
” 函数中借助标准化函数 “NormalizedDeltaRotator()
” 获取 “MovementRotation
” 和 “AimRotation
” 的差量,它是旋转体变量 “DeltaRotation
” 插值的目标值,存储在中间旋转体变量 “DeltaRot
” 中;然后借助旋转体插值函数 “RInterpTo
” 在 “DeltaRotation
” 和 “DeltaRot
” 间进行插值以实现平滑过渡,函数返回值赋值给 “DeltaRotation
”,“DeltaRotation
” 在 Yaw 方向上的分量即 “YawOffset
” 的值。/*** BlasterAnimInstance.h ***/...UCLASS() class BLASTER_API UBlasterAnimInstance : public UAnimInstance {GENERATED_BODY().../* P45 倾斜与侧向移动(Leaning And Strafing)*/UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;float YawOffset;UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;float Leaning;FRotator CharacterRotationLastFrame; // 人物角色上一帧的旋转FRotator CharacterRotation; // 人物角色当前的旋转FRotator DeltaRotation; // 旋转体插值的当前值 /* P45 倾斜与侧向移动(Leaning And Strafing)*/ };
/*** BlasterAnimInstance.cpp ***/...// 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画 void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) {.../* P45 倾斜与侧向移动(Leaning And Strafing)*/// 获取侧向移动的偏航值(Offset Yaw for Strafing)FRotator AimRotation = BlasterCharacter->GetBaseAimRotation(); // 获取角色瞄准基旋转,主要用于控制角色视线方向与鼠标移动方向一致,同时避免角色模型旋转// 若存在控制器(如玩家控制器),则瞄准方向会跟随角色“眼睛”旋转;若为 AI 控制,则瞄准方向基于角色朝向。FRotator MovementRotation = UKismetMathLibrary::MakeRotFromX(BlasterCharacter->GetVelocity()); // 获取人物角色相对于世界场景 X 轴移动的旋转体变量// YawOffset = UKismetMathLibrary::NormalizedDeltaRotator(MovementRotation, AimRotation).Yaw; // 标准化获取 MovementRotation 和 AimRotation 在 Yaw 方向的差值FRotator DeltaRot = UKismetMathLibrary::NormalizedDeltaRotator(MovementRotation, AimRotation); // 标准化获取 MovementRotation 和 AimRotation 的差量DeltaRotation = FMath::RInterpTo(DeltaRotation, DeltaRot, DeltaTime, 6.f); // FMath::RInterpTo() 实现旋转体 DeltaRotation 和 DeltaRot 的插值,保证平滑过渡YawOffset = DeltaRotation.Yaw; // YawOffset 即 DeltaRotation 在 Yaw 方向上的分量// 获取人物角色的倾斜度CharacterRotationLastFrame = CharacterRotation; // 保存人物角色上一帧的旋转CharacterRotation = BlasterCharacter->GetActorRotation(); // 获取当前人物角色的旋转const FRotator Delta = UKismetMathLibrary::NormalizedDeltaRotator(CharacterRotation, CharacterRotationLastFrame); // 标准化获取人物角色当前旋转与上一帧旋转的差量const float Target = Delta.Yaw / DeltaTime; // Delta 的 Yaw 值可能会很小,需要除以 DeltaTime(动画更新每一帧的时间)进行缩放,获取目标 Yaw 值const float Interp = FMath::FInterpTo(Leaning, Target, DeltaTime, 6.f); // FMath::FInterpTo() 实现 Lean 到 Target 插值(Interporlation),保证平滑过渡,这里最后一个参数 6.f 是插值速度Leaning = FMath::Clamp(Interp, -90.f, 90.f); // FMath::Clamp() 限制数值范围,如果 Interp 的值超过设定的最小值 -90.0 或最大值 90.0,就将 Lean 赋值为设定的最小值或最大值;否则直接将 Interp 赋值给 Lean。/* P45 倾斜与侧向移动(Leaning And Strafing)*/ }
-
编译后进行测试,可以发现人物角色人物角色往左侧向移动与向右侧向移动的动画衔接很流畅,并且向后移动时不会再发生抽搐。
45.5 Summary
本节课我们实现了人物角色在装备武器状态下奔跑时的倾斜与侧向移动的动画逻辑。首先,在人物角色动画实例类 “UBlasterAnimInstance
” 中声明并计算了两个关键变量:“YawOffset
”(用于控制持枪奔跑侧向移动方向)和 “Leaning
”(用于控制持枪奔跑身体倾斜度)。通过获取角色的瞄准旋转 “AimRotation
” 和移动方向旋转 “MovementRotation
”,并使用标准化旋转差量函数 “NormalizedDeltaRotator()
” 计算它们之间的差量,得到了 “YawOffset
” 的初始值。随后,我们在 “EquippedWeapon
” 状态机的动画蓝图中,将计算好的 “YawOffset
” 节点连接到奔跑混合空间 “EquippedRun
” 节点的对应输入引脚,驱动角色根据移动和瞄准方向播放正确的侧向持枪奔跑动画。
紧接着,我们解决了动画播放的网络同步问题。在战斗组件(UCombatComponent)中,我们将 “EquippedWeapon
” 注册为复制属性,并为其指定了复制通知 Repnotify 函数 “OnRep_EquippedWeapon()
”。在该函数中,我们设置了角色移动组件“UCharacterMovementComponent
” 的关键属性 “bOrientRotationToMovement
” 为 false(禁止角色朝向移动方向)以及 “bUseControllerRotationYaw
” 为 true(使角色Yaw旋转与控制器同步),确保了服务器、所有客户端(操控和非操控)上的人物角色持枪移动的朝向一致性,都能面对前方进行侧向移动。
接下来我们开始计算 “Leaning
” 值,通过比较角色当前帧与上一帧的旋转获取差量 “Delta
”,然后对 “Delta
” 的 Yaw 分量进行缩放获取目标倾斜量,借助插值函数 “FInterpTo()
” 在 “Leaning
” 和目标倾斜量进行平滑插值,最后将 “Leaning
” 的最终值限制在 [-90, 90] 的范围内,以控制身体倾斜的自然幅度。同样的,我们在 “EquippedWeapon
” 状态机的动画蓝图中,将计算好的 “Leaning
” 节点连接到奔跑混合空间 “EquippedRun
” 节点的对应输入引脚,驱动人物角色在转向时播放正确的倾斜动画。
为了处理人物角色在进行向左/右侧向移动时,如果快速向另一边转向,侧向移动动画在 -180 与 180 度之间跳跃导致的抽搐问题,我们首先尝试直接在 “EquippedRun
” 混合空间直接设置水平坐标的插值时间,在测试中我们发现当人物角色向后移动时会发生抽搐。为了同时解决这两个问题,我们又在 C++ 中使用 “RInterpTo
” 函数对移动方向旋转 “MovementRotation
” 和瞄准旋转 “AimRotation
” 的差量进行平滑插值处理后,再获取 Yaw 分量值,即 “YawOffset
”。
最终,我们实现了角色在任意方向持枪奔跑时,身体能平滑、自然地进行侧向移动和倾斜,并在多人游戏环境下所有客户端都能正确同步显示。