本文為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) 的區間,確保所有機器上的人物角色瞄準偏移的動畫都能夠正確同步。