在“業餘程序員”係列,我想分享一下作為一名業餘程序員的經驗。我的分享有兩個主要目的:一是證明非專業的程序員也能製作出原型;二是向專業程序員展示設計師是怎麼做程序的,也許能得到一些反饋和建議吧!最後,本係列也是對我本人工作的一些反思。這次我先介紹我如何使用Unity為個人項目製作自定義操作器:
為什麼使用自定義操作器,而不使用Unity默認的那個?
你可能會問自己:“但為什麼不使用Unity自帶的角色控製器呢?”事實上,我試過了,至少在一開始的時候。但後來我遇到了一些問題:
1、Unity的默認角色控製器是基於Unity的物理引擎的:如果你想製作一款物理平台遊戲(遊戲邦注:如《Little Big Planet》、《Trine》等),這也許是個不錯的解決方案。然而,我需要一種“敏銳”而準確的操作,像老式2D平台遊戲(《馬裏奧》、《索尼克》、《超級食肉男孩》等)那樣的,所以Unity的物理引擎太難調整了。
2、Unity默認的角色操作器是基於膠囊狀碰撞器:所以,玩家能夠輕易地在斜坡上行走,但當你要做的是朝右移動的平台遊戲時,膠囊就會卡在邊緣上,這是不可行的:

2d Platformer-01
3、最後,當我想到以後的AI尋徑,我決定自己做一個控製器:我認為在一般的平台遊戲中,使用簡單的功能如跑(速度)和跳(高度)可以更容易控製AI在路徑上的移動,並結合遊戲世界的限製條件(碰撞和重力)。
基本原則 沒什麼特別的,關於2D遊戲的碰撞有很多網上教程,我采用的方法不過是結合了我看過的東西和我自己的(不成功的)經驗。
因為我的原型是一款貼圖平台遊戲,我本可以使用簡單的係統來檢查我想移動的貼圖是不是牆體。出於若幹原因,主要是因為我之前不懂得用這種方法處理斜坡,我選擇了更通用的係統,即使用光線投射來檢測碰撞。
所以,基本的碰撞概念是非常簡單的:控製器以一定的速度值移動,除非這個速度導致它的碰撞器遇到另一個碰撞器。所以,我們隻需要檢測這些可能的碰撞!
為了檢測這些碰撞,正如我前麵所說的,我使用光線投射:當有一個速度值時,就有光線會搜索這個速度的方向(X軸和Y軸)的碰撞。如果光線發現碰撞,命中點會確定這個控製器在這個軸上能夠達到的最大值。
對於四個方向上的各個光線,偽代碼如下:
If speed on x axis > 0
If raycast to the right hits a collision on point (Px,Py)
Set xMaxLimit = Px
取決於控製器碰撞器的大小和貼圖的大小,我必須在每一邊放兩束光線,即一個角一束。但如果你的兩束光比你的貼圖要大,那麼你使用兩束光就會遇到如圖所示的問題:

2d Platformer-02
對一邊的光線不止一束的情況,我還必須比較它們的命中點P1和P2,並保留最接近的一個:
Set xMaxLimit = Minimum value between P1x and P2x
最後,我檢測控製器的各邊是否被阻塞,如果是則速度設為0。如果沒有,則控製器將仍然被阻塞,但保持原來的速度值。所以,如果你突然想走另一條路,你必須等到相反的加速度彌補實際速度,最終將速度增加到另一個方向上:它使玩家感覺到“被卡住”。
解決辦法相當簡單:If controller position on x axis >= xMaxLimit – 1 (I added a 1 pixel buffer to prevent errors)
Set rightBlocked to true
If rightBlocked is true AND speed on x axis > 0
Set speed on x axis = 0
我想這是非常簡單和傳統的辦法吧……
重力問題 重力就是指,當控製器在空中下落時,每一幀,Y軸上的速度會在重力的作用下加上一個均勻加速度:
speed on y axis = speed on y axis + gravity acceleration * delta time
(在這裏我就不談delta time(時間增量)了,因為程序員知道那是什麼東西,而非程序員可以很容易在網上找到解釋。)
我現在不想在這裏詳細地解釋跳躍係統(因為我想另寫一文介紹)。這裏的重點是,當跳躍輸入被調用時,一個衝擊速度會在Y軸上被賦加給控製器,然後重力一步一步地抵銷高度,使控製器進入下落狀態。
然後我使用相同的係統來確定移動限製和著陸標記:
If raycast to the bottom hits a collision on point (Px,Py)
Set yMinLimit = Px
If controller position on y axis <= yMinLimit + 1
Set grounded to true
If grounded is true AND is not jumping
Set speed on y axis = 0
Else
Apply gravity (see above)
你可以看到上述代碼的一些變體: 1、如果底部方向到光線上有速度,則不必檢測,因為總是有速度(由持續的重力加速度產生)。
2、相同地,如果著陸,則不必檢測。
3、如果控製器正在跳躍,則在設置Y軸上的速度為0以前需要檢測:否則,當跳躍衝力被賦到Y軸上的速度時,它直接回到0,因為控製器在下一幀裏可能仍然貼著地麵。

