獨立遊戲開發中的物理系統

王寅寅發表於2020-06-10
作者:王寅寅

注:本文選自機械工業出版社出版的《獨立遊戲開發:基礎、實踐與創收》一書的小節,略有改動。經出版社授權刊登於此。

獨立遊戲開發中的物理系統

Unity物理系統更準確的叫法應該是物理引擎(Physics Engine),該引擎是採用NVIDIA的PhysX物理引擎實現的,為避免與遊戲引擎本身的名字衝突,本書還是稱其為物理系統。所謂物理系統,是指在遊戲物件上實現加速度、碰撞、重力、摩擦力及各種外力作用的一系列功能集合。Unity物理系統又分為2D和3D兩種型別,兩者在使用上大體相似,主要區別是3D物理系統多了一個維度。

Unity物理系統沒有總開關,只要在遊戲物件上附加並正確設定了物理元件(如Rigidbody、Collider、Joint、Effector等元件),即使用了物理系統功能。下面我們繼續開發案例遊戲,並基於物理系統實現主角的移動、跳躍、自由落體及更復雜的碰撞檢測等功能。

遊戲物件調整

對於Road,我們需要將其調整為一個有一定距離且主角可以站立的路面。首先將Road的Sprite Renderer元件Draw Mode屬性選擇為Tiled(在該屬性下,影像會根據遊戲物件尺寸自動填充,就像連續的瓦片一樣),然後在場景中拖曳Road的左右邊框(需要確保工具欄中的變換工具為Rect Tool狀態),適當增大其寬度後即可得到一個連續的路面。接下來調整碰撞器範圍,在Box Collider 2D元件中單擊Edit Collider按鈕後,碰撞器範圍即進入可編輯狀態,調整完後再次單擊Edit Collider按鈕即可。我們還需要取消碰撞器的Is Trigger屬性,以保證主角與路面的碰撞不可穿透,此時儘管Road並未附加Rigidbody 2D元件,但它相當於一個Static狀態的剛體。另外,之前的Road指令碼已經不適用了,我們將其對應指令碼元件從檢視視窗移除,並將該指令碼檔案從專案視窗刪除即可。如圖1和圖2所示:圖1展示了檢視視窗中Road的相關元件情況,標註框中為相關的調整項;圖2展示了Road在場景檢視中的情況,注意其碰撞器範圍是一個極細的矩形綠色框(圖中可能不容易看出來,請讀者結合實際操作檢視),我們將該範圍上邊框調整在Road高度二分之一的位置,對應馬路中央,也是遊戲角色的水平落腳點。

獨立遊戲開發中的物理系統
圖1    Road遊戲物件相關元件情況

獨立遊戲開發中的物理系統
圖2    調整後的Road遊戲物件

注:在圖1中,有一個三角形警告符號,其內容提示我們:當前本Sprite影像資源的匯入設定可能會造成Tiled模式下的繪製錯誤。但很明顯,我們這裡並未出現繪製錯誤,筆者在實際工作中也尚未遇到過此類錯誤,忽略該警告即可。或者,可在該Sprite的影像資源匯入設定中,將Mesh Type屬性設定為Full Rect以消除該警告。

對於Player,我們需要讓其擁有重力以及合適的碰撞範圍。首先將Rigidbody 2D元件的Gravity Scale屬性設定為4,以接受該值大小的重力等級。接著重新選擇碰撞器,由於主角有一個近似圓形的外觀,因此可用圓形的Circle Collider 2D元件替換Box Collider 2D元件,並適當調整其範圍大小,如圖3所示。

獨立遊戲開發中的物理系統
圖3  調整後的Player遊戲物件

對於RoadBlock,可用類似方法調整其碰撞範圍並刪除RoadBlock指令碼即可,具體步驟這裡不再贅述。

渲染順序修正

我們先執行遊戲,可以看到主角會因重力向路面下落,最終被錯誤地顯示在馬路後面,如圖4所示。要修正此問題,我們需要了解下Sprite Renderer元件的Sorting Layer與Order in Layer屬性:Sorting Layer屬性中可新增一系列特定名稱的排序分組,Unity將按照組順序依次渲染其中的Sprite;當多個Sprite同屬一個Sorting Layer分組時,則可通過Order in Layer屬性的值大小來決定它們的渲染順序。

值得注意的是,Soring Layer與Layer雖然只差一個單詞,但在Unity中它們是兩個不同的概念,可閱讀書中第5章中有關Layer的簡要介紹。另外讀者應知曉,Sprite Renderer元件功能不屬於物理系統功能。

下面,我們開始調整Sorting Layer與Order in Layer。首先,在Sorting Layer中新增一個Player分組:任意選擇一個遊戲物件,在檢視視窗中單擊Sorting Layer屬性右側的Default按鈕,並在展開的下拉選單中選擇Add Sorting Layer選項,即可開啟標籤與層的有關設定,其中,Sorting Layers欄下預設僅有一個Default分組,右下角的加減號(“+ -”)可增減分組,拖曳左側的等號(“=”)則可調整組順序,這裡我們新增一個Player分組,並保持現有順序即可。然後為Player的Sprite選擇該分組:單擊Player遊戲物件,直接將其Sorting Layer屬性右側的選項選擇為剛才新增的Player分組即可。接下來,Road與RoadBlock之間同樣需要調整渲染順序:將RoadBlock拖曳到Road上,可看到錯誤的前後關係,此時保持兩者的Sorting Layer同屬預設Default分組,我們保持Road的Order in Layer屬性為0,再將RoadBlock的Order in Layer屬性設為1,即可修正渲染順序(RoadBlock為0,Road為-1也可以)。圖5展示了調整後的執行效果。

