UE4的移動碰撞

遊資網發表於2019-04-10
上古時代的遊戲並不會使用例如PhysX等物理引擎,例如Quake和Doom,開發者們都會自己編寫簡單的碰撞檢測模組來完成角色移動邏輯。雖然碰撞檢測需要的物理演算法很簡單,但想讓遊戲操作起來更加順暢,往往需要非常多的細節處理邏輯。這些特殊的移動處理邏輯叫做collide and slide演算法,經過了10多年的積累沉澱,這套邏輯已經非常成熟,被應用到各種型別的遊戲上。

好奇的同學可能會問,既然有了PhysX物理引擎,為什麼不直接用它來完成角色移動呢?

原因有很多,這裡列舉幾個比較典型的

  • 討厭的tunneling effect,用過物理引擎的同學可能會遇到,如果角色的移動速度過快,它很可能會穿透牆。所以,角色的最大速度往往是被限制的,這並不能滿足遊戲設計需求。即使不出現tunnel effect,角色碰到了牆角,會出現抽搐抖動,甚至移動到不可知的位置。
  • 不能直接控制角色,想讓物理引擎的剛體移動,需要施加impluse或者force推力,這並不能讓角色移動到想要到的位置。
  • 摩擦力問題,想讓角色站在斜坡上,需要設定無限大的摩擦力,但這回導致角色無法在斜坡上移動。
  • 不受控制的跳躍,在類似波浪線有起有伏的地形上移動,不可避免的會騰空。


以上這些情況如果使用物理引擎幾乎是無法避免的,所以目前幾乎所有的遊戲都會自定義自己的移動模組,模組的複雜程度根據遊戲的型別規模有著天壤之別,運動類遊戲和第一人稱射擊遊戲的移動模組往往是最複雜的。而第一人稱射擊類遊戲的移動模組更具有通用性,經過多年發展,已經比較成熟,所以本文參考UE4中的程式碼,抽取其中核心邏輯,向大家介紹collide and slide演算法。

在瞭解UE4的移動邏輯之前,我們先熟悉下碰撞的基礎介面

UE4移動中碰撞檢測主要使用PhysX的Geometry Queries(幾何查詢)功能

  • 射線檢測RayCasts
  • 重疊檢測Overlaps
  • 滲透深度計算Penetration Depth
  • Sweeps檢測
  • InitialOverlaps檢測


UE4把查詢後返回的hit封裝成了FHitResult

FHitResult的結構如下

bBlockingHit是否發生碰撞

bStartPenetrating是否在檢測開始就有滲透情況

Time碰撞後實際移動距離除以檢測移動距離

Distance碰撞後實際移動距離

Location碰撞後最終位置

ImpactPoint碰撞接觸點

Normal碰撞切面法向量

ImpactNormal碰撞切面法向量(非膠囊體和球體檢測與Normal不同)

TraceStart檢測開始位置

TraceEnd檢測結束位置

PenetrationDepth滲透深度

我們可以藉助以下兩種移動中常見的情況熟悉一下這些引數,

第一種是常見的膠囊體Sweep查詢

UE4的移動碰撞

查詢開始結束分別是TraceStart和TraceEnd兩個位置,如果碰到了障礙,bBlockingHit就是true,膠囊體最終會停在Location位置,它移動的距離是Distance,Time是一個0到1的值,表示實際移動距離比查詢距離。還有一些可能會用的引數,比如碰撞接觸點ImpacePoint,碰撞切面法向量Normal和ImpactNormal

第二種常見的情況通常是InitialOverlaps,開始位置檢測到了重疊

UE4的移動碰撞

這時候bStartPenetrating是true,通過滲透深度計算可以獲得PenetrationDepth,這個引數對於處理移動中穿透的情況非常重要

仔細觀察的話可以發現上面膠囊體的Sweep就是一次簡單的移動過程,UE4將這個過程進一步封裝成了SafeMoveUpdatedComponent,它是UE4移動最關鍵的函式,幾乎所有的移動都要靠它來完成。它的主要功能有以下幾點

  • 篩選Hit
  • SetLocation並遞迴更新子元件
  • UpdateOverlap,Overlap檢測
  • 解決滲透的情況,bStartPenetration
  • 返回檢測結果Hit


下面分別介紹一下這些功能,注意下面的符號▽△用於表示函式的開始和結束

SafeMoveUpdatedComponent

