Godot游戲開(kāi)發(fā)實(shí)踐之三:容易被忽視的Resource

一、前言

首先,特大喜訊,奔走相告, Godot 愛(ài)好者們又有新的窩了——我們國(guó)人自建的 Godot 論壇: Godot中文社區(qū)已經(jīng)正式開(kāi)放,這里有一手的開(kāi)發(fā)資源,最新的科技動(dòng)向,開(kāi)發(fā)上有啥問(wèn)題可以隨時(shí)發(fā)帖,歡迎大家隨時(shí)到論壇來(lái)討論、交流和學(xué)習(xí)游戲開(kāi)發(fā)的最新技術(shù)。 :grin:

那么,回過(guò)頭來(lái),今天要探討的話題是 Godot 中極容易被新手忽視的 Resource 資源類。開(kāi)發(fā)過(guò) Unity 游戲的同學(xué)們知道一個(gè)叫 ScriptableObject 的很有用的類,它可以用于數(shù)據(jù)的包裝,在不少場(chǎng)合中應(yīng)該是非常有用的,那么在 Godot 中有沒(méi)有這個(gè)類似的特性呢?嗯,也有,這就是我們今天要談到的 Resource 資源類型。

官網(wǎng)也有對(duì) Resources 的相關(guān)介紹,我們知道場(chǎng)景是不能拖拽的,也是固定不變的,如果要用場(chǎng)景來(lái)保存一些普通數(shù)據(jù),肯定不太合理,這時(shí)候我們可以使用 Resource 資源類。相比 Node 其優(yōu)點(diǎn)也很明顯,使用非常靈活,同樣可以編寫(xiě)腳本,可以定義屬性和方法,創(chuàng)建資源文件方便,直接拖拽應(yīng)用即可。 "OK, FINE!" 這些我都會(huì)談到,更重要的是,我今天會(huì)利用 Resource 提出一個(gè)全新的、靈活的、“強(qiáng)力”解耦的 EventBus 全局事件模式。感興趣嗎?那我們繼續(xù)。

主要內(nèi)容: Resource 的相關(guān)用法簡(jiǎn)介
閱讀時(shí)間: 8 分鐘
永久鏈接: http://liuqingwen.me/2020/08/17/godot-game-devLog-3-talk-about-resource/
系列主頁(yè): http://liuqingwen.me/introduction-of-godot-series/

二、正文

Resource 并不神秘,但是很容易被忽視。其實(shí)我們平時(shí)創(chuàng)建的場(chǎng)景、節(jié)點(diǎn)中就包含了各種不同類型的資源文件,官網(wǎng)中的一張圖展示了某些節(jié)點(diǎn) Node 和資源 Resource 的關(guān)系:

Nodes and Resources

相信上圖中的名稱都不陌生,游戲場(chǎng)景開(kāi)發(fā)過(guò)程中可能會(huì)使用上多種資源類型,常見(jiàn)的就有:圖片資源、碰撞圖形、各種材質(zhì)、 UI 主題、音頻流、漸變、曲線等等,甚至我們常用的 AnimationPlayer 節(jié)點(diǎn)中創(chuàng)建的動(dòng)畫(huà),以及 GDScript 腳本、著色器代碼也都是資源。

常用資源類型

資源的創(chuàng)建和使用也非常簡(jiǎn)單,不過(guò),目前在 Godot 3 版本中也存在一些局限性,接下來(lái)我們?cè)敿?xì)聊聊。

Resource 的創(chuàng)建與使用

創(chuàng)建 Resource 資源的方式就有多種,平常都是在 Node 節(jié)點(diǎn)的屬性面板中直接創(chuàng)建,比如 New 一個(gè)玩家的碰撞體圖形的形狀,或是動(dòng)畫(huà)播放器中的各種動(dòng)畫(huà),粒子系統(tǒng)新建的材質(zhì)等等,這些資源有一個(gè)特點(diǎn):我們開(kāi)箱即用,很少保存。

資源文件也可以單獨(dú)創(chuàng)建,假設(shè)我們需要?jiǎng)?chuàng)建一個(gè)需要在很多地方使用的資源,比如通用的主題資源、字體資源、瓦片地圖 TileSet 資源等等,那么我們可以單獨(dú)創(chuàng)建相應(yīng)類型的資源文件,保存起來(lái),在不同場(chǎng)景中輕松實(shí)現(xiàn)重復(fù)利用。在屬性面板或者節(jié)點(diǎn)屬性中都可以新建資源文件:

