lua游戏开发实践指南光盘_Godot游戏开发实践之三:容易被忽视的Resource
一、前言
首先,特大喜訊,奔走相告, Godot 愛好者們又有新的窩了——我們國人自建的 Godot 論壇:Godot中文社區已經正式開放,這里有一手的開發資源,最新的科技動向,開發上有啥問題可以隨時發帖,歡迎大家隨時到論壇來討論、交流和學習游戲開發的最新技術。
那么,回過頭來,今天要探討的話題是 Godot 中極容易被新手忽視的 Resource 資源類。開發過 Unity 游戲的同學們知道一個叫 ScriptableObject 的很有用的類,它可以用于數據的包裝,在不少場合中應該是非常有用的,那么在 Godot 中有沒有這個類似的特性呢?嗯,也有,這就是我們今天要談到的 Resource 資源類型。
官網也有對 Resources 的相關介紹,我們知道場景是不能拖拽的,也是固定不變的,如果要用場景來保存一些普通數據,肯定不太合理,這時候我們可以使用 Resource 資源類。相比 Node 其優點也很明顯,使用非常靈活,同樣可以編寫腳本,可以定義屬性和方法,創建資源文件方便,直接拖拽應用即可。"OK, FINE!" 這些我都會談到,更重要的是,我今天會利用 Resource 提出一個全新的、靈活的、“強力”解耦的 EventBus 全局事件模式。感興趣嗎?那我們繼續。
主要內容:Resource 的相關用法簡介閱讀時間:8 分鐘永久鏈接:http://liuqingwen.me/2020/08/17/godot-game-devLog-3-talk-about-resource/系列主頁:http://liuqingwen.me/introduction-of-godot-series/
二、正文
Resource 并不神秘,但是很容易被忽視。其實我們平時創建的場景、節點中就包含了各種不同類型的資源文件,官網中的一張圖展示了某些節點 Node 和資源 Resource 的關系:
相信上圖中的名稱都不陌生,游戲場景開發過程中可能會使用上多種資源類型,常見的就有:圖片資源、碰撞圖形、各種材質、 UI 主題、音頻流、漸變、曲線等等,甚至我們常用的 AnimationPlayer 節點中創建的動畫,以及 GDScript 腳本、著色器代碼也都是資源。
資源的創建和使用也非常簡單,不過,目前在 Godot 3 版本中也存在一些局限性,接下來我們詳細聊聊。
Resource 的創建與使用
創建 Resource 資源的方式就有多種,平常都是在 Node 節點的屬性面板中直接創建,比如 New 一個玩家的碰撞體圖形的形狀,或是動畫播放器中的各種動畫,粒子系統新建的材質等等,這些資源有一個特點:我們開箱即用,很少保存。
資源文件也可以單獨創建,假設我們需要創建一個需要在很多地方使用的資源,比如通用的主題資源、字體資源、瓦片地圖 TileSet 資源等等,那么我們可以單獨創建相應類型的資源文件,保存起來,在不同場景中輕松實現重復利用。在屬性面板或者節點屬性中都可以新建資源文件:
新建資源文件后記得保存,保存的文件后綴名一般是?.tres?也有?.res?文件類型的,區別在于以文本格式保存還是二進制文件格式保存:
保存好的資源文件我們可以隨時修改其相關屬性值,雙擊資源文件即可,另外,也可以創建多個副本,比如字體資源復制( duplicate )一份,然后修改字體大小屬性,使用在不同的地方。
資源的使用方式就簡單了,可以直接拖拽到對應屬性中,也可以在屬性下拉列表中點擊 Load 加載。系統自帶的資源比較齊全,當然我們也可以自定義資源類型。資源從本質上來說仍然是一種腳本文件,創建自定義資源首先需要創建一個繼承自?Resource?類的腳本:
# 繼承自 Resource 說明這是一個資源腳本extends Resourceclass_name CustomResource, 'res://CustomResource/custom_icon.svg'# 資源也可以定義普通的屬性export var variable1 := ''export var variable2 := 0# ...# 資源也可以定義一些方法func printInfo() -> void:# ...在上面新建的代碼中我們聲明了資源的類名(?CustomResource?)以及資源的圖標(res://CustomResource/custom_icon.svg?)。創建好之后,可以在新建資源列表中發現相對應的自定義資源類型,這一系列過程可以參考下圖:
是不是非常簡單?趕緊動手創建一個壓壓驚。
Resource 相關問題與局限
資源的創建和使用確實簡單,不過 Godot 3 中對于自定義資源還是有點小坑,這里提出來,希望對新手朋友們有用。
1. 不能使用自定義 Resource 為變量類型
我們創建自定義資源時,可以給資源定義個類名?class_name CustomResource?,但是在代碼中確不能定義該類型的資源變量:
var resource1 : Resource # 沒問題var resource2 : CustomResource # 不支持!上面的代碼運行會報錯:
built-in:4 - Parse Error: Invalid export type. Only built-in and native resource types can be exported.
避免這個問題的方法就是使用父類型?Resource?作為變量的類型,不過這樣會導致在export?屬性中可以賦予任意類型的資源文件,非常不方便、不人道。當然你可以在代碼中進行判斷:
if resource && resource is CustomResource:# 代碼...不過,好消息是這個問題會在 Godot 4.0 中得到解決。
2. 使用 Resouce 要注意資源是引用類型
如果一個資源文件被多個節點使用,這個時候你只要改變了某個節點下該資源的任意一個屬性,結果都會導致其他節點下該資源跟隨發生變化!
舉個例子,游戲資源中有一個?font_resource.res?字體資源文件,當你改變了資源屬性中字體的大小后,其他所有使用了該資源的 UI 界面字體都會發生改變。這也是為什么新手們經常會遇到這種情況:*創建一個節點,添加碰撞體,新建一個碰撞體圖形,設置好之后復制該節點并重命名,修改新碰撞節點的圖片和碰撞體圖形,莫名發現之前節點的碰撞體圖形也發生了改變*,其實就是這個原因。
所以,在 Godot 中一個小小的變量值改變都需要重新創建一個資源,這也不算什么大問題,我們可以右鍵資源文件?Duplicate?復制一個,或者使用?Make Unique?方式使指定資源唯一化。
3. 使用 Resouce 要注意避免循環引用
如果你的項目中創建了不少自定義資源文件,自定義資源代碼中又引用了其他類型的資源,那么有可能會出現這種錯誤;
"scene/resources/resource_format_text.cpp:1387 - Circular reference to resource being saved found: 'res://src/.../???.tres' will be null next time it's loaded."
其實循環引用問題(?Circular reference?)在普通 GD 代碼中也會出現,而出現在自定義資源中則會變得難以發覺。解決這個問題的方法就是不要在編輯器中直接給資源賦值,轉而在運行時判斷然后動態加載 Resource ,示例如下:
export var resource : Resource # 自定義資源export var resourceFilePath : String # 資源路徑func method() -> void:if resource == null:# 運行時加載資源文件resource = load(resourceFilePath)# 代碼...這種情況應該比較少見,暫時不做深入討論,后面的文章遇到了再詳述,當然,我們翹首以待的 4.0 版本會解決這個問題。
4. 其他的小問題
如果修改資源腳本中的圖標或者類名后,其他引用了這個 Resource 的代碼就會報錯,類似?Resource 類已經損壞,加載不完整之類。重新啟動項目就可以了。
有時候還會遇到這種小 BUG :
core/script_language.cpp:244 - Condition "!global_classes.has(p_class)" is true. Returned: String()
有點莫名,也不容易重現,我估計是修改了 Resource 腳本類名引起的,反正重啟項目就沒事了。
這些小問題說明目前 Godot 的資源類型還不夠完善, Waiting for Godot 4.0 藥到病除,哈哈!
創建 Resource 相當于 DataContainer
創建自定義 Resource 的一個經典用途就是當做數據容器。創建一個個資源文件就相當于創建了一個個數據容器,這些數據容器一般沒有其他功能,只是獨立保存一些應用數據,不論是修改還是使用都非常方便且靈活。
舉個具有實際應用場景的例子,在一個 Player 或者 AI 腳本中,如果存在著大量數據屬性,而這些數據屬性一般不會發生改變,或者只是一些配置參數,那么我們完全可以將其抽離出來作為一個單獨的數據類——這也是《重構-改善既有代碼的設計》一書中提倡的重構方式之一。
# 玩家類export var name := 'player'export var moveSpeed := 200export var rotateSpeed := 5# 其他一些屬性...在 Godot 中這個所謂的單獨數據類可以使用內部類進行包裝:
# 玩家類# 內部類class Data:var name := 'player'var moveSpeed := 200var rotateSpeed := 5func _init():pass內部類雖然可以封裝數據,但是在腳本范圍之外使用則非常蹩腳,也不方便在編輯器中進行編輯,這時候我們可以使用自定義資源類解決這個痛點:
extends Resourceexport var name := 'player'export var moveSpeed := 200export var rotateSpeed := 5然后創建單個或者多個資源文件,在編輯器的屬性面板中修改對應的屬性值,在其他代碼中使用起來非常方便:
export var dataResource : Resource = nullfun _ready() -> void:if dataResource != null:print(dataResource.name, dataResource.moveSpeed, dataResource.rotateSpeed)作為數據容器和 ScriptableObject 有點類似,接下來我們看 Resource 的另一個非常有用的場景。
用 Resource 創建全局事件的 EventBus
可以說這是本文的重點,目前我還沒有看到有任何人在項目中使用過這種方式,且聽我慢慢道來~~~
首先,關于 Godot 中的?signal?信號以及觀察者模式相信大家都已經駕輕就熟了,一般在游戲開發中我們都會準守?signal up, call down?的準則,即往上層發送信號,往下層直接調用。當游戲變得越來越復雜的時候,信號可能已經充滿了整個項目,比如某個多人游戲中信息面板需要接收并顯示多種不同類型的信號:玩家按下回車鍵發送的文字信息、玩家某個戰場獲得勝利發出的信號、某個玩家退出游戲發出的信號、官方服務器推送的信息等等,因為這些信息發生在不同的場景,處理起來并不簡單,我能想到的解決方式有這么幾種:
使用?get_node('../root/node_path')?方式,不推薦并表示強烈譴責,這會造成強耦合,擴展、維護和重構極其困難
使用?Global AutoLoad?,也就是 Singleton 單例模式,有效解決耦合,但是維護相當困難,牽一發而動全身,調試困難
使用 Resource 創建相應的事件資源,強力解耦,使用起來非常方便,調試也非常簡單,易擴展和維護
關于第二種方式是大家推薦的模式,我在之前的示例中就使用過:(Godot游戲開發實踐之一:使用High Level Multiplayer API制作多人游戲(上)), GDQuest 的文檔中也介紹了這種模式:https://www.gdquest.com/docs/guidelines/best-practices/godot-gdscript/event-bus/?,示例代碼如下:
# 這是一個 AutoLoad 單例extends Node# 可以定義多個通用信號signal new_message(content)# 其他代碼...其他場景中使用也非常簡單:
# 場景 1 中發送信號:GameConfig.emit_signal('new_message', '......')# 場景 2 中接收處理信號:GameConfig.connect('new_message', self, '_on_NewMessage_arrive')但是這種方式有一個很大的缺陷:全局引用導致重構困難。因為單例相當于全局模式,任何地方都可以引用,重構時一旦改動單例中某個方法或者屬性都有可能引起其他地方因為引用失效而導致運行奔潰,尋找這些引用并不容易,這也為什么 GDQuest 推薦的 EventBus 模式是單獨創建的只有信號沒有其他代碼的腳本文件。
廢話一堆,一起來看看利用 Resource 創建的事件模式吧!首先創建一個事件資源:
# 自定義資源extends Resourceclass_name EventResource, 'res://EventResource/event_icon.svg'# 自定義信號signal custom_event(type, message)# 可以定義一些屬性export var type := 'defaultEvent'# 自定義方法用于發送信號的包裝,也可以直接發送信號func emitSignal(object) -> void:self.emit_signal('custom_event', type, object)接下來,我們可以創建一些事件資源文件,比如?message_event.trestrigger_event.tres?,不同的文件可以更改、配置不同的參數,然后在其他腳本中使用:
export var messageEvent : Resource = nullexport var triggerEvent : Resource = null# 可以使用事件資源偵聽事件func someMethod1() -> void:if triggerEvent && triggerEvent is EventResource:triggerEvent.connect('custom_event', self, '_onTriggerEventHandler')# 也可以使用事件資源發送事件func someMethod2() -> void:if messageEvent && messageEvent is EventResource:messageEvent.emitSignal(info)因為這些事件都是資源類型,在節點屬性中可以直接拖拽使用,而且可有可無,均不影響整個項目的運行,在本示例中玩家的屬性配置如下圖:
可以看到?Player1?只接收?message_event?事件,?Player3?只派發trigger_event?事件,而?Player2?則無任何配置,可謂一目了然。
總結一下使用 Resource 創建事件的一些優點:
強力解耦!不依賴其他文件或者腳本、節點,很容易進行重構
便于調試,代碼中只要注意?null?引用即可,刪除或者添加相關事件都非常友好
便于測試,修改事件相關屬性值非常方便,一改全改
可以考慮在大型項目中應用
并沒有十全十美的萬能解決方案,當然也是有缺點的,比如一堆的只是改變了某一個變量值的?.res?文件等。重要的是,目前還沒有實際項目支持這個事件模式,有待大家的開發和探索啊。
三、總結
好了,這篇就聊了一個簡單的 Resource 話題,希望能給新手朋友們帶來一點點幫助,給高手朋友們開拓一點點亮光,那這篇文章也就值了。
記住我們 Godot 愛好者的新家:Godot中文社區( http://godoter.top/ )?,歡迎常回家看看!
本篇的 Demo 以及相關代碼已經上傳到 Github ,地址:https://github.com/spkingr/Godot-Demos , 后續繼續更新,原創不易,希望大家喜歡!
我的博客地址:http://liuqingwen.me ,我的博客即將同步至騰訊云+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3sg12o13bvwgc ,歡迎關注我的微信公眾號(第一時間更新+游戲開發資源+相關資訊):
新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!總結
以上是生活随笔為你收集整理的lua游戏开发实践指南光盘_Godot游戏开发实践之三:容易被忽视的Resource的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 半年辞退30多个程序员,大厂“开猿节流”
- 下一篇: 2021年度最佳开源软件榜单出炉!