一摆昧、前言
首先绅你,特大喜訊忌锯,奔走相告, Godot 愛好者們又有新的窩了——我們國人自建的 Godot 論壇: Godot中文社區(qū)已經(jīng)正式開放张咳,這里有一手的開發(fā)資源晶伦,最新的科技動向,開發(fā)上有啥問題可以隨時(shí)發(fā)帖族沃,歡迎大家隨時(shí)到論壇來討論脆淹、交流和學(xué)習(xí)游戲開發(fā)的最新技術(shù)盖溺。 :grin:
那么,回過頭來昆禽,今天要探討的話題是 Godot 中極容易被新手忽視的 Resource 資源類。開發(fā)過 Unity 游戲的同學(xué)們知道一個(gè)叫 ScriptableObject 的很有用的類捡硅,它可以用于數(shù)據(jù)的包裝壮韭,在不少場合中應(yīng)該是非常有用的喷屋,那么在 Godot 中有沒有這個(gè)類似的特性呢屯曹?嗯是牢,也有陕截,這就是我們今天要談到的 Resource 資源類型农曲。
官網(wǎng)也有對 Resources 的相關(guān)介紹乳规,我們知道場景是不能拖拽的,也是固定不變的笙以,如果要用場景來保存一些普通數(shù)據(jù)猖腕,肯定不太合理倘感,這時(shí)候我們可以使用 Resource 資源類老玛。相比 Node 其優(yōu)點(diǎn)也很明顯蜡豹,使用非常靈活,同樣可以編寫腳本豹休,可以定義屬性和方法威根,創(chuàng)建資源文件方便洛搀,直接拖拽應(yīng)用即可留美。 "OK, FINE!" 這些我都會談到伸刃,更重要的是捧颅,我今天會利用 Resource 提出一個(gè)全新的碉哑、靈活的、“強(qiáng)力”解耦的 EventBus 全局事件模式妆毕。感興趣嗎笛粘?那我們繼續(xù)闰蛔。
主要內(nèi)容: Resource 的相關(guān)用法簡介
閱讀時(shí)間: 8 分鐘
永久鏈接: http://liuqingwen.me/2020/08/17/godot-game-devLog-3-talk-about-resource/
系列主頁: http://liuqingwen.me/introduction-of-godot-series/
二、正文
Resource 并不神秘蚤吹,但是很容易被忽視。其實(shí)我們平時(shí)創(chuàng)建的場景繁涂、節(jié)點(diǎn)中就包含了各種不同類型的資源文件扔罪,官網(wǎng)中的一張圖展示了某些節(jié)點(diǎn) Node 和資源 Resource 的關(guān)系:
相信上圖中的名稱都不陌生矿酵,游戲場景開發(fā)過程中可能會使用上多種資源類型全肮,常見的就有:圖片資源辜腺、碰撞圖形评疗、各種材質(zhì)百匆、 UI 主題瞧毙、音頻流宙彪、漸變、曲線等等悲没,甚至我們常用的 AnimationPlayer 節(jié)點(diǎn)中創(chuàng)建的動畫示姿,以及 GDScript 腳本栈戳、著色器代碼也都是資源子檀。
資源的創(chuàng)建和使用也非常簡單,不過亩进,目前在 Godot 3 版本中也存在一些局限性归薛,接下來我們詳細(xì)聊聊主籍。
Resource 的創(chuàng)建與使用
創(chuàng)建 Resource 資源的方式就有多種崇猫,平常都是在 Node 節(jié)點(diǎn)的屬性面板中直接創(chuàng)建需忿,比如 New 一個(gè)玩家的碰撞體圖形的形狀屋厘,或是動畫播放器中的各種動畫汗洒,粒子系統(tǒng)新建的材質(zhì)等等,這些資源有一個(gè)特點(diǎn):我們開箱即用瞻凤,很少保存阀参。
資源文件也可以單獨(dú)創(chuàng)建蛛壳,假設(shè)我們需要?jiǎng)?chuàng)建一個(gè)需要在很多地方使用的資源衙荐,比如通用的主題資源忧吟、字體資源斩披、瓦片地圖 TileSet 資源等等,那么我們可以單獨(dú)創(chuàng)建相應(yīng)類型的資源文件,保存起來乡话,在不同場景中輕松實(shí)現(xiàn)重復(fù)利用绑青。在屬性面板或者節(jié)點(diǎn)屬性中都可以新建資源文件:
新建資源文件后記得保存闸婴,保存的文件后綴名一般是 .tres
也有 .res
文件類型的邪乍,區(qū)別在于以文本格式保存還是二進(jìn)制文件格式保存:
保存好的資源文件我們可以隨時(shí)修改其相關(guān)屬性值庇楞,雙擊資源文件即可吕晌,另外,也可以創(chuàng)建多個(gè)副本烙心,比如字體資源復(fù)制( duplicate )一份淫茵,然后修改字體大小屬性痘昌,使用在不同的地方辆苔。
資源的使用方式就簡單了扼劈,可以直接拖拽到對應(yīng)屬性中,也可以在屬性下拉列表中點(diǎn)擊 Load 加載骑冗。系統(tǒng)自帶的資源比較齊全,當(dāng)然我們也可以自定義資源類型巧涧。資源從本質(zhì)上來說仍然是一種腳本文件遥倦,創(chuàng)建自定義資源首先需要?jiǎng)?chuàng)建一個(gè)繼承自 Resource
類的腳本:
# 繼承自 Resource 說明這是一個(gè)資源腳本
extends Resource
class_name CustomResource, 'res://CustomResource/custom_icon.svg'
# 資源也可以定義普通的屬性
export var variable1 := ''
export var variable2 := 0
# ...
# 資源也可以定義一些方法
func printInfo() -> void:
# ...
在上面新建的代碼中我們聲明了資源的類名( CustomResource
)以及資源的圖標(biāo)( res://CustomResource/custom_icon.svg
)缩筛。創(chuàng)建好之后堡称,可以在新建資源列表中發(fā)現(xiàn)相對應(yīng)的自定義資源類型却紧,這一系列過程可以參考下圖:
是不是非常簡單啄寡?趕緊動手創(chuàng)建一個(gè)壓壓驚挺物。 :joy:
Resource 相關(guān)問題與局限
資源的創(chuàng)建和使用確實(shí)簡單识藤,不過 Godot 3 中對于自定義資源還是有點(diǎn)小坑,這里提出來稽穆,希望對新手朋友們有用舌镶。
1. 不能使用自定義 Resource 為變量類型
我們創(chuàng)建自定義資源時(shí)餐胀,可以給資源定義個(gè)類名 class_name CustomResource
否灾,但是在代碼中確不能定義該類型的資源變量:
var resource1 : Resource # 沒問題
var resource2 : CustomResource # 不支持鸣奔!
上面的代碼運(yùn)行會報(bào)錯(cuò):
built-in:4 - Parse Error: Invalid export type. Only built-in and native resource types can be exported.
避免這個(gè)問題的方法就是使用父類型 Resource
作為變量的類型,不過這樣會導(dǎo)致在 export
屬性中可以賦予任意類型的資源文件断楷,非常不方便崭别、不人道紊遵。當(dāng)然你可以在代碼中進(jìn)行判斷:
if resource && resource is CustomResource:
# 代碼...
不過暗膜,好消息是這個(gè)問題會在 Godot 4.0 中得到解決学搜。
2. 使用 Resouce 要注意資源是引用類型
如果一個(gè)資源文件被多個(gè)節(jié)點(diǎn)使用瑞佩,這個(gè)時(shí)候你只要改變了某個(gè)節(jié)點(diǎn)下該資源的任意一個(gè)屬性炬丸,結(jié)果都會導(dǎo)致其他節(jié)點(diǎn)下該資源跟隨發(fā)生變化蜒蕾!
舉個(gè)例子咪啡,游戲資源中有一個(gè) font_resource.res
字體資源文件撤摸,當(dāng)你改變了資源屬性中字體的大小后,其他所有使用了該資源的 UI 界面字體都會發(fā)生改變钥飞。這也是為什么新手們經(jīng)常會遇到這種情況:創(chuàng)建一個(gè)節(jié)點(diǎn)代承,添加碰撞體论悴,新建一個(gè)碰撞體圖形,設(shè)置好之后復(fù)制該節(jié)點(diǎn)并重命名幔亥,修改新碰撞節(jié)點(diǎn)的圖片和碰撞體圖形帕棉,莫名發(fā)現(xiàn)之前節(jié)點(diǎn)的碰撞體圖形也發(fā)生了改變饼记,其實(shí)就是這個(gè)原因具则。 :grin:
所以,在 Godot 中一個(gè)小小的變量值改變都需要重新創(chuàng)建一個(gè)資源低斋,這也不算什么大問題膊畴,我們可以右鍵資源文件 Duplicate 復(fù)制一個(gè)唇跨,或者使用 Make Unique 方式使指定資源唯一化轻绞。
3. 使用 Resouce 要注意避免循環(huán)引用
如果你的項(xiàng)目中創(chuàng)建了不少自定義資源文件政勃,自定義資源代碼中又引用了其他類型的資源兼砖,那么有可能會出現(xiàn)這種錯(cuò)誤;
"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."
其實(shí)循環(huán)引用問題( Circular reference )在普通 GD 代碼中也會出現(xiàn)懒叛,而出現(xiàn)在自定義資源中則會變得難以發(fā)覺薛窥。解決這個(gè)問題的方法就是不要在編輯器中直接給資源賦值,轉(zhuǎn)而在運(yùn)行時(shí)判斷然后動態(tài)加載 Resource 佩番,示例如下:
export var resource : Resource # 自定義資源
export var resourceFilePath : String # 資源路徑
func method() -> void:
if resource == null:
# 運(yùn)行時(shí)加載資源文件
resource = load(resourceFilePath)
# 代碼...
這種情況應(yīng)該比較少見趟畏,暫時(shí)不做深入討論赋秀,后面的文章遇到了再詳述律想,當(dāng)然技即,我們翹首以待的 4.0 版本會解決這個(gè)問題姥份。
4. 其他的小問題
如果修改資源腳本中的圖標(biāo)或者類名后澈歉,其他引用了這個(gè) Resource 的代碼就會報(bào)錯(cuò)屿衅,類似 Resource 類已經(jīng)損壞涤久,加載不完整之類。重新啟動項(xiàng)目就可以了考抄。
有時(shí)候還會遇到這種小 BUG :
core/script_language.cpp:244 - Condition "!global_classes.has(p_class)" is true. Returned: String()
有點(diǎn)莫名川梅,也不容易重現(xiàn)贫途,我估計(jì)是修改了 Resource 腳本類名引起的待侵,反正重啟項(xiàng)目就沒事了秧倾。 :joy:
這些小問題說明目前 Godot 的資源類型還不夠完善傀缩, Waiting for Godot 4.0 藥到病除扑毡,哈哈瞄摊!
創(chuàng)建 Resource 相當(dāng)于 DataContainer
創(chuàng)建自定義 Resource 的一個(gè)經(jīng)典用途就是當(dāng)做數(shù)據(jù)容器换帜。創(chuàng)建一個(gè)個(gè)資源文件就相當(dāng)于創(chuàng)建了一個(gè)個(gè)數(shù)據(jù)容器惯驼,這些數(shù)據(jù)容器一般沒有其他功能递瑰,只是獨(dú)立保存一些應(yīng)用數(shù)據(jù)抖部,不論是修改還是使用都非常方便且靈活。
舉個(gè)具有實(shí)際應(yīng)用場景的例子乡恕,在一個(gè) Player 或者 AI 腳本中傲宜,如果存在著大量數(shù)據(jù)屬性夫啊,而這些數(shù)據(jù)屬性一般不會發(fā)生改變,或者只是一些配置參數(shù)谆趾,那么我們完全可以將其抽離出來作為一個(gè)單獨(dú)的數(shù)據(jù)類——這也是《重構(gòu)-改善既有代碼的設(shè)計(jì)》一書中提倡的重構(gòu)方式之一沪蓬。
# 玩家類
export var name := 'player'
export var moveSpeed := 200
export var rotateSpeed := 5
# 其他一些屬性...
在 Godot 中這個(gè)所謂的單獨(dú)數(shù)據(jù)類可以使用內(nèi)部類進(jìn)行包裝:
# 玩家類
# 內(nèi)部類
class Data:
var name := 'player'
var moveSpeed := 200
var rotateSpeed := 5
func _init():
pass
內(nèi)部類雖然可以封裝數(shù)據(jù)跷叉,但是在腳本范圍之外使用則非常蹩腳云挟,也不方便在編輯器中進(jìn)行編輯,這時(shí)候我們可以使用自定義資源類解決這個(gè)痛點(diǎn):
extends Resource
export var name := 'player'
export var moveSpeed := 200
export var rotateSpeed := 5
然后創(chuàng)建單個(gè)或者多個(gè)資源文件帖世,在編輯器的屬性面板中修改對應(yīng)的屬性值日矫,在其他代碼中使用起來非常方便:
export var dataResource : Resource = null
fun _ready() -> void:
if dataResource != null:
print(dataResource.name, dataResource.moveSpeed, dataResource.rotateSpeed)
作為數(shù)據(jù)容器和 ScriptableObject 有點(diǎn)類似哪轿,接下來我們看 Resource 的另一個(gè)非常有用的場景窃诉。
用 Resource 創(chuàng)建全局事件的 EventBus
可以說這是本文的重點(diǎn)飘痛,目前我還沒有看到有任何人在項(xiàng)目中使用過這種方式宣脉,且聽我慢慢道來~~~
首先,關(guān)于 Godot 中的 signal
信號以及觀察者模式相信大家都已經(jīng)駕輕就熟了堪遂,一般在游戲開發(fā)中我們都會準(zhǔn)守 signal up, call down
的準(zhǔn)則币旧,即往上層發(fā)送信號猿妈,往下層直接調(diào)用彭则。當(dāng)游戲變得越來越復(fù)雜的時(shí)候,信號可能已經(jīng)充滿了整個(gè)項(xiàng)目输瓜,比如某個(gè)多人游戲中信息面板需要接收并顯示多種不同類型的信號:玩家按下回車鍵發(fā)送的文字信息搔啊、玩家某個(gè)戰(zhàn)場獲得勝利發(fā)出的信號、某個(gè)玩家退出游戲發(fā)出的信號负芋、官方服務(wù)器推送的信息等等旧蛾,因?yàn)檫@些信息發(fā)生在不同的場景蚜点,處理起來并不簡單拌阴,我能想到的解決方式有這么幾種:
- 使用
get_node('../root/node_path')
方式陪拘,不推薦并表示強(qiáng)烈譴責(zé)纤壁,這會造成強(qiáng)耦合欠痴,擴(kuò)展喇辽、維護(hù)和重構(gòu)極其困難 - 使用 Global AutoLoad 菩咨,也就是 Singleton 單例模式抽米,有效解決耦合糙置,但是維護(hù)相當(dāng)困難云茸,牽一發(fā)而動全身,調(diào)試?yán)щy
- 使用 Resource 創(chuàng)建相應(yīng)的事件資源谤饭,強(qiáng)力解耦查辩,使用起來非常方便胖笛,調(diào)試也非常簡單,易擴(kuò)展和維護(hù)
關(guān)于第二種方式是大家推薦的模式宜岛,我在之前的示例中就使用過:(Godot游戲開發(fā)實(shí)踐之一:使用High Level Multiplayer API制作多人游戲(上))长踊, GDQuest 的文檔中也介紹了這種模式: https://www.gdquest.com/docs/guidelines/best-practices/godot-gdscript/event-bus/ ,示例代碼如下:
# 這是一個(gè) AutoLoad 單例
extends Node
# 可以定義多個(gè)通用信號
signal new_message(content)
# 其他代碼...
其他場景中使用也非常簡單:
# 場景 1 中發(fā)送信號:
GameConfig.emit_signal('new_message', '......')
# 場景 2 中接收處理信號:
GameConfig.connect('new_message', self, '_on_NewMessage_arrive')
但是這種方式有一個(gè)很大的缺陷:全局引用導(dǎo)致重構(gòu)困難萍倡。因?yàn)閱卫喈?dāng)于全局模式身弊,任何地方都可以引用列敲,重構(gòu)時(shí)一旦改動單例中某個(gè)方法或者屬性都有可能引起其他地方因?yàn)橐檬Ф鴮?dǎo)致運(yùn)行奔潰,尋找這些引用并不容易淮逊,這也為什么 GDQuest 推薦的 EventBus 模式是單獨(dú)創(chuàng)建的只有信號沒有其他代碼的腳本文件秧耗。
廢話一堆,一起來看看利用 Resource 創(chuàng)建的事件模式吧珠闰!首先創(chuàng)建一個(gè)事件資源:
# 自定義資源
extends Resource
class_name EventResource, 'res://EventResource/event_icon.svg'
# 自定義信號
signal custom_event(type, message)
# 可以定義一些屬性
export var type := 'defaultEvent'
# 自定義方法用于發(fā)送信號的包裝杭朱,也可以直接發(fā)送信號
func emitSignal(object) -> void:
self.emit_signal('custom_event', type, object)
接下來空民,我們可以創(chuàng)建一些事件資源文件衔瓮,比如 message_event.tres
trigger_event.tres
衔彻,不同的文件可以更改、配置不同的參數(shù),然后在其他腳本中使用:
export var messageEvent : Resource = null
export var triggerEvent : Resource = null
# 可以使用事件資源偵聽事件
func someMethod1() -> void:
if triggerEvent && triggerEvent is EventResource:
triggerEvent.connect('custom_event', self, '_onTriggerEventHandler')
# 也可以使用事件資源發(fā)送事件
func someMethod2() -> void:
if messageEvent && messageEvent is EventResource:
messageEvent.emitSignal(info)
因?yàn)檫@些事件都是資源類型,在節(jié)點(diǎn)屬性中可以直接拖拽使用,而且可有可無纬纪,均不影響整個(gè)項(xiàng)目的運(yùn)行靶庙,在本示例中玩家的屬性配置如下圖:
可以看到 Player1 只接收 message_event
事件, Player3 只派發(fā) trigger_event
事件,而 Player2 則無任何配置殴玛,可謂一目了然坦刀。
總結(jié)一下使用 Resource 創(chuàng)建事件的一些優(yōu)點(diǎn):
- 強(qiáng)力解耦!不依賴其他文件或者腳本、節(jié)點(diǎn),很容易進(jìn)行重構(gòu)
- 便于調(diào)試,代碼中只要注意
null
引用即可,刪除或者添加相關(guān)事件都非常友好 - 便于測試,修改事件相關(guān)屬性值非常方便,一改全改
- 可以考慮在大型項(xiàng)目中應(yīng)用
并沒有十全十美的萬能解決方案搅方,當(dāng)然也是有缺點(diǎn)的绣溜,比如一堆的只是改變了某一個(gè)變量值的 .res
文件等。重要的是,目前還沒有實(shí)際項(xiàng)目支持這個(gè)事件模式,有待大家的開發(fā)和探索啊。 :smile:
三、總結(jié)
好了,這篇就聊了一個(gè)簡單的 Resource 話題,希望能給新手朋友們帶來一點(diǎn)點(diǎn)幫助,給高手朋友們開拓一點(diǎn)點(diǎn)亮光,那這篇文章也就值了。
記住我們 Godot 愛好者的新家: Godot中文社區(qū) 镊讼,歡迎臣婷常回家看看螺垢!
本篇的 Demo 以及相關(guān)代碼已經(jīng)上傳到 Github 痊土,地址: https://github.com/spkingr/Godot-Demos , 后續(xù)繼續(xù)更新,原創(chuàng)不易鲸睛,希望大家喜歡晴股! :smile:
我的博客地址: http://liuqingwen.me ,我的博客即將同步至騰訊云+社區(qū)呢诬,邀請大家一同入駐: https://cloud.tencent.com/developer/support-plan?invite_code=3sg12o13bvwgc ,歡迎關(guān)注我的微信公眾號(第一時(shí)間更新+游戲開發(fā)資源+相關(guān)資訊):