開發類爐石的3D卡牌遊戲Demo的階段性總結(一)

一個開發者的點滴積累
使用工具:UE4 (4.20.3)
使用資源:Dungeon_Areas(付費)、ParagonShinbi(免費)、ParagonSunWukong(免費)
開發耗時:3756分鐘
執行流程
開啟遊戲,先進入遊戲開始介面,介面上有兩個按鈕:遊戲開始和退出遊戲。
點選遊戲開始,載入場景,場景中有兩個英雄,我的介面上有3張牌,以及回合結束按鈕。
選中牌,然後移動到戰場中,召喚一個隨從。拖拽自己的英雄到對方的英雄身上,自己的英雄會走過去,攻擊,然後退回來。
點選回合結束按鈕,然後敵方英雄進行攻擊。攻擊的時候顯示血量減少。然後回合結束。
再由我進行攻擊,然後遊戲結束,顯示結束介面。然後退回到開始介面。
目前開發完成的功能是:
拖拽自己的英雄到對方英雄身上,自己的英雄走過去,攻擊,然後退回來。
開發過程與反思
遊戲模式
原本沒有在意遊戲模式,因為幾乎所有的教程都只會告訴你遊戲模式設定成什麼樣子,至於遊戲模式是用來做什麼的都不會提。我也是在機緣巧合之下才會仔細地閱讀了一遍遊戲模式的文件,雖然沒有解決我遇到的問題,但是也讓我對遊戲模式和遊戲狀態有了一個很好的瞭解,這一節就是來介紹遊戲模式的。
先來看看UE4文件中時如何介紹遊戲模式的:
即便是最開放的遊戲也有一些基礎規則,這些規則在UE4中被稱為 遊戲模式(Game Mode) 。對大多數關卡來說,基礎規則包括:
- 當前玩家和旁觀者數量,以及允許的最大玩家數和旁觀者數。
- 玩家如何進入遊戲,這包括選擇出生點的規則,以及其他出生/重生行為的規則。
- 遊戲能否被暫停,如果可以被暫停,那麼如何處理暫停。
- 關卡之間如何切換,遊戲是否要從電影模式啟動。
你可以看到,這些規則真的很基礎,基礎到基本不用理會的地步,事實上,我們真的不需要花太多時間在上面。
大多數情況下,我們的遊戲需要一個自定義的Game Mode。而這個Game Mode通常是一個繼承自Game Mode的藍圖類。

這是我們可以重新定義類的一部分,這個專案裡,只需要重新定義兩個類就行了:Player Controller Class和Default Pawn Class。沒錯,就是後面有黃色返回箭頭的那兩個。UE4真是太人性化了!
Player Controller是用來定義如何操作的類。UE4預設不會顯示滑鼠,要顯示的話就需要定義一個Player Controller類,然後將滑鼠控制的相關操作開啟。不過說實話,對Game Mode,Player Controller和Default Pawn三個東西還是自己重新定義來的心安。

新建的Player Controller將滑鼠操作全勾選上
最後是character。這裡的character並沒有實際的意義,遊戲本身是一個策略遊戲,一個上帝視角的遊戲,不是隻操作一個character的遊戲。所以,這裡的character是一個空類,佔個位罷了。
觀察視角
官方文件中指出:如果你的遊戲中沒有一個Player Start(玩家起始)物件,那麼玩家會從座標(0,0,0)的位置開始。所以,不管需不需要,最好還是在地圖中放一個Player Start物件。
不過我在這個專案裡就沒有放,因為我覺得不需要的東西就不用存在。
遊戲需要一個45度俯視的觀察角度,所以在場景中加了一個CameraActor。有兩種方法設定這個視角,第一種方法是呼叫Set View Target with Blend函式,將Camera Actor啟用。這種方法的特點是非常靈活,如果你需要切換多個攝像機的話可以使用。第二種方法是直接在CameraActor的細節面板上把Auto Activate for Player設定成Player 0,就跟下面的圖片一樣:

這種方法就是簡單,如果不需要切換攝像機,那麼這種方法是最合適的。顯然,我採用的就是這種方法。
邏輯控制
如何讓角色響應滑鼠的控制呢?
首先需要明確,有多少的滑鼠訊息要響應。滿打滿算只有四個訊息:懸停、不懸停、按下、釋放。滑鼠移動到角色上時,需要明確地提示玩家這個角色是可以操作的,所以,滑鼠的樣式需要改變。當玩家按下左鍵,滑鼠樣式也要改變,示意玩家已經抓住了這個角色。當玩家抓住這個角色的時候,將滑鼠移動到其他地方,角色的朝向就會改變。此時,鬆開滑鼠,角色就會移動過去進行攻擊。
兩個關鍵點:1、如何判定已經抓住角色?2、如何計算角色的朝向?
1、如何判定已經抓住角色?
滑鼠能抓住角色只有角色在原點的時候才有效。所以,角色有一個初始狀態,Idle(一開始的時候用一個bool變數IsAtOrigin來判斷角色是否處於原點,隨著玩家的狀態越來越多,發現這種方式非常不適合,於是改成用玩家的狀態來區分,簡潔大方)。滑鼠必須在角色上(通過IsOnCharacter來判斷),然後按下左鍵才能算是抓住(將這種狀態儲存到變數IsGrabbed中)。具體的邏輯如下圖所示:

