本篇部落格為遊戲開發記錄,博主只是想自己做個移動元件給自己做遊戲用,此間產生的一些經驗也做一個分享。
為了在3D世界中自由的控制我們的角色,引擎一般會提供一些基礎的移動元件,上層使用者做提供一些每幀的速度輸入,移動元件應該返還一個正確的位置,一般來說就是保證不會穿模和沿著牆面滑行。
為了達成這個目的 常規思路有兩種,一種是直接使用動力學 rigidbody,另一種是基於運動學rigidbody,或者你的世界完全沒有物理互動 那麼也可以無rigidbody。
第一種動力學rigidbody,相信大家都不陌生,就是讓物理引擎去接管我們的運動,我們提供力 速度寫入,物理引擎會自動判斷周圍有哪些物體,然後使用碰撞檢測、碰撞處理演演算法去解動力學求交,並且各種速度、加速度、角速度都會因為動力學求解而非常的自然。但是對於很多遊戲而言想要駕馭動力學其實也是一件難事,比如我們的主角 通常都要保持一個直立的站立、穩定的旋轉量,於是一般這類基於動力學剛體的移動元件都會鎖住旋轉量,或者純動力學利用joint或者其他機制來保證主角的平衡。
動力學剛體我找到了一個案例,來自CatCoding的 movement實現:https://catlikecoding.com/unity/tutorials/movement/
第二種則是運動學剛體,Kinematic,這類剛體和前面的剛體的主要區別在於,運動學剛體,物理引擎是將其當成質量無限大的物體來處理的,這樣各種碰撞交叉產生的擠出便不會對此類剛體生效,此類剛體可以實現想去哪就去哪,可以被上層Gameplay邏輯精準控制位置,同時PhysX也會對這類剛體的運動有所記錄,運動學剛體可以擠開各種動力學剛體。
如果要基於此類剛體做移動元件,主要就是解決和其他碰撞體的交叉問題,讓運動學剛體運動不穿模,動力學剛體的擠開則是完全根據質量等進行物理計算,運動學其實在這塊把這類問題暴露給上層,理論上交給上層的控制性更高。
其實Unity有提供一個現成的元件,名字叫CharacterController,他其實是PhysX基於Capsule的一個移動封裝,很多專案也都在使用。
網路上也有一些開源的KCC外掛,比如說
OpenKCC
UnityAsset Store的Kinematic Character Controller
其實Unreal提供的移動元件也是一個KCC的實現,實現的功能很多,提供了狀態機的思路,所以大家用起來才這麼方便,可以參考一下知乎的移動元件剖析文章: 《Exploring in UE4》移動元件詳解[原理分析]
這一些移動元件我都大體有看過,我這邊參考UnityAssetStore的框架結構結合一些UE的思想做了一個簡單的移動元件。
這裡先說一下移動元件的設計目的,解決什麼問題。
上層只用輸入一個速度值,速度值傳入移動元件,移動元件根據速度計算當幀的位移量,然後根據此位移量做移動預測,在預測位置做位置校準,保證最後輸出的位置是不穿模的位置。並且提供一些狀態量給上層,包括有沒有踩到地面,踩到的地面是不是一個斜面,當幀有沒有發生碰撞的事件,什麼時候落地的事件,以及幫助上層處理上臺階的問題。
至於你想實現某些地形要做什麼特殊處理,都可以基於提供的狀態量做拓展,比如在斜面上的行為完全在上層寫,或者自己再做一些額外的場景查詢功能,自己維護狀態。
以下是我FixedUpdate裡會跑的執行流程。
void Simulate()
{
characterController.BeforeCharacterUpdate(Time.fixedDeltaTime);
TimeIntegration();
InitPositionOverlapTest();
GroundDetection();
MovementDetection();
PendingLeaveGroundLoop();
ApplayDeltaPos();
characterController.AfterCharacterUpdate(Time.fixedDeltaTime);
}
玩家首先在每幀Update進行Input輸入,我們在update裡把這些資料記錄下來,傳入移動元件,然後移動元件在FixedUpdate裡對這些記錄的輸入狀態進行速度改變,注意一定是FixedUpdate裡做這些速度改變,因為unity涉及物理的tick都跑在FixedUpdate。因此我們移動元件可以提供一個UpdateVelocity介面,讓上層所有的運動速度修改都走這個介面,這樣就可以保證不會在錯誤的時機寫入速度。
整個執行流程一開始試一次時間積分,用於記錄一下當幀的移動元件位置targetPos,跑UpdateVelocity的邏輯改變速度,改變後的速度進行積分得到造成的當幀位移量deltaPos,targetPos會在最後和deltaPos相加然後走Kinematic 剛體的MovePosition和MoveRotation介面來應用。
地面檢測要處理是否踩到地面,是否踩到斜面,對於很低的小碰撞體要可以跨過去(其實也就是支援上臺階)。
這裡給出一句簡要總結: 斜面是特殊的地面,臺階是特殊的斜面。
地面檢測的流程:
1、從上往下做CapsuleCast 尋找潛在的地面碰撞體,取最近的
2、如果cast中了地面就根據預測位置進行Ovelap,檢測是否和地面碰撞體相交
3、如果檢測到了相交就ComputePenetration計算怎樣解相交,將計算出的解相交的位移量進行應用
當我們真的完成了一次解相交 便可以做一個丟擲一個落地了的事件。
這裡畫了一幅圖 ,
黑色 原始位置
綠色 預測位置
藍色 校正位置
紅色是cast 用來確定是和哪個碰撞解穿插
黃色使我們一次計算得到實際位移量。
碰撞點在capsule的下半身的位置,這裡可以定義一個最大的地面碰撞點高度,排除上半身cast到東西的情況。
需要注意的是Cast的起點應該向上提一些,這樣避免膠囊體貼住地面、有一定交叉的時候Cast不到地面。
另外CapsuleCast得到的RaycastHit的Normal有一些波動,不是正確的Normal,需要進行Normal修正,特別是在斜面判斷上。
根據cast出的地面的法線,計算夾角,給出一個最大的能上斜面的角度,夾角大於這個度數則判斷為斜面,需要指出正對的屬於90度的斜面。
我們每幀的即將產生的位移量應該在這個法向量所在的平面進行投影,這樣就能實現沿著斜面運動。
首先碰撞點低於下半身球的碰撞點,已經過濾了一波,說明你面對的是一個很低的阻擋物,接下來我們通過一些額外的Raycast可以判斷他是否是臺階。
我這裡就是通過兩層射線,下層射線和上層射線,下層射線比上層射線短一點,如果下層射線打中了上層射線沒打到則說明這是個臺階,很簡單粗暴,但是很有效。
然後臺階 = 角度很大的斜面,此時經過一次ComputePenetration,我們的狀態會在不穩定斜面上,此時我們就可以判斷,加入此時的輸入的deltaPos是朝向著這個很大的斜面的。就可以判斷能不能上臺階了,如果能上就執行上臺階的功能。
具體上臺階就是讓deltaPos 向上方和原來的速度方向做一個調整,同時因為是上臺階,可以將當前速度的y方向改成0,等於你踩在了臺階上。
我們在地面上運動 如果要起跳就是把y軸速度置為一個值,然後傳進去,此時我們的地面檢測應該先關閉一小段時間,並且把movement在地面上的狀態置為false,來保證上層按下跳躍之後的下一幀就能起跳,避免下一幀還是movement在地面上狀態為true,影響上層邏輯PendingLeaveGroundLoop就是做了簡單定時器。
當需要跳躍的時候就RequestJump,這樣定時器就會啟動保證落地狀態正確。
移動檢測 其實非常簡單
包含兩個部分
1、初始位置錯誤
比如我們提供了直接SetPosition這樣的傳送介面,傳送之後的位置如果有和其他東西相交,那麼先解這個相交,這個過程在InitPositionOverlapTest裡做了。
2、當幀速度應用後位置錯誤
將targetPos+deltaPos得到當幀預測位置,對預測位置進行OverlapTest,然後按順序解各個overlap的相交量。
當然我們也會過濾一些碰撞體,比如純動力學物體我就不進行解相交,這樣我們的運動元件就可以擠開一些動力學的物體,不過這樣其實也不是很好,會遇到各種問題,比如把一些動力學的東西擠到牆裡去,其實計算一下動力學物體的滲透深度然後改速度應用給這些動力學物體會是一個好的解法。
如果我們真的解掉了相交,那麼就可以丟擲一個碰到了東西的事件。
好的,完成了以上步驟我們就基本能得到一個簡單的,可以在地面上跑來跑去可以跨越臺階的可以跳以及移動不會穿模的簡單移動元件了。
為什麼要自己做一個移動元件呢,主要身為遊戲開發者,還是我認為對於移動這種最基本的東西還是應該有絕對的把握。把很多的運動交給物理引擎是很方便,但是當我們覺得不滿的時候想改起來在上層還是困難比較多,自己實現一個簡單的再根據專案檢驗來完善自己的移動元件會是一件有價值的事情。
我同時也在B站上傳一個視訊,可以點選連結來看看效果。
想明白了,開始做了,做的有反饋,想的愈明白。
感謝你的閱讀,我是飛翔的子明,期待我們都做出我們心中的遊戲。
2023.6.30