UPrimitiveComponent::MoveComponentImpl

呼叫SweepMulti獲取合理的Hit

呼叫SweepMulti得到的所有Hit需要拉回微小的距離(縮小hit.time),避免因為浮點數精度的問題導致跟碰撞物重疊

如果檢測到多個block hit,優先選擇不是在初始位置就檢測到block的hit,否則的話選取跟運動方向最相反的hit

UE4的移動碰撞

如上圖是俯檢視,圓形是膠囊體,方形是碰撞物,紅色箭頭是運動方向,膠囊體同時跟3個障礙物發生的碰撞,得到了3個hit,也就是圖中的3個綠色剪頭,按照篩選規則,選取跟紅色箭頭方向最相反的,也就是中間的綠色箭頭的hit。

SetPosition以及相關操作

  • 呼叫SetWorldLocationAndRotation
  • 更新ComponentToWorld Transform矩陣
  • 更新父元件和遞迴更新子元件
  • 更新導航網格資料,Bounds邊界
  • 更新RenderTransform以及PhysicsTransform


呼叫UpdateOverlap更新重疊狀態

  • 呼叫OverlapMulti,獲得檢測結果
  • 更新Overlap Components列表,刪除不再Overlap的Component,新增新的Component。
  • 更新子Component的


Overlap Components列表

  • 更新PhysicsVolume(比如進入離開水域)


UPrimitiveComponent::MoveComponentImpl

如果呼叫MoveComponentImpl返回的hit結果bStartPenetrating是true,需要呼叫ResolvePenetration解決穿透的問題

ResolvePenetration

UE4的移動碰撞

上圖是俯檢視,圓形代表膠囊體,方形是障礙物,膠囊體跟左邊的障礙物穿透了,比較直觀的解決方法是將它按照左邊重疊的綠色箭頭拉回,拉回的距離就是上面提到的PenetrationDepth變數,如果拉回過程中又跟右邊的障礙物穿透了,這時候會得到右邊的綠色箭頭,左右兩邊的箭頭疊加,也就是向量相加,會得到中間向下的箭頭,按著這個方向拉回,就會避免穿透問題。如果調整位置成功了,還需要再次嘗試最開始的移動。

ResolvePenetration

SafeMoveUpdatedComponent

SafeMoveUpdateComponent可以看做是底層碰撞檢測和上層移動邏輯的中間層,是基礎的移動單元,接下來我們要介紹的移動邏輯,看似複雜,其實都是由這些移動單元構成的。整個移動邏輯的主函式是PerformMovement,我們還是按照函式的呼叫順序梳理一遍它的主要邏輯。

PerformMovement

1.根據輸入向量InputVector計算加速度向量Acceleration

2.隨著被騎乘物MovementBase(比如電梯,載具)移動

3.將衝力Impulse和推力Force作用於速度Velocity,一般用於擊退和徑向運動

4.根據不同的運動狀態運動

  • MOVE_None(不做運動)
  • MOVE_Walking(踩地面上運動)
  • MOVE_NavWalking(踩導航網格上運動)
  • MOVE_Falling(在空中受重力加速度)
  • MOVE_Flying(不受重力加速度的運動)
  • MOVE_Swiming(在水中運動)
  • MOVE_Custom(自定義運動,比如插值運動)


先看下MOVE_Walking

PhysWalking

首先將速度和加速度的垂直方向分量設為0,方向始終保持在水平面上

CalcVelocity

1.計算速度,先設定為RequestedVelocity(尋路元件PathFollowingComponent根據路徑不斷設定該速度)

2.加速度是0的時候,將受到減速度BrakingDeceleration和摩擦力的影響而減速

3.加速度不是0的時候,摩擦力將會影響速度方向改變快慢

4.計算速度向量Velocity+=Acceleration*DeltaTime

5.最後,如果支援RVOAvoidance,將會根據RVO重新計算速度,避免跟其他角色重疊在一起,效果就像被彈回來。

CalcVelocity

MoveAlongFloor

計算移動向量Delta=Velocity*DeltaTime

UE4的移動碰撞

根據地面坡度調整移動向量方向,如上圖需要改為沿著面1坡度的方向,也就是紅色箭頭的方向,呼叫SafeMoveUpdatedComponent

UE4的移動碰撞