判斷抓住的邏輯
2、如何計算角色的朝向?
要計算朝向,需要一個前置條件,那就是能否觸發。觸發的條件有兩個,其一:角色必須被抓住;其二:滑鼠移動的距離必須大於一定數值(這裡是200)。兩個條件都滿足後,觸發這種狀態就會被儲存到IsTroggleOn變數中。

計算是否觸發
如果能觸發,還需要計算角色的旋轉角度。方法是這樣,先在滑鼠位置與角色位置之間拉一個向量(滑鼠位置-角色位置),將這個向量標準化後,計算與角色朝向向量之間的點積,這個點積就是旋轉角度的cos值。然後通過acosd函式將其轉換成角度,特別要注意的是,輸出的角度需要有正負,用來區分角色是從當前朝向往左轉(加負值)還是往右轉(加正值):

計算旋轉角度
算完角度後,更新角色的朝向就簡單了,只需在原來的旋轉角度值上加上計算出的角度,像這樣:

設定角色角度
整個重新整理流程就是這樣:

重新整理流程
角色動畫
對於角色動畫的實現,真的是走了很多彎路。我是一個做棋牌遊戲開發的程式員,腦子裡的思維模式是觸發式思維。
觸發式思維就是如果要產生什麼動作,必須有觸發這個動作的一次呼叫
但是3D遊戲不一樣,3D遊戲的場景永遠處於重新整理之中,所以角色的動畫應該是輪詢式(每次重新整理時都獲取當前的狀態,來決定需要繪製成什麼樣)。所以一開始我做的時候對動畫的控制都是放在關卡藍圖裡,然後直接設定Animation Blueprint。這種方式如果場景中只有一個角色還能應付,多兩個就應付不來了。要意識到這兩種模式之間的區別真不是一件容易的事情,我的運氣真是太好了。
播放角色動畫有兩個關鍵點,一是如何切換角色的動畫,二是如何更新角色的位置。
如何切換角色動畫?
官方推薦做法:使用Animation Blueprint來控制動畫。方式是從新建目錄中定位到動畫->動畫藍圖,然後在彈出的視窗中選好目標骨架,這樣就建立好了一個動畫藍圖(Animation Blueprint)。
在動畫藍圖中,最適當的方式應該就是建立一個狀態機,讓動畫藍圖根據當前的狀態來播放適當的動畫。角色有六種狀態,分別是:待機、前進、停止前進、攻擊、後退、停止後退。狀態切換的順序也是按照這個順序,也就是說,角色只能順序切換這些狀態,不能跳狀態切換(例如從待機到攻擊)。在狀態機中的表現就是這樣:

動畫狀態切換的狀態機
切換的條件分別是:
- Idle -> Moving:角色狀態變成MovingFwd(前進)
- Moving -> Stop:角色狀態變成FwdStop(停止前進)
- Stop -> Attacking:前一個動畫剩餘時間<=2.5秒
- Attacking -> MovingBack:前一個動畫剩餘時間<=0.5秒
- MovingBack -> BackStop:角色狀態變成BwdStop(停止後退)
- BackStop -> Idle:前一個動畫剩餘時間<=2.5秒
因為從停下到攻擊到返回時一套完整的流程(停止後退到待機狀態也是這樣),所以不需要根據狀態改變來切換,動畫放到一定程度就可以到下一階段了,而且這時候需要將動畫的狀態同步給角色,因此還需要新增一些動畫事件。

動畫事件
動畫事件有3個:進入攻擊狀態、進入返回狀態、進入待機狀態。這些事件可以在狀態上新增,也可以在狀態之間的切換操作上新增。下圖就是在Attacking上新增的事件:

進入攻擊狀態
動畫狀態的切換需要讓角色知道,動畫藍圖也需要從角色藍圖中獲取狀態以便選取適當的動畫。前者可以通過定義一個事件排程器(ShinbiAnimationsStatus)然後呼叫來實現,後者可以在每一幀都會呼叫的Update事件裡去獲取角色狀態。

從角色中獲取狀態
如何更新角色位置?
角色位置更新首先需要在前進或者後退狀態才會有效。通過計算原點和目標點之間的向量,標準化之後乘上一個速度,再加到當前位置上就非常容易實現:

位置計算
前進與後退的區別也就是向量的指向不同了而已。
設定角色的位置,呼叫SetActorLocation即可,另外,當玩家到目標位置或者玩家回到原點之後,需要更新一下玩家的狀態,這個操作由Calculate Character Status函式來完成。

更新位置
專案管理
U盤。資源實在太大了,沒法上傳github,甚至連碼雲都沒法上傳(碼雲單個檔案限制大小為100M)。
當前進展
目前要做的就是角色之間的碰撞(collision)響應。只是我對碰撞的瞭解甚少,所以需要仔細的閱讀碰撞相關文件,要完成相關的碰撞功能,時間估計不會低於500分鐘。
總結
想要儘快地實現功能,直接參考已有的專案並不是最快的方法(如果沒人解釋為什麼要這麼做的話),最快的方式還是老老實實地閱讀文件,對整個功能有一個完整的理解。
參考資料:
ofollow,noindex">官方文件(英文)