《ATD》 游戲邏輯
先說明一下,全局游戲邏輯的全局並不是指變量的全局暴露,而是說負責游戲世界的整體邏輯。
全局游戲邏輯設計的話相對輕松一點:
- 首先為了更好管理個體游戲對象,引入了 對象工廠 來控制個體有對象的生命周期。
- 金錢管理器 負責玩家的金錢數據管理,例如擊殺獎勵,關卡結算獎勵。
- 塔管理器 負責用規則限制塔的邏輯,例如建造一個塔的位置限制,建造塔的金錢消耗。
- 關卡管理器 負責生成每波怪物。
為了輔助這些邏輯,還額外引入了消息系統組件,路徑管理器,怪物生成器三個腳本。
構造如下:
《ATD》游戲對象目錄設置:
引入消息系統是為了讓游戲邏輯可以監聽個體對象之間的交互消息,從而做出一些符合游戲邏輯的行為。
例如,監聽到基地個體對象死亡的消息,應判斷游戲失敗。
游戲邏輯比較多腳本都需要讀入配置文件數據的功能,方便動態更新游戲。
此外,腳本應在Inspector面板應提供一些可調的邏輯參數,方便調試全局邏輯(例如金錢數調99999999)。
《ATD》 消息系統組件實現
觀察者模式
觀察者模式 是一個常見的設計模式,其定義對象間一種一對多的依賴關系,使得每當一個對象改變狀態,則所有依賴它的對象都會得到通知並自動更新。有關該設計模式的更具體內容,本文就不多講述。
在《ATD》里, 消息系統組件 接受任何 消息中心 轉發的消息。
各個依賴 消息系統組件 的模塊需要在 消息系統組件 訂閱自己關心的消息類型(注冊委托)。
任何地方都可以向 消息中心發出消息,消息中心 接着再轉發該消息給各個 消息系統組件 ,接着 消息系統組件 根據消息類型,執行關心該類型的已注冊委托對象。
實際上這個設計模式很重要,常常用於UI與邏輯的交互。
而在《ATD》里,被改造成一個新的消息機制,用於模型之間的交互和模型與全局邏輯之間的交互。
《ATD》的核心思想是:一切基於消息驅動。攻擊是給敵人對象發送一個攻擊類型的消息,上Buff是給自己這個個體對象發送一個Buff類型的消息...幾乎每一個行為都是通過消息來驅動的。這在以后做高度定制的成就系統更是有潛在的幫助,畢竟成就系統也訂閱其中幾個感興趣的類型消息即可獲取想要的數據,而不會造成更多的耦合。
對象死亡解引用
在實際的實現中,發現個體對象死亡而引發的引用丟失問題非常多。
一個解決方法是:依賴個體對象引用的代碼都需要使用一個 消息系統組件 從而對死亡類型的消息進行監聽,當聽到自己依賴的對象死亡時,則立即解除引用。這方法工作的很好。
《ATD》 對象工廠實現
工廠模式
工廠模式 是一個常見的設計模式:工廠往往是一個全局單例,用來管理對象的生命周期。
不過在《ATD》項目里, 對象工廠 的職責是:管理所有個體對象。
但需要生成個體對象時,必須使用 對象工廠 提供的生成對象接口。
至於銷毀個體對象,一個要注意的問題是,游戲對象銷毀和個體死亡是兩種不同的概念:
一個個體對象受到傷害,血量低於0時,即可被判定為個體死亡,然而由於游戲效果需要保留屍體(例如用作死亡動畫),所以此時游戲對象不應被銷毀。除非直到該游戲對象的控制器組件認為該銷毀游戲對象。
也就是說當個體組件死亡時,這個個體游戲對象不應存在於游戲邏輯中,而是相當於變成了一個游戲場景的擺設物。
所以 對象工廠 應該至少有兩個存儲容器:
一個存儲表示所有個體對象,另一個存儲表示個體存活的個體對象。
- 個體對象被判定個體死亡時,對象工廠 應該注銷該個體的存在。
- 個體對象被判定為游戲對象銷毀時, 對象工廠 應該銷毀該游戲對象。
查詢優化
前面說到 對象工廠 至少使用兩個容器的原因,實際上還有另一個原因是游戲邏輯有很多需要查詢游戲個體的操作。
而僅使用存儲對象的容器是不夠優的,因為很可能遍歷到一些個體死亡而對象存在的個體對象,浪費效率。
實際上,《ATD》的 對象工廠 還專門用第三個容器來表示存活怪物對象,這是因為許多塔的行為樹攻擊行為都需要遍歷所有怪物個體對象,而不需要遍歷到其他個體對象。
額外:說到查詢,就不得不提一下 世界查詢器,它是一個全局單例類,職責是提供查詢接口,例如:
- 實現爆炸效果,需要查詢某點方圓半徑10米的所有對象,從而對查詢的每個對象造成爆炸影響。
- 指向性定位目標對象,查詢某點發出一條射線碰到的第一個對象,並定位之。
- 由於某個區域內發生警報,需要查詢該區域內的所有對象來逐個通知。
實際上由於急於實現,《ATD》的對象工廠的實現包含了簡單的世界查詢器的功能。
在以后的擴展,最好這兩者需要分離開,對象工廠只負責對象的生命周期,而世界查詢器作為一個輔助工具,內維護各種數據結構以加速查詢。
lazy delete
當一個個體對象向 對象工廠 請求摧毀該對象本身時, 對象工廠 並不立即Destroy該對象,而是將其SetActive(false),並添加到死亡對象列表。
當 對象工廠 接到一個新的個體對象構造請求時,若死亡列表有對象,從死亡對象列表中選一個個體對象進行屬性的覆寫,然后再將其SetActive(true);若死亡列表為空,才使用生成函數,真正生成一個新的個體對象。
這是個常見的操作,通過屬性的覆寫就能“生成”一個新的對象,可以極大的減少new/Destory對象的開銷(特別是在這個塔防游戲里,個體對象的生成/死亡十分頻繁)。
《ATD》 Buff系統組件實現
基本實現
Buff系統組件 是屬於個體游戲對象的一種組件類,它負責容納Buff對象,並計算這些Buff對象對個體屬性造成的影響。
前篇說到,當Buff對象生成時,應造成個體屬性的一次改變(Buff生效影響);當Buff對象銷毀時,再造成個體屬性的一次改變(Buff失效影響)。
對於一般的整形/浮點屬性數值的影響,直接加減屬性即可。
而對於布爾屬性數值的影響,往往需要額外維護一個計數,當計數為0時視為false,當計數為1或以上則視為true。
計算順序
一開始, Buff系統組件 每幀的計算函數,大概內容順序:
- Update: 處理Buff消息后,根據消息添加Buff,然后計算一次生效影響。特別地,若已有Buff對象與待添加的Buff是同種ID,則對已有Buff對象進行生命期的疊加。
- Update: 減少各個Buff對象的生命期(減去一幀或者一幀的時間)。
- LateUpdate: 判斷各個Buff對象的生命期是否結束,若結束則移除Buff對象,並計算一次失效影響。
這段邏輯看似正常,然而在某種特殊情況可能會造成不好的性能影響:光環類型的技能
這種技能會每幀向方圓范圍一定距離的個體對象發送光環對應的Buff消息,而這種Buff生命期只有一幀。
倘若按照上圖的順序執行,一個一直停留在光環范圍內的個體對象竟然在1s內重復生成和釋放一個Buff對象60~80次(可以思考下為什么)。
於是,為了解決這個問題,換成了下圖這種執行順序,很好的解決了:
《ATD》 UI/HUD/特效/音樂
應為UI/HUD/特效/BGM各自編寫一個 UI管理器/HUD管理器/特效管理器/音樂管理器 :
一是方便管理顯示,二是更好的與游戲邏輯/游戲模型來交互。
然后也要為這些管理器引入 消息系統組件 用於輔助,從而接受一些重要的消息來改變顯示效果。
舉個例子,Buff特效管理器,通過監聽游戲模型的Buff消息,來給對應的游戲模型生成Buff特效對象。
此時,項目整體架構關系如圖:
是不是感覺有點像MVC視圖?(笑
《ATD》 日志調試工具
當項目變得龐大起來時,各種Debug消息在Console噴涌而出。當你在Debug的時候,我保證你根本不會想看到一堆無關的Debug消息。
親身體驗過,運行團隊項目,彈出一大堆別人寫的Debug.log消息,才有了編寫日志工具的想法。
一個日志調試工具是必不可少的:
在你想輸出Debug信息時,你需要指定它的類型。
當你查看調試信息時,你可以在面板里勾選你關注的Debug信息類型。
《ATD》的日志工具:
結語
Unity《ATD》塔防RPG類3D游戲架構設計系列博文就到此結束,就兩篇不多,懶得再寫了。
博主本人一年半前才開始接觸Unity,學術淺薄,有關知識仍不多,文章很多不成熟的解決方案或者錯誤,請多多指出。
不過博主的Unity之路也可能到此畫上一個句號,因為博主對C++語言情有獨鍾,再加之偏向開發PC端主機端大型游戲的發展規划,因此以后就要踏上UE4的征途了(實際上正在學UE4)。