前往
大廳
主題 達人專欄

[Unity x 資料驅動 #1] 模組化遊戲最高境界,使用XML、DLL等徹底分離資料與邏輯。

%%鼠 拒收病婿 | 2022-10-24 01:25:46 | 巴幣 8208 | 人氣 1748

前言:

舉例來說,動作遊戲的人物基本都有跑、跳、攻擊、閃躲等方法,以前
我做了一個工具來集中控制方法觸發的順序:開發者僅需在控制腳本上先預設幾種動作變數,填些他們的參數,之後就交給這個工具去排執行順序與冷卻時間等等。如此一來,開發者就可以專心地在呼叫動作與其他方法宣告,不用去管誰先誰後,不用去判斷該不該中斷目前行動等等問題。

但它有個缺點,就是資料帶不走!  也就是這個「腳本跟上面的參數」和物件綁死了,一個不小心可能會因為Unity的Reload導致參數不見,或移植別的物件時某些參數又要重新調整等等,時時都活在「參數」會不會不見的恐懼中!  (其實也沒那麼嚴重。)

因此,隨著這次的新開發,就來做個新架構解決這個問題吧!


舊工具的描述,都要成為過去了就不用認真看日文打甚麼了。  主要是在說明工具是如何透過「優先度」等參數控制玩家物件從Idle > Jump Start > Jump epic > Jump Fall > Jump End動作的,開發者僅需定義各動作的播放的動畫與參數,接著就交給此工具去排執行順序。

照慣例偷偷宣傳個人網站


新架構,新概念!
以前的文章有介紹過「邏輯分離的資料驅動」,那時候我是在Unity打包好方法接口,以物件的方式傳給lua(分離邏輯端),優點是Lua簡單上手、能像文本一樣跟物件打包帶走,缺點是接口必須事先定義好,且Unity不支援Lua使用動態include (在該文章中我用一個管理腳本回傳所需的接口模組,去模仿include的動作),且真正的"方法"還是在Unity內,Lua只是分離了「使用方法的邏輯」而已,若哪天要改還是得從內部改。

因此,我思考如何將掛在物件上的腳本(包含類別變數的方法、呼叫等)做成分離的資料。最好的方法應該是做成dll,但若dll需要跟遊戲內的資料互動的話,就也得在dll去ref那個架構才行。 到最後會變成一群dll檔去做互動,遊戲只是載入與執行的容器。

最終設計出這個架構,以下慢慢解釋:



DLL是甚麼?
Windows 用戶在自己裝函式庫到開發環境時應該常常看到「dll遺失」的error吧。 DLL (Dynamic-link library (動態連結函式庫)),可以說是打包的函式庫,直覺來說跟exe差別在於exe啟動後他會去直行進入點的方法(例如Main()函數),而dll就是可以用來存放方法的容器,需要時再從裡面找。

例如之前的專案,我在Unity中調用的OpenCV C++函式庫就是在跟Dll互動喔。

延伸閱讀:動態連結程式庫 (DLL) - Windows Client | Microsoft Learn

XML是什麼?
XML(可延伸標記式語言(Extensible Markup Language)),XML跟HTML一樣是標籤語言,常見於專案設定檔、網路API等等,詳細就不講古了單刀直入:「為什麼用XML?」

與最常拿來比較的"Json"來說,XML比Json老、比Json胖,但格式很明確、生態系完整。
檔案、變數多了之後你可能會拼錯字、漏打等,若用Json就會Debug到死,當然可以自己做一個Json檢查器,但何不直接用XML的呢? XML有格式檢查語言DTD與Schema,前者比較老、簡單但能檢查的格式限制多,後者導入了類別的概念,能建立起縝密的架構。

例如DTD定義一性別屬性:

換成Schema寫法:


現在懂DTD比較「簡單」的意思了吧XD

不寫格式檢查可以嗎?

可以,執行上沒差,但痛苦一下寫完檢查檔,之後打xml就能有香香的自動提示功能喔 (VSC裝套件會自動檢查)。
 

實際使用

說明:遊戲中有兩個可互動物件:玩家跟大白球。 大白球平時會跟著玩家,玩家可以使用攻擊鍵1往前方丟出大白球,或用攻擊鍵2近戰打擊大白球。 若近戰打到大白球則玩家會往上彈。 再加上其他零碎功能如:播放動畫、打擊效果、打擊事件、落地檢查等等,「全部」都是分離邏輯處理的喔!  實際Unity內的物件上只有簡單掛著讀取Xml的Actor元件。



建立Interface

這邊的Interface是指系統與資料之間的溝通介面,類別並不是真的屬於Interface。
為了確保介面的獨立性,我創了一個新project。
 
定義介面類別:

為何使用partial class?
  • interface / abstract class無法被xml Serialize解析。
  • partial 比concert class 多一點擴充空間。

Unity端Load XML


簡單的範例XML文本:


讀取成功!
 

必要的耦合:Unity.dll

定義好溝通橋梁後開始寫獨立腳本,為了能操縱物件,勢必需要使用的Unity的類別與方法。
在UnityEditor下找到Unity.dll檔,加入project的ref清單。
來源:Unity and DLLs (what-could-possibly-go-wrong.com)
 

基本方法 和 傳遞資料

XML定義呼叫名為"Idle"的方法。

XML內容

獨立腳本中定義Idle 的內容:

結果
 

動態載入dll並呼叫方法

邏輯:載入dll > 取得dll裡面定義的類別 > 實例話類別 > 呼叫方法。

範例: 傳遞GameObject資料

定義Setup函數,將Unity GameObject以 System.object的格式傳送。



結果:
 
 

後記:
篇幅有點長,先分段,下一篇會講解 「事件(event)」的邏輯分離喔!
偷偷透漏架構圖:
 

後記2:
如果撇除最近身體抱病,認真算起來這個工具開發2-3天(包含demo物件的方法),以往製作一個角色動作要2天,現在只要2小時。  感覺這樣程式的部分很快就搞定了,有、有點想...找有經驗的美術一起做QQ  ((設計好難....



 
送禮物贊助創作者 !
0
留言

創作回應

幼蟲
如果你的意思是有兩份dll,一份是原本類別定義所在,一份是擴充該類別的話
只能說不可行,編譯會先失敗,partial的限制是同個類別的partial class必須在同一個assembly
類似的問題在stackoverflow上也有
https://stackoverflow.com/questions/647385/is-it-possible-to-have-two-partial-classes-in-different-assemblies-represent-the

又如果你的意思是一份dll兩個partial class的話,那我會建議直接合併成一個完整的class就好
2022-10-27 06:50:26
%%鼠 拒收病婿
原來如此 感謝提供。
話說還有一個想請教,這邊的IActor類別一開始是定義為Interface,只是後來要做XML解析時發現不支援介面型態,只好改成class,這種「想把它當介面使用,但礙於情況只能定義為類別」的時候,命名規則會是依照實際型態(class)還是意圖(interface)呢??
2022-10-27 14:14:17
幼蟲
這個問題的更前面還有一個問題
通常會使用Interface的目的大致有兩種: 1.依賴反轉 2.多型
再來就是一些偶有奇效的特殊用法會用Interface
所以更前面的那個問題會是: 為什麼你要用Interface? 是甚麼情境或理由讓你覺得Interface會帶來更多好處?
2022-10-27 23:24:13
%%鼠 拒收病婿
的確@@,我不常用到interface型態,大多是遇到一些限制後偷懶又改回class,只有幾次練習替自訂類別用過IEnumerator。 Interface給我的感覺像是"為了執行某功能所需資料而制定一個小的結構標準。" 能確保某類別對於該功能接口的完整性,而同時Interface的特性讓人不會想輕易修改它結構,很明顯地提醒自己「它只是一個資料交換的結構」。

所以我想,就這個案例而言 Interface是一個比較嚴謹但廣用的、對特定功能的資料交換結構,至於好處,除了提醒自己不要亂增胖它之外好像就沒了。[e17]
2022-10-28 20:10:14
幼蟲

這樣看起來你很有可能誤用了interface
interface和abstract class的目的之一是將行為與資料抽象化(多型的運作原理)
抽象化則是為了本質的放大和消除不相關的事物
也因為如此所以你無法對上述兩個東西new出一個物件(沒有確切的型別資訊與實作細節)
這也是為什麼你的XML無法反序列化成interface
因為預設的解析器不會知道你想將資料反序列化成甚麼類別(做得到的話堪稱通靈等級的解析器XD)

最後就你的用例,這會比較偏向所謂的Contract(合約)
兩邊說好使用介面,然後不能輕易更動,甚至是不能更動
這裡說的介面是概念性的,不一定是interface,一般類別的簽章也等同介面的概念
在設計上我會將Contract類別或interface集中放到一個叫做Contracts的資料夾下
要是動到這資料夾下的東西就會知道另外一側有相當大的機會要跟著改
這種約定特定資料夾用途的做法是"約定優於配置"的應用
2022-10-28 22:21:31
%%鼠 拒收病婿
學到了,謝謝! [e16]
請問會介意我之後有空在這塊多做一篇學習探討嗎?
2022-10-28 22:54:31
幼蟲

至於你的「想把它當介面使用,但礙於情況只能定義為類別」這題
目前看來只能說是因為在interface的用法上不太正確所衍生出來的問題
不過這也剛好是個很好的例子
寫程式有時會碰到一些難解的問題
適時思考"自己是不是哪裡搞錯與問題源頭是否真的在這"也是有助於程式功力的精進
當然知識與經驗不足的狀況下會難以察覺問題在何處
所以平時的學習跟進修是少不了的
這也是初階工程師與中高階工程師的分水嶺所在
2022-10-28 22:45:08
幼蟲
不會介意喔
2022-10-29 20:15:18
追蹤 創作集

作者相關創作

更多創作