創(chuàng)建并保存資源文件

新建資源文件后記得保存,保存的文件后綴名一般是 .tres 也有 .res 文件類型的,區(qū)別在于以文本格式保存還是二進(jìn)制文件格式保存:

保存資源為文件

保存好的資源文件我們可以隨時(shí)修改其相關(guān)屬性值,雙擊資源文件即可,另外,也可以創(chuàng)建多個(gè)副本,比如字體資源復(fù)制( duplicate )一份,然后修改字體大小屬性,使用在不同的地方。

資源的使用方式就簡(jiǎn)單了,可以直接拖拽到對(duì)應(yīng)屬性中,也可以在屬性下拉列表中點(diǎn)擊 Load 加載。系統(tǒng)自帶的資源比較齊全,當(dāng)然我們也可以自定義資源類型。資源從本質(zhì)上來(lái)說(shuō)仍然是一種腳本文件,創(chuàng)建自定義資源首先需要?jiǎng)?chuàng)建一個(gè)繼承自 Resource 類的腳本:

# 繼承自 Resource 說(shuō)明這是一個(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)相對(duì)應(yīng)的自定義資源類型,這一系列過(guò)程可以參考下圖:

創(chuàng)建自定義資源以及資源實(shí)例

是不是非常簡(jiǎn)單?趕緊動(dòng)手創(chuàng)建一個(gè)壓壓驚。 :joy:

Resource 相關(guān)問(wèn)題與局限

資源的創(chuàng)建和使用確實(shí)簡(jiǎn)單,不過(guò) Godot 3 中對(duì)于自定義資源還是有點(diǎn)小坑,這里提出來(lái),希望對(duì)新手朋友們有用。

1. 不能使用自定義 Resource 為變量類型

我們創(chuàng)建自定義資源時(shí),可以給資源定義個(gè)類名 class_name CustomResource ,但是在代碼中確不能定義該類型的資源變量:

var resource1 : Resource # 沒(méi)問(wèn)題
var resource2 : CustomResource # 不支持!

上面的代碼運(yùn)行會(huì)報(bào)錯(cuò):

built-in:4 - Parse Error: Invalid export type. Only built-in and native resource types can be exported.

避免這個(gè)問(wèn)題的方法就是使用父類型 Resource 作為變量的類型,不過(guò)這樣會(huì)導(dǎo)致在 export 屬性中可以賦予任意類型的資源文件,非常不方便、不人道。當(dāng)然你可以在代碼中進(jìn)行判斷:

if resource && resource is CustomResource:
    # 代碼...

不過(guò),好消息是這個(gè)問(wèn)題會(huì)在 Godot 4.0 中得到解決。

2. 使用 Resouce 要注意資源是引用類型

如果一個(gè)資源文件被多個(gè)節(jié)點(diǎn)使用,這個(gè)時(shí)候你只要改變了某個(gè)節(jié)點(diǎn)下該資源的任意一個(gè)屬性,結(jié)果都會(huì)導(dǎo)致其他節(jié)點(diǎn)下該資源跟隨發(fā)生變化!

舉個(gè)例子,游戲資源中有一個(gè) font_resource.res 字體資源文件,當(dāng)你改變了資源屬性中字體的大小后,其他所有使用了該資源的 UI 界面字體都會(huì)發(fā)生改變。這也是為什么新手們經(jīng)常會(huì)遇到這種情況:創(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è)資源,這也不算什么大問(wèn)題,我們可以右鍵資源文件 Duplicate 復(fù)制一個(gè),或者使用 Make Unique 方式使指定資源唯一化。

3. 使用 Resouce 要注意避免循環(huán)引用

如果你的項(xiàng)目中創(chuàng)建了不少自定義資源文件,自定義資源代碼中又引用了其他類型的資源,那么有可能會(huì)出現(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)引用問(wèn)題( Circular reference )在普通 GD 代碼中也會(huì)出現(xiàn),而出現(xiàn)在自定義資源中則會(huì)變得難以發(fā)覺(jué)。解決這個(gè)問(wèn)題的方法就是不要在編輯器中直接給資源賦值,轉(zhuǎn)而在運(yùn)行時(shí)判斷然后動(dòng)態(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)該比較少見(jiàn),暫時(shí)不做深入討論,后面的文章遇到了再詳述,當(dāng)然,我們翹首以待的 4.0 版本會(huì)解決這個(gè)問(wèn)題。