如果返回Hit結果是block,如上圖碰到了面2,通過返回的Hit的Normal引數檢測到面2的斜面坡度較緩,這時可以將剩下的移動向量改為沿著面2移動,再次呼叫SafeMoveUpdatedComponent,如果返回的Hit結果還是block或者面2非常陡峭(如下圖所示),可以開始嘗試呼叫StepUp上樓的邏輯

StepUp

UE4的移動碰撞

理想情況下的上樓梯過程如圖所示,它是由3次移動構成,首先向上移動MaxStepHeight高度,然後向前移動(向前移動過程中如果檢測到block,需要呼叫SlideAlongSurface),最後向下移動,落到面2上面。當然,存在很多情況會導致StepUp失敗,比如移動過程中檢測到穿透Penetration,最終無法落到一個合理的落腳點(比如面2比較陡峭),都會導致呼叫StepUp失敗,在這種情況下,我們需要呼叫SlideAlongSurface,貼著面走

StepUp

在呼叫SlideAlongSurface貼著面走之前,需要呼叫HandleImpact,處理碰撞發生後帶來的副作用

HandleImpact

傳送MoveBlockedBy事件,如果開啟bEnablePhysicsInteraction,可以給與剛體一個反推力

HandleImpact

SlideAlongSurface

UE4的移動碰撞

二維的圖示並不能很好表示貼牆走的情況,我們看下上面這個截圖,紅色箭頭表示最開始移動方向,撞到面2後,我們呼叫StepUp失敗,嘗試SlideAlongSurface,於是移動方向變為貼著面2的黃色箭頭,如果按照黃色箭頭的移動過程中很不幸又碰到了一個面,我們需要呼叫TwoWallAdjust

TwoWallAdjust

利用兩個面法向量計算面2和麵3的夾角,如果夾角大於90度,我們可以將移動方向變為沿著面3的綠色箭頭

UE4的移動碰撞

如果面2和麵3的夾角小於90度,我們可以沿著面2和麵3的夾縫(如下圖的綠色向量)繼續移動,這個夾縫向量可以通過面2和麵3的法向量的叉乘結果計算出來,當然這個夾縫向量的傾斜角度不能過於陡峭,否則角色也是不能按照這個方向移動的。

UE4的移動碰撞

TwoWallAdjust


SlideAlongSurface


MoveAlongFloor

到這裡MoveAlongFloor就執行完了,然後還需要呼叫FindFloor,檢測地面,調整縱座標,保證角色始終貼著地表

FindFloor

FindFloor返回的結果也是個比較重要的結構,我們看下它的引數

FFindFloorResult

bBlockHit是否跟地面有碰撞

bWalkableFloor可以行走的地面

bLineTrace是否是通過line trace檢測出來的結果

FloorDist Sweep查詢到地面的距離

LineDist LineTrace查詢到地面的距離

HitResult跟地面的FHitResult

ComputeFloorDist

一般情況下,比如下圖中的情況,我們只需要一次垂直向下Sweep檢測就可以計算出FloorDist,注意檢測的距離是之前StepUp向上檢測的距離。這時候FloorDist等於返回Hit的Distance

UE4的移動碰撞

如果返回的的Hit是bStartPenetration是true話則需要用一個縮小的膠囊體來重新向下Sweep,算出來的FloorDist減去縮水的高度就是原膠囊體跟地面的距離

UE4的移動碰撞

如果用縮小膠囊體Sweep還是有穿透情況,這時候需要改用line trace,從膠囊體的中心向下trace膠囊體的半個身高,如果檢測到了hit,則可以計算出陷入到地面以下的高度

UE4的移動碰撞

注意無論是sweep還是line

trace設定膠囊體向上抬的調整高度MaxPenetrationAdjust最大隻能是膠囊體的半徑,如果陷入地下的深度大於調整高度,一次調整是無法將膠囊體從地面抬出來的,往往需要多幀處理才可以。

ComputeFloorDist

FindFloor

通過呼叫AdjustFloorHeight根據之前計算的FloorDist來調整角色的垂直座標

如果FindFloorResult的bWalkableFloor是false,需要呼叫CheckFall,切換成MOVE_Falling狀態

PhysWalking

其他幾種運動狀態這裡不做具體說明,大體邏輯是基本相似的,區別在於計算速度和對返回Hit的特殊處理上。

Physx也提供了CharacterController移動庫,有興趣的可以參考下。

作者:李雪峰
專欄地址:https://zhuanlan.zhihu.com/p/33529865

相關文章