獨立遊戲開發中的物理系統
圖4   錯誤的渲染順序

獨立遊戲開發中的物理系統
圖5    修正的渲染順序

基於物理系統的移動

這裡我們修改PlayerController指令碼,將當前基於Transform元件的移動替換為基於物理系統功能的移動,如程式碼1所示。

程式碼1   PlayerController.cs

  1. using UnityEngine;

  2. public class PlayerController : MonoBehaviour
  3. {
  4.     // 用於引用Player的Rigidbody 2D元件
  5.     private Rigidbody2D body;
  6.     // 表示主角的移動速度
  7.     public float speed;

  8.     private void Start()
  9.     {
  10.         // 獲取Player的Rigidbody 2D元件
  11.         body = GetComponent<Rigidbody2D>();
  12.     }

  13.     private void FixedUpdate()
  14.     {
  15.         KeyboardControl();
  16.     }

  17.     private void KeyboardControl()
  18.     {
  19.         // 通過鍵盤左右鍵輸入乘以速度變數得出水平速度
  20.         float sp = speed * Input.GetAxis("Horizontal");
  21.         // 根據水平速度和應有的垂直速度影響剛體速度
  22.         body.velocity = new Vector2(sp, body.velocity.y);
  23.     }
  24. }
複製程式碼

上述程式碼中的GetComponent是一個泛型方法,用於獲取已附加元件,尖括號內為該元件的具體型別,這裡我們獲取Player的Rigidbody 2D元件,並由body變數引用。velocity是Rigidbody 2D元件中一個屬性,代表當前剛體的移動速度,我們把一個匿名二維向量賦值給它,即可實現剛體速度驅動遊戲物件的移動。在這個匿名二維向量中,X維度值即左右方向鍵輸入值與speed變數的積,Y維度值則對應剛體當前在該維度應有的速度(也就是說,我們僅控制水平速度,而不直接控制垂直速度,垂直速度將由外力實現,例如,下落時由物理系統重力產生向下的速度;跳躍時由人物跳躍力產生向上的速度)。

注:對精準名詞解釋一下,velocity表示速度,speed表示速率。速度是有方向和大小的向量,而速率是沒有方向的值。在沒有特別說明時,本書把兩者都稱為速度。

基於物理系統的跳躍與碰撞

我們繼續編寫PlayerController指令碼程式碼,為主角新增跳躍能力,並使用更復雜的碰撞檢測來判定其是否站立在地面上,如程式碼2所示。

程式碼2    PlayerController.cs

  1. using UnityEngine;

  2. public class PlayerController : MonoBehaviour
  3. {
  4.     // 用於引用Player的Rigidbody 2D元件
  5.     private Rigidbody2D body;
  6.     // 表示主角的移動速度
  7.     public float speed;

  8.     private void Start()
  9.     {
  10.         // 獲取Player的Rigidbody 2D元件
  11.         body = GetComponent<Rigidbody2D>();
  12.     }

  13.     private void FixedUpdate()
  14.     {
  15.         KeyboardControl();
  16.     }

  17.     private void KeyboardControl()
  18.     {
  19.         // 通過鍵盤左右鍵輸入乘以速度變數得出水平速度
  20.         float sp = speed * Input.GetAxis("Horizontal");
  21.         // 根據水平速度和應有的垂直速度影響剛體速度
  22.         body.velocity = new Vector2(sp, body.velocity.y);
  23.     }
  24. }
複製程式碼


在上述程式碼中,我們新增了onGround與jumpPower變數,並使用了OnCollisionStay2D與OnCollisionExit2D方法。onGround變數是一個布林值,用於說明主角是否站立在地面上(或其他可站立物體上),當該值為真時,我們使用GetAxis("Vertical")獲取上下方向鍵輸入值,並在輸入值大於0時(向上的方向)呼叫AddForce方法在剛體上增加一個瞬時的力,該力是一個匿名二維向量,作為引數傳遞給AddForce方法,我們這裡只需要一個向上的力以產生向上的速度,因此該二維向量的X維度值設為0,Y維度值則設為代表跳躍力大小的jumpPower變數。

OnCollisionStay2D方法會在Player始終與碰撞物件接觸時連續執行,我們使用它檢測主角是否站立在地面上,其中,contactCount屬性儲存了Player與某個碰撞物件之間碰撞點的數量(多數情況下為1),我們用cnum變數儲存該數量,並結合for迴圈與GetContact方法遍歷所有的碰撞點;contact變數則用於依次儲存遍歷結果,每一個碰撞點都有一個normal屬性,該屬性是一個法線向量(這裡該向量是一個長度為1,並與Player碰撞點切線垂直的二維向量),當該向量Y維度值為1時,Player的碰撞點必然在正下方,即站立在地面上。這裡我們將該值的判定標準設為0.8,以考慮碰撞點稍稍偏離正下方的情況。OnCollisionExit2D方法會在Player脫離任意碰撞時執行,我們直接在其中將onGround變數賦值為假,即說明Player處於懸空狀態。

若此時執行遊戲,主角將會以滾動方式移動,這不是我們想要的效果。可在Rigidbody 2D元件中展開Constraints選項並勾選Freeze Rotation Z屬性以解決此問題,如圖6所示。最後在檢視視窗的Player Controller指令碼元件中,為Speed與Jump Power屬性分別設定一個合適的值(這裡我們設定為3和150),即可執行遊戲測試最終效果。

獨立遊戲開發中的物理系統
圖6  在Rigidbody 2D元件中鎖定Z軸旋轉

相關文章