4. 其他的小問(wèn)題

如果修改資源腳本中的圖標(biāo)或者類名后,其他引用了這個(gè) Resource 的代碼就會(huì)報(bào)錯(cuò),類似 Resource 類已經(jīng)損壞,加載不完整之類。重新啟動(dòng)項(xiàng)目就可以了。

有時(shí)候還會(huì)遇到這種小 BUG :

core/script_language.cpp:244 - Condition "!global_classes.has(p_class)" is true. Returned: String()

有點(diǎn)莫名,也不容易重現(xiàn),我估計(jì)是修改了 Resource 腳本類名引起的,反正重啟項(xiàng)目就沒(méi)事了。 :joy:

這些小問(wèn)題說(shuō)明目前 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ù)容器一般沒(méi)有其他功能,只是獨(dú)立保存一些應(yīng)用數(shù)據(jù),不論是修改還是使用都非常方便且靈活。

舉個(gè)具有實(shí)際應(yīng)用場(chǎng)景的例子,在一個(gè) Player 或者 AI 腳本中,如果存在著大量數(shù)據(jù)屬性,而這些數(shù)據(jù)屬性一般不會(huì)發(fā)生改變,或者只是一些配置參數(shù),那么我們完全可以將其抽離出來(lái)作為一個(gè)單獨(dú)的數(shù)據(jù)類——這也是《重構(gòu)-改善既有代碼的設(shè)計(jì)》一書(shū)中提倡的重構(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è)資源文件,在編輯器的屬性面板中修改對(duì)應(yīng)的屬性值,在其他代碼中使用起來(lái)非常方便:

export var dataResource : Resource = null

fun _ready() -> void:
    if dataResource != null:
        print(dataResource.name, dataResource.moveSpeed, dataResource.rotateSpeed)

作為數(shù)據(jù)容器和 ScriptableObject 有點(diǎn)類似,接下來(lái)我們看 Resource 的另一個(gè)非常有用的場(chǎng)景。

用 Resource 創(chuàng)建全局事件的 EventBus

可以說(shuō)這是本文的重點(diǎn),目前我還沒(méi)有看到有任何人在項(xiàng)目中使用過(guò)這種方式,且聽(tīng)我慢慢道來(lái)~~~

首先,關(guān)于 Godot 中的 signal 信號(hào)以及觀察者模式相信大家都已經(jīng)駕輕就熟了,一般在游戲開(kāi)發(fā)中我們都會(huì)準(zhǔn)守 signal up, call down 的準(zhǔn)則,即往上層發(fā)送信號(hào),往下層直接調(diào)用。當(dāng)游戲變得越來(lái)越復(fù)雜的時(shí)候,信號(hào)可能已經(jīng)充滿了整個(gè)項(xiàng)目,比如某個(gè)多人游戲中信息面板需要接收并顯示多種不同類型的信號(hào):玩家按下回車鍵發(fā)送的文字信息、玩家某個(gè)戰(zhàn)場(chǎng)獲得勝利發(fā)出的信號(hào)、某個(gè)玩家退出游戲發(fā)出的信號(hào)、官方服務(wù)器推送的信息等等,因?yàn)檫@些信息發(fā)生在不同的場(chǎng)景,處理起來(lái)并不簡(jiǎn)單,我能想到的解決方式有這么幾種:

  1. 使用 get_node('../root/node_path') 方式,不推薦并表示強(qiáng)烈譴責(zé),這會(huì)造成強(qiáng)耦合,擴(kuò)展、維護(hù)和重構(gòu)極其困難
  2. 使用 Global AutoLoad ,也就是 Singleton 單例模式,有效解決耦合,但是維護(hù)相當(dāng)困難,牽一發(fā)而動(dòng)全身,調(diào)試?yán)щy
  3. 使用 Resource 創(chuàng)建相應(yīng)的事件資源,強(qiáng)力解耦,使用起來(lái)非常方便,調(diào)試也非常簡(jiǎn)單,易擴(kuò)展和維護(hù)

關(guān)于第二種方式是大家推薦的模式,我在之前的示例中就使用過(guò):(Godot游戲開(kāi)發(fā)實(shí)踐之一:使用High Level Multiplayer API制作多人游戲(上)), GDQuest 的文檔中也介紹了這種模式: https://www.gdquest.com/docs/guidelines/best-practices/godot-gdscript/event-bus/ ,示例代碼如下:

