《UE5_C++多人TPS完整教程》学习笔记50 ——《P51 多人游戏中的俯仰角(Pitch in Multiplayer)》


本文为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)](#P51 多人游戏中的俯仰角(Pitch in Multiplayer))
  • [51.1 旋转体的同步机制](#51.1 旋转体的同步机制)
  • [51.2 映射 AO_Pitch 区间](#51.2 映射 AO_Pitch 区间)
  • [51.3 Summary](#51.3 Summary)

P51 多人游戏中的俯仰角(Pitch in Multiplayer)

本节课我们将对瞄准偏移俯仰角变量 "AO_Pitch" 进行网络同步,以解决上节课中服务器和客户端人物角色瞄准偏移动画不同步的问题。


51.1 旋转体的同步机制

  1. 打开 Visual Studio,在 "BlasterCharacter.cpp" 中的 "AimOffset()" 函数中添加调试代码,将客户端上服务器所控制的人物角色的瞄准偏移俯仰角 "Pitch" 打印到消息日志中。

    cpp 复制代码
    /*** BlasterCharacter.cpp ***/
    
    ...
    
    // 瞄准偏移
    void ABlasterCharacter::AimOffset(float DeltaTime)
    {
        if (Combat && Combat->EquippedWeapon == nullptr) return;
        FVector Velocity = GetVelocity();												// 获取人物角色速度向量
        Velocity.Z = 0.f;																// 不关心 Z 轴速度,设置为 0
        float 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 多人游戏中的俯仰角*/
    }
    
    ...
  2. 编译后进行测试。当我们控制服务器上的人物角色持续向下进行瞄准时,可以看到客户端上打印出的 "AO_Pitch" 的值从大于 0 减小到 0,然后再跳跃到 360.0360.0 开始减小,所以客户端上服务器控制的人物角色会向上进行瞄准。这便是问题所在,因为我们本来期望的是 "AO_Pitch" 的值在减小到 0 之后在负数的区间 [-90,0) 里面连续变化,这样 "AO_Pitch" 的值就可以和我们瞄准偏移 "HipAimOffset" 和 "AimAimOffset"中的垂直轴 [-90,0) 区间对应上。


  3. 出现这样的测试结果与虚幻引擎对旋转体 Yaw、Pitch 和 Roll 三个方向上的分量值进行网络同步的机制有关。为了减小网络同步的压力,通常在发送方会将它们进行压缩传输,在此过程中首先会将角度限定在 [0->360) 的浮点数区间,任何角度的负值都会变成正值,以便在压缩时映射到 [0->65535) 的字节区间。而在接收方,解压缩过程则相反,将角度从字节值重新映射到 [0->360) 的浮点值。

    PlayerInfo 高频同步解决方案
    需求目的

    分析一个常见的需求: "在 1P 客户端显示 3P 的 Transform "。

    显然,在客户端存在 3P 的 Pawn 时,可以直接取 PawnTransform;但出于性能考虑,会进行各种 AOI 机制,在较远距离时客户端会将 3P 的 Pawn 裁剪掉,只留下 PlayerState(或者某个不被剪裁的数据 Channel) 用于同步。

    一个直观的想法是将 Transform 直接通过对应的 PlayerState 属性同步给所有客户端;但出于性能考虑,对于同步一般会开启 PushModel;这种高频字段会频繁将 PlayerState 对应 ActorChannelMarkDirty,导致 PushModel 功能基本失效,频繁进行同步的 Diff 等大开销的操作;所以需要一个机制对这种情况进行优化。
    核心思路

    对于 DS,创建一个 Channel 专门用于同步 Player 的高频变化信息,如 LocationRotation 等;

    对于同步的信息,进行适当的同步降频(不需要每帧同步)、字节压缩(舍弃部分精度,精确到 float 没有意义);

    同时为了保证 Client 的信息相对正确(同步降频会导致 Location 不连续),在 1P Client 进行信息的预测插值;


    ------ 《[UE] PlayerInfo高频同步解决方案》

  4. 具体可以参阅虚幻引擎的源码,值得注意的是函数 "CompressAxisToShort()" 中在将角度从浮点数量化为字节值、四舍五入后使用了位运算 "& 0xFFFF" ,其作用主要在于只保留最后 16位,剔除 16 位前所有的二进制值 ,这样返回值的类型就是 "uint16" 。

    cpp 复制代码
    /*** Rotator.h ***/
    
    ...
    
    template<typename T>
    FORCEINLINE uint16 TRotator<T>::CompressAxisToShort( T Angle )
    {
        // map [0->360) to [0->65536) and mask off any winding
        return 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 区间

  1. 了解了虚幻引擎对旋转体分量值进行网络同步的机制,接下来我们只需将 "AO_Pitch" 的变化区间 [270, 360) 映射到 [-90, 0) 即可。在 "BlasterCharacter.cpp" 中的 "AimOffset()" 函数中定义两个区间,然后调用 "FMath::GetMappedRangeValueClamped()" 进行区间映射即可。

    cpp 复制代码
    /*** BlasterCharacter.cpp ***/
    
    ...
    
    // 瞄准偏移
    void ABlasterCharacter::AimOffset(float DeltaTime)
    {
        if (Combat && Combat->EquippedWeapon == nullptr) return;
        FVector Velocity = GetVelocity();												// 获取人物角色速度向量
        Velocity.Z = 0.f;																// 不关心 Z 轴速度,设置为 0
        float 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 多人游戏中的俯仰角*/
    }
    
    ...
  2. 编译后进行测试,可以发现无论是 "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) 的区间,确保所有机器上的人物角色瞄准偏移的动画都能够正确同步。


相关推荐
CandyU22 小时前
UE5 基础应用 —— 09 - 展示类小项目
ue5
vonlycn2 小时前
UE5 性能优化(1) 模型合并,材质合并
ue5·材质
The Chosen One9854 小时前
C++ : AVL树-详解
开发语言·c++
zzyzxb4 小时前
std::enable_shared_from_this
c++
SNAKEpc121384 小时前
QML和Qt Quick
c++·qt
hansang_IR4 小时前
【题解】洛谷 P4286 [SHOI2008] 安全的航线 [递归分治]
c++·数学·算法·dfs·题解·向量·点积
GanGuaGua5 小时前
Linux系统:线程的互斥和安全
linux·运维·服务器·c语言·c++·安全
怀旧,5 小时前
【C++】18. 红⿊树实现
开发语言·c++
lsnm5 小时前
【LINUX网络】IP——网络层
linux·服务器·网络·c++·网络协议·tcp/ip