本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P51 多人游戏中的俯仰角(Pitch in Multiplayer)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author) Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
文章目录
- P51 多人游戏中的俯仰角(Pitch in Multiplayer)
- 51.1 旋转体的同步机制
- 51.2 映射 AO_Pitch 区间
- 51.3 Summary
P51 多人游戏中的俯仰角(Pitch in Multiplayer)
本节课我们将对瞄准偏移俯仰角变量 “AO_Pitch
” 进行网络同步,以解决上节课中服务器和客户端人物角色瞄准偏移动画不同步的问题。
51.1 旋转体的同步机制
-
打开 Visual Studio,在 “
BlasterCharacter.cpp
” 中的 “AimOffset()
” 函数中添加调试代码,将客户端上服务器所控制的人物角色的瞄准偏移俯仰角 “Pitch
” 打印到消息日志中。/*** BlasterCharacter.cpp ***/...// 瞄准偏移 void ABlasterCharacter::AimOffset(float DeltaTime) {if (Combat && Combat->EquippedWeapon == nullptr) return;FVector Velocity = GetVelocity(); // 获取人物角色速度向量Velocity.Z = 0.f; // 不关心 Z 轴速度,设置为 0float Speed = Velocity.Size(); // 获取人物角色速度向量的模(大小)bool bIsInAir = GetCharacterMovement()->IsFalling(); // 判断人物角色是否掉落从而判断人物角色是否在空中if (Speed == 0.f && !bIsInAir) { // 当人物角色静止站立且不跳跃时FRotator CurrentAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f); // 获取人物角色当前瞄准旋转FRotator DeltaAimRotation = UKismetMathLibrary::NormalizedDeltaRotator(CurrentAimRotation, StartingAimRotation); // 标准化获取 CurrentAimRotation 和 StartingAimRotation 的差量AO_Yaw = DeltaAimRotation.Yaw; // 获取人物角色瞄准偏航角bUseControllerRotationYaw = false; // 禁用控制器旋转偏航}if (Speed > 0.f || bIsInAir) { // 当奔跑或跳跃时StartingAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f); // 改变奔跑或跳跃状态转换为静止站立状态时的起始瞄准旋转AO_Yaw = 0.f; // 由于启用了控制器旋转偏航,人物角色朝向始终面向控制器当前朝向,因此设置 AO_Yaw 为 0 bUseControllerRotationYaw = true; // 启用控制器旋转偏航}AO_Pitch = this->GetBaseAimRotation().Pitch; // 获取人物角色瞄准俯仰角/* P51 多人游戏中的俯仰角*/if (!HasAuthority() && !IsLocallyControlled()) {UE_LOG(LogTemp, Warning, TEXT("AO_Pitch: %f"), AO_Pitch);}/* P51 多人游戏中的俯仰角*/ }...
-
编译后进行测试。当我们控制服务器上的人物角色持续向下进行瞄准时,可以看到客户端上打印出的 “
AO_Pitch
” 的值从大于 0 减小到 0,然后再跳跃到 360.0 从 360.0 开始减小,所以客户端上服务器控制的人物角色会向上进行瞄准。这便是问题所在,因为我们本来期望的是 “AO_Pitch
” 的值在减小到 0 之后在负数的区间 [-90,0) 里面连续变化,这样 “AO_Pitch
” 的值就可以和我们瞄准偏移 “HipAimOffset
” 和 “AimAimOffset
”中的垂直轴 [-90,0) 区间对应上。
-
出现这样的测试结果与虚幻引擎对旋转体 Yaw、Pitch 和 Roll 三个方向上的分量值进行网络同步的机制有关。为了减小网络同步的压力,通常在发送方会将它们进行压缩传输,在此过程中首先会将角度限定在 [0->360) 的浮点数区间,任何角度的负值都会变成正值,以便在压缩时映射到 [0->65535) 的字节区间。而在接收方,解压缩过程则相反,将角度从字节值重新映射到 [0->360) 的浮点值。
PlayerInfo 高频同步解决方案
需求目的
分析一个常见的需求: “在 1P 客户端显示 3P 的Transform
”。
显然,在客户端存在 3P 的Pawn
时,可以直接取Pawn
的Transform
;但出于性能考虑,会进行各种 AOI 机制,在较远距离时客户端会将 3P 的Pawn
裁剪掉,只留下 PlayerState(或者某个不被剪裁的数据Channel
) 用于同步。
一个直观的想法是将Transform
直接通过对应的PlayerState
属性同步给所有客户端;但出于性能考虑,对于同步一般会开启 PushModel;这种高频字段会频繁将PlayerState
对应ActorChannel
给MarkDirty
,导致PushModel
功能基本失效,频繁进行同步的Diff
等大开销的操作;所以需要一个机制对这种情况进行优化。
核心思路
对于 DS,创建一个Channel
专门用于同步Player
的高频变化信息,如Location
、Rotation
等;
对于同步的信息,进行适当的同步降频(不需要每帧同步)、字节压缩(舍弃部分精度,精确到float
没有意义);
同时为了保证Client
的信息相对正确(同步降频会导致Location
不连续),在 1PClient
进行信息的预测插值;
—— 《[UE] PlayerInfo高频同步解决方案》
-
具体可以参阅虚幻引擎的源码,值得注意的是函数 “
CompressAxisToShort()
” 中在将角度从浮点数量化为字节值、四舍五入后使用了位运算 “& 0xFFFF
”,其作用主要在于只保留最后 16位,剔除 16 位前所有的二进制值,这样返回值的类型就是 “uint16
” 。/*** Rotator.h ***/...template<typename T> FORCEINLINE uint16 TRotator<T>::CompressAxisToShort( T Angle ) {// map [0->360) to [0->65536) and mask off any windingreturn FMath::RoundToInt(Angle * (T)65536.f / (T)360.f) & 0xFFFF; }template<typename T> FORCEINLINE T TRotator<T>::DecompressAxisFromShort( uint16 Angle ) {// map [0->65536) to [0->360)return (Angle * (T)360.f / (T)65536.f); }...
51.2 映射 AO_Pitch 区间
- 了解了虚幻引擎对旋转体分量值进行网络同步的机制,接下来我们只需将 “
AO_Pitch
” 的变化区间 [270, 360) 映射到 [-90, 0) 即可。在 “BlasterCharacter.cpp
” 中的 “AimOffset()
” 函数中定义两个区间,然后调用 “FMath::GetMappedRangeValueClamped()
” 进行区间映射即可。/*** BlasterCharacter.cpp ***/...// 瞄准偏移 void ABlasterCharacter::AimOffset(float DeltaTime) {if (Combat && Combat->EquippedWeapon == nullptr) return;FVector Velocity = GetVelocity(); // 获取人物角色速度向量Velocity.Z = 0.f; // 不关心 Z 轴速度,设置为 0float Speed = Velocity.Size(); // 获取人物角色速度向量的模(大小)bool bIsInAir = GetCharacterMovement()->IsFalling(); // 判断人物角色是否掉落从而判断人物角色是否在空中if (Speed == 0.f && !bIsInAir) { // 当人物角色静止站立且不跳跃时FRotator CurrentAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f); // 获取人物角色当前瞄准旋转FRotator DeltaAimRotation = UKismetMathLibrary::NormalizedDeltaRotator(CurrentAimRotation, StartingAimRotation); // 标准化获取 CurrentAimRotation 和 StartingAimRotation 的差量AO_Yaw = DeltaAimRotation.Yaw; // 获取人物角色瞄准偏航角bUseControllerRotationYaw = false; // 禁用控制器旋转偏航}if (Speed > 0.f || bIsInAir) { // 当奔跑或跳跃时StartingAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f); // 改变奔跑或跳跃状态转换为静止站立状态时的起始瞄准旋转AO_Yaw = 0.f; // 由于启用了控制器旋转偏航,人物角色朝向始终面向控制器当前朝向,因此设置 AO_Yaw 为 0 bUseControllerRotationYaw = true; // 启用控制器旋转偏航}AO_Pitch = this->GetBaseAimRotation().Pitch; // 获取人物角色瞄准俯仰角/* P51 多人游戏中的俯仰角*/// if (!HasAuthority() && !IsLocallyControlled()) {// UE_LOG(LogTemp, Warning, TEXT("AO_Pitch: %f"), AO_Pitch);// }if (AO_Pitch > 90.f && !IsLocallyControlled()) { // 对于其他机器上非本地控制的人物角色FVector2D InRange(270.f, 360.f);FVector2D OutRange(-90.f, 0.f);AO_Pitch = FMath::GetMappedRangeValueClamped(InRange, OutRange, AO_Pitch); // 将区间 [270,360) 映射到 [-90,0)}/* P51 多人游戏中的俯仰角*/ }...
- 编译后进行测试,可以发现无论是 “
HipAimOffset
” 还是 “AimAimOffset
” 的动画都能在所有机器上正确同步。
51.3 Summary
本节课我们成功解决了多人游戏中瞄准偏移俯仰角(Pitch)的网络同步问题。
在测试中我们发现,在客户端上服务器所控制的人物角色持续向下瞄准时,“AO_Pitch
” 的值会从 360 开始递减而不是保持在 [-90,0) 的区间,导致瞄准方向相反。通过深入分析虚幻引擎的旋转体同步机制,我们了解到虚幻引擎将 [0->360) 角度值映射到 [0->65536) 再进行压缩传输进行网络传输,这导致了负角度值被转换为正角度值,从而造成客户端与服务器上的瞄准动画显示不一致。
因此,我们在 “BlasterCharacter.cpp
” 中的 “AimOffset()
” 函数中通过调用 “FMath::GetMappedRangeValueClamped()
” 函数,将 “AO_Pitch
” 的 [270,360) 区间映射到 [-90,0) 的区间,确保所有机器上的人物角色瞄准偏移的动画都能够正确同步。