# 這是一個(gè) AutoLoad 單例
extends Node
# 可以定義多個(gè)通用信號(hào)
signal new_message(content)

# 其他代碼...

其他場(chǎng)景中使用也非常簡(jiǎn)單:

# 場(chǎng)景 1 中發(fā)送信號(hào):
GameConfig.emit_signal('new_message', '......')

# 場(chǎng)景 2 中接收處理信號(hào):
GameConfig.connect('new_message', self, '_on_NewMessage_arrive')

但是這種方式有一個(gè)很大的缺陷:全局引用導(dǎo)致重構(gòu)困難。因?yàn)閱卫喈?dāng)于全局模式,任何地方都可以引用,重構(gòu)時(shí)一旦改動(dòng)單例中某個(gè)方法或者屬性都有可能引起其他地方因?yàn)橐檬Ф鴮?dǎo)致運(yùn)行奔潰,尋找這些引用并不容易,這也為什么 GDQuest 推薦的 EventBus 模式是單獨(dú)創(chuàng)建的只有信號(hào)沒(méi)有其他代碼的腳本文件。

廢話一堆,一起來(lái)看看利用 Resource 創(chuàng)建的事件模式吧!首先創(chuàng)建一個(gè)事件資源:

# 自定義資源
extends Resource
class_name EventResource, 'res://EventResource/event_icon.svg'
# 自定義信號(hào)
signal custom_event(type, message)
# 可以定義一些屬性
export var type := 'defaultEvent'
# 自定義方法用于發(fā)送信號(hào)的包裝,也可以直接發(fā)送信號(hào)
func emitSignal(object) -> void:
    self.emit_signal('custom_event', type, object)

接下來(lái),我們可以創(chuàng)建一些事件資源文件,比如 message_event.tres trigger_event.tres ,不同的文件可以更改、配置不同的參數(shù),然后在其他腳本中使用:

export var messageEvent : Resource = null
export var triggerEvent : Resource = null

# 可以使用事件資源偵聽(tīng)事件
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)屬性中可以直接拖拽使用,而且可有可無(wú),均不影響整個(gè)項(xiàng)目的運(yùn)行,在本示例中玩家的屬性配置如下圖:

玩家相關(guān)設(shè)置

可以看到 Player1 只接收 message_event 事件, Player3 只派發(fā) trigger_event 事件,而 Player2 則無(wú)任何配置,可謂一目了然。

總結(jié)一下使用 Resource 創(chuàng)建事件的一些優(yōu)點(diǎn):

  1. 強(qiáng)力解耦!不依賴其他文件或者腳本、節(jié)點(diǎn),很容易進(jìn)行重構(gòu)
  2. 便于調(diào)試,代碼中只要注意 null 引用即可,刪除或者添加相關(guān)事件都非常友好
  3. 便于測(cè)試,修改事件相關(guān)屬性值非常方便,一改全改
  4. 可以考慮在大型項(xiàng)目中應(yīng)用

并沒(méi)有十全十美的萬(wàn)能解決方案,當(dāng)然也是有缺點(diǎn)的,比如一堆的只是改變了某一個(gè)變量值的 .res 文件等。重要的是,目前還沒(méi)有實(shí)際項(xiàng)目支持這個(gè)事件模式,有待大家的開(kāi)發(fā)和探索啊。 :smile:

三、總結(jié)

好了,這篇就聊了一個(gè)簡(jiǎn)單的 Resource 話題,希望能給新手朋友們帶來(lái)一點(diǎn)點(diǎn)幫助,給高手朋友們開(kāi)拓一點(diǎn)點(diǎn)亮光,那這篇文章也就值了。

記住我們 Godot 愛(ài)好者的新家: Godot中文社區(qū) ,歡迎常回家看看!

本篇的 Demo 以及相關(guān)代碼已經(jīng)上傳到 Github ,地址: https://github.com/spkingr/Godot-Demos , 后續(xù)繼續(xù)更新,原創(chuàng)不易,希望大家喜歡! :smile:

我的博客地址: http://liuqingwen.me ,我的博客即將同步至騰訊云+社區(qū),邀請(qǐng)大家一同入駐: https://cloud.tencent.com/developer/support-plan?invite_code=3sg12o13bvwgc ,歡迎關(guān)注我的微信公眾號(hào)(第一時(shí)間更新+游戲開(kāi)發(fā)資源+相關(guān)資訊):

IT自學(xué)不成才

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容