前往
大廳
主題

卡牌遊戲練習筆記——改善序列化問題

pudding | 2022-02-21 21:10:03 | 巴幣 1112 | 人氣 333

最近在思考著要怎麼製作卡牌遊戲,而途中在將遊戲資料轉換成Json的部分時遇上了一些困難,因此就想說來把過程記下來做一些相關的筆記吧,特別注意這篇文章跟之前的相比會比較沒那麼仔細,它會更具焦在過程中想到了什麼樣的問題以及解決辦法。

首先想到的問題是究竟要怎麼實作每張卡牌的效果? 雖然每張卡牌都有各自的數值與邏輯,但是如果我們每一張卡牌都特別寫一個類別出來的話那似乎又太雜亂,試想一下如果今天像魔法風雲會那樣每次有新的系列出來都要添加可能兩百張左右的新卡,在專案裡宣告這麼多的類別似乎不是個明智的選擇。

因此相比起讓卡牌擁有自己的邏輯,我會更傾向於將卡牌設計成只帶有資料的物件,每一張卡牌都是帶有不同資料的CardData類別 (或著更精確來說,是其子類別SpellCard或MinionCard,代表法術或手下)
而當你打出卡牌時,你會把卡牌的資料傳給遊戲系統,交給後者去判斷該怎麼處理。

講的具體一些吧,以我自己的設計為例,卡牌的基底(CardData)帶有
字串:
-CardID
-CardName
-CardDescription
整數:
-MpCost
自行宣告在CardData中的enum(列舉):
-CardType (卡牌類別,手下或法術,但後來其實沒有在用)
-CardColor (這一個其實是Flags,因為一張卡可能同時有多種顏色)

而卡牌的子類別——法術牌(SpellCard),則額外帶有一個CardData.ActionBase物件(宣告在CardData之中),它代表著卡牌執行的動作 (如造成傷害、抽牌等)
-CastAction (代表施法時會執行的動作)

另一個子類別——手下牌(MinionCard),除了有較多的額外資料,它也有另外實作了ICharacter介面,定義了手下跟玩家一樣都是有生命值跟陣營屬性的類別,並且也因為有的手下在打出後會有額外效果 (例如爐石戰記中的戰吼),因此手下目前也有一個ActionBase

到了這裡,我們先不要急著深入去看ActionBase有著哪些資料或函式,先來想想看這些卡牌的序列化問題。
為什麼突然要講序列化呢? 這是因為我並不打算要把這些卡牌的資料全部存在專案的程式碼裡,二來是考量到如果未來多了很多不同系列的卡牌,那是否可以做到只在需要某個系列的牌時,才把該系列的卡牌載入進來。
因此才會希望能透過序列化的方式將卡牌資料轉成外部文檔(這邊使用Json),然後需要的時候再讀取並轉換回卡牌資料。

首先浮上來的第一個問題是多型的轉換,假設你今天有著一個CardData陣列,裡面有著各個不同的手下跟法術,但是你在轉成Json的時候格式卻會是固定的 (只會將父類別CardData的資料轉換)
這部分可以參考看看官方的說明,文章在開頭中便提到C#並不支援多型的序列化

不過官方有提到可以在轉換成Json的時候一律將物件看成object類別來處理(Unity中的JsonUtility.ToJson貌似預設就是這麼做)
這樣的話你的所有資料都會成功被轉換,但是接下來還會有反序列化的問題,當我們讀取Json的時候還需要能知道當下那個物件是屬於哪一個類別。

因此我就找到了下面這篇教學

在該教學影片中,它實作了ISerializationCallbackReceiver這個介面來幫助自己客製化轉換資料的流程,其中還需要再額外實作OnBeforeSerialize()和OnAfterDeserialize()這兩個函式來定義自己序列化前(轉出)和反序列化後(轉入)要額外進行的動作。

接著我們再看到變數的部分,這裡的三個變數分別是
-List<ItemInstance> items (ItemInstance是道具的資料,特別注意他刻意加上了NonSerialized標籤,因此你不會在轉出的Json文檔中看到他)
-List<string> _serializedItems (這一個存的是將道具序列化後的Json字串,也就是上一個items串列)
-List<string> _itemType (這一個存的是每個道具的實際類別,遠程或進戰,用於在反序列化的時候可以分辨當下的資料應該被看做哪個子類別)

我們可以在影片中看到他序列化後的結果
然後可以再看到反序列化後,Unity這邊會再接著把那些Json字串各自轉成類別字串中對應的類別並存到ItemInstance列表中

但是這麼做的話有一個小問題,物件身上的enum在轉換成json的時候會以數字的形式呈現,舉CardData為例的話,其中有著EColor這樣的Flags enum

如果今天有一張紅藍的雙色卡牌,它在轉成Json後顯示的數值會是3,但我在文檔中看到3的時候會很難聯想到說他是雙色卡,在一般情況下這並不是什麼大問題,但是在這裡我會希望輸出的文檔能夠讓企劃人員方便修改,甚至如果之後要新加某張卡牌時,他們也可以直接在文檔中添加新的卡牌資料。
(例如像我目前做的效果)

因此我後來新增了另一個CardTemplate類別來表示卡牌的詳細資料 (主要用於序列化),他會把所有類別的卡牌都需要的資料欄位列出,並在讀取(反序列化)的時候依照CardType去決定自己要轉化哪些欄位或忽視哪些資料

而序列化則主要發生在CardTable這個類別上,該類別實作了稍早提到的ISerializationCallbackReceiver,並且存有需要被轉成Json的TableID和可以給別人編輯的CardTemplates這個列表
沒有被序列化的CardsDict則會在序列化前/反序列化後被用到,前者將我們程式裡自己生的卡牌資料 (可能工程師在實驗某些新效果) 轉成Template好展示在Json中,後者則將Json讀進來的Template資料轉回卡牌資料存到Dictionary中 (但是我們這裡先不深入討論怎麼轉換)

另外也採用了工廠樣式(Factory Pattern),額外新增了一個CardFactory靜態類別,他提供了一個GetCardCloneByID的函式,只要把卡牌ID帶入他就會生成那個ID所對應之卡牌的深層複製。 (使用深層複製是為了避免異動到原有資料,遊玩過程可能會需要多次生成某張卡牌)
(對,我知道我的架構圖畫的很醜XD)
這裡的ID設計會先拆出TableID(系列名稱,顏色),看一下你要的卡表是否有載入到Dictionary了,有的話就順著過去問那張卡表有沒有此卡牌ID對應的資料。
之所以選用工廠樣式的主要原因是要省去每次生成卡牌的麻煩,這樣的話其他類別如遊戲系統就只需要知道玩家的套牌是由哪些編號的卡牌組成就可以直接請CardFactory幫忙製造了。
(原本生成每張卡牌都需要準備一大堆的資料)

好啦,怎麼改善序列化的問題這次就先寫到這,如果覺得有其他更好的方法或著希望我再深入分享的也歡迎提出。
我目前練習的感想覺得卡牌遊戲比想像的還要難做不少,例如現在的卡牌資料也還有一些沒想好的部分,估計未來Template那邊也會有需要新增的欄位,到時候還需要思考舊有的資料會不會受影響,希望到時候會比較順利……雖然也要我有試著繼續做下去啦XD

題外話,我之前也有找到關於C# JsonConverter的方法
但是Unity看起來無法直接使用的關係,因而作罷

創作回應

更多創作