2d Platformer-03
光源的重要性 這不是一個大問題,我認為對專業的程序員來說是小菜一碟。但我自己花了一些時間,我真的想在這裏分享一下我是怎麼理解它的:
一開始,我的光線是由控製器碰撞器的角產生的。問題是,控製器不能正確地處理右邊的碰撞。現在我知道它是一個代碼執行順序的問題,用下圖更容易解釋:

2d Platformer-04
後來我發現了一個解決辦法:光線從更遠處投射,以產生“緩衝區”:

2d Platformer-05
斜坡問題 斜坡……當想到碰撞係統時,我總是很怕斜坡。我怕到不得不去尋找“無斜坡”的做法!但是,我最終克服了斜坡障礙!
令人吃驚的是,在我整合基本的碰撞係統後,控製器居然能夠應付斜坡了。好吧,雖然處理得不是很漂亮,但至少不會卡在斜坡上了,這極大地鼓勵了我。事實上爬坡很好,因為X軸上的極限位置總是“推回”。然而,下坡很成問題,因為當玩家跑得非常快時,他會先在X軸上移動,然後受重力作用在斜坡上下落,這就產生了“彈跳”下坡現象!
另外,在老式2D平台遊戲中,玩家碰到斜坡的底部中心。但當我的基本碰撞係統運作時,控製器碰到的是斜坡地麵的最接近底部角度的地方。

2d Platformer-06
為了讓控製器固定在斜坡地麵上,我嚐試了多種解決辦法,從各種教程到自己購買方案,我最終決定使用比較簡單的一個。基本上就是:
1、檢測控製器是否與斜坡接觸
2、如果是,則直接設置Y軸關聯到斜坡碰撞的Y軸位置
檢測是否與斜坡接觸 首先,為了檢測控製器是否與斜坡接觸,我檢測它是否“在斜坡的上方”。為此,當一束底部光線發現碰撞時,我隻要檢測這個碰撞法向量和右邊的單位向量之間的差異:
If raycast to the bottom hits a collision on point (Px,Py)
If angle between collision hit normal vector and right unit vector differs from 90°
set slopeOnHitPoint to true
當兩束底部光線之一不能確定控製器是否真的在斜坡的上方時,是因為發生了如下圖所示的情況:

2d Platformer-07
所以為了確定是否在斜坡之上,以下條件之一必須為真:
1、如果左光線和右光線均檢測到斜坡,則控製器在斜坡之上
2、如果隻有一束光線檢測到斜坡,且作為命中點的斜坡被另一個命中點來得高
然後,為了確定控製器是否在斜坡上,我必須確定它是否接觸斜坡或在斜坡的上方。然而,我發現我需要更大的“緩衝區”來防止當控製器在斜坡上時退出它的“著陸”狀態。所以,修改法的偽代碼是:
If aboveSlope
Set groundCheckValue to yMinLimit + 5
Else
Set groundCheckValue to yMinLimit + 1
If controller position on y axis <= groundCheckValue
Set grounded to true
If grounded is true And is not jumping
Set speed on y axis = 0
If aboveSlope
set onSlope to true
Else
Apply gravity
Set onSlope to false
設置控製器Y位置 既然我已經知道控製器是否在斜坡上了,那麼接下我隻要確定它的位置就行了。
因為我希望控製器“呆在”斜坡的底部中心,從它的中心投射一束垂直向下的光線,且使用命中Y位置當作新的yMinLimit。

2d Platformer-08
然後,為了避免控製器產生彈跳下坡的現象,我拋棄了Y軸速度,直接設置控製器Y位置為yMinLimit(X軸上的速度從未改變):
If onSlope
Set controller y position to yMinLimit
Else
Set controller y position to actual y position * y speed * deltaTime
Set x position to actual x position * x speed * deltaTime
峰值問題 當所有這些棘手的小問題都似乎解決了,我又遇到一個新問題:頂點!事實上,當達到頂點時,控製器就被認為位於斜坡上,它繼續用來自中心的光線確定yMinLimit。所以,隻要這個中心超過頂點,控製器就會產生碰撞,yMinLimit如下圖所示:

2d Platformer-09
作為開發者,我必須承認當時我不知道我是否希望我的遊戲中出現這種碰撞……但我不想因為自己不能處理它們就回避它們!
事實上,我沒有找到這個問題的清楚解決辦法,但我所選擇的做法似乎也蠻管用的……
首先,我檢測控製器是否在斜坡的上方,也就是頂點在左邊:左光線是否檢測到斜坡,且介於左命中點與中心命中點之間的距離是否大於5(任意值,取決於對貼圖大小、控製器碰撞器的大小的測試)。
最後,如果控製高於頂點,我就使用其他邊的光線命中點來確定yMinLimit。

2d Platformer-10
這個辦法並不完美,因為中心光線和邊光線之間沒有過渡,不能很精準地確定yMinLimit,且它產生一個有點兒怪異的切換。但我仍然希望有一天能找到更穩妥的解決辦法。
結論 總之,在自己處理碰撞問題上,我確實遇到很嚴峻的考驗;但結果滿足了我的要求。如果某些程序員看到本文能給我一些反饋,我會很高興的。我還要問自己是否有更複雜的、處理其他類幾何形狀的碰撞係統也使用了類似方法?如果你知道,就請滿足我的好奇心吧!