雜談——從Godot遷移到Defold可能用得到的一些東西
運(yùn)行結(jié)構(gòu)、定位系統(tǒng)
先從游戲世界說(shuō)起。在項(xiàng)目設(shè)置中規(guī)定一個(gè)啟動(dòng)集合,進(jìn)入游戲時(shí)這個(gè)“啟動(dòng)集合”會(huì)形成一個(gè)「主游戲世界」。運(yùn)行時(shí),通過(guò)集合代理創(chuàng)建各種各樣的子游戲世界。
運(yùn)行時(shí),游戲世界下只有對(duì)象層級(jí)和組件層級(jí)。集合形成的嵌套只改變對(duì)象在定位系統(tǒng)的Path,集合內(nèi)的對(duì)象間嵌套僅僅形成了針對(duì)部分屬性的相對(duì)綁定關(guān)系,【因此,對(duì)象間嵌套后,父子對(duì)象URL實(shí)際上是同層級(jí)分立的】
因此,忘掉SceneTree的自由嵌套和get_node(path)吧!在Defold中我們使用URL來(lái)進(jìn)行組件的定位,先從絕對(duì)地址講起,完整的絕對(duì)地址比較像這個(gè)樣子:
a_world:/a_sub_collection/an_object#a_component
有一點(diǎn)需要注意:一個(gè)集合可能被加載成一個(gè)新的游戲世界,也有可能只是用集合工廠產(chǎn)生的一般集合。因此,編輯器Properties面板內(nèi)顯示的URL是相對(duì)于你所編輯的根集合而言的,不包含根集合的名稱(chēng)。
工廠和集合工廠產(chǎn)生的對(duì)象會(huì)直接放在當(dāng)前的游戲世界根部,它們的絕對(duì)地址會(huì)比較像這個(gè)格式:
a_world:/my_game_object0
a_world:/my_collection0/sth_game_object
Defold定位系統(tǒng)還有個(gè)稱(chēng)作ID的概念,但I(xiàn)D的語(yǔ)境義比較多:
- ?對(duì)象名稱(chēng),如Properties面板里給出的id
- ?去除socket(游戲世界標(biāo)識(shí))的絕對(duì)地址。例如,把factory.create(sth_factory_url)的返回值打印出來(lái),會(huì)得到形如“hash: [/instance42]”這樣的輸出,而其中的“/instance42”就是去除socket的絕對(duì)地址。
- ?消息/輸入動(dòng)作的哈希值
有點(diǎn)混亂,因此個(gè)人的建議是API里看到id什么的要反應(yīng)過(guò)來(lái),平時(shí)就避開(kāi)這個(gè)說(shuō)法吧(
接下來(lái)就是各種速記符了,先介紹最簡(jiǎn)單的兩種:
“.” ?根據(jù)上下文評(píng)估,定位到當(dāng)前對(duì)象上
“#” 根據(jù)上下文評(píng)估,定位到當(dāng)前腳本
需要注意,這兩種速記符【不是通配符】,因此“.#a_component”這種寫(xiě)法是定位不上的。
有時(shí)需要保證我們的腳本組件具有一定的泛用性。Defold用相對(duì)地址來(lái)解決這個(gè)問(wèn)題,相對(duì)地址也是兩種:
- ?組件級(jí)的相對(duì)地址,如“#a_component”,用于控制同屬一個(gè)對(duì)象的其他組件。
- ?對(duì)象級(jí)的相對(duì)地址,如“an_object”、“an_object#a_component”,可以控制同一集合層級(jí)的其他對(duì)象,以及它們的組件。
相對(duì)地址沒(méi)有提供跨集合定位的能力,針對(duì)這種需求,使用絕對(duì)地址是比較穩(wěn)妥的選擇。
最后,URL有三種形式:字符串urlstring、哈希值hash,以及msg.url()產(chǎn)生的URL包。調(diào)用需要使用URL的API時(shí),如果傳入的是urlstring和hash,Defold就會(huì)走一次將urlstring/hash轉(zhuǎn)換為URL包的流程,因此預(yù)存URL包也是一種行之有效的優(yōu)化手段。
消息系統(tǒng)、實(shí)現(xiàn)分組
個(gè)人而言,在Godot4(GDScript2.0)引入了Signal Callable概念,支持“信號(hào)連接到函數(shù)”寫(xiě)法之前,在Godot里用信號(hào)來(lái)實(shí)現(xiàn)組件間溝通是比較痛苦的。
而Defold的想法和Godot不太一樣。依托定位系統(tǒng)的“超能力”(不是),他們選擇了點(diǎn)對(duì)點(diǎn)“私聊”的方法解決問(wèn)題?—— URL能觸及到的地方就是消息的可達(dá)之地。
一般通過(guò)msg.post()方法發(fā)消息,消息可以發(fā)到一個(gè)對(duì)象,也可以發(fā)到特定組件。
這里特別講一下腳本接收消息:除開(kāi)一部分“引擎預(yù)先規(guī)定的消息”,發(fā)到對(duì)象的消息會(huì)引發(fā)對(duì)象下每個(gè)腳本組件的on_message()回調(diào)。
接下來(lái)關(guān)注一下消息相關(guān)API:
msg.post方法給出了三個(gè)參數(shù):receiver、message_id、[message]
on_message回調(diào)中有四個(gè)參數(shù):self、message_id、message、sender。
(這里的receiver其實(shí)就是URL,三種形式都可以)
從這里可以窺見(jiàn)消息的組成:message_id和message。游戲內(nèi)相互作用情況繁雜,因此需要區(qū)分不同種類(lèi)的消息。在Defold中,用來(lái)區(qū)分消息種類(lèi)的手段就是message_id(消息的哈希值)。
方便起見(jiàn),msg.post()允許直接輸入string充當(dāng)message_id的參數(shù)值,也可以用hash,但on_message()的message_id只可能是hash,這意味著如果msg.post的message_id傳入的是字符串,系統(tǒng)在幕后會(huì)進(jìn)行一次轉(zhuǎn)換。
因此,務(wù)必養(yǎng)成用hash來(lái)表示消息id的習(xí)慣。一般使用hash()方法將字符串顯式轉(zhuǎn)換為符合條件的hash值,為了避免反復(fù)調(diào)用hash()方法,建議將hash的返回值預(yù)存下來(lái)。
不過(guò),URL包優(yōu)于urlstring/hash,因此如果要轉(zhuǎn)換/預(yù)存URL,建議直接轉(zhuǎn)到URL包。
那么message_id旁邊message又是什么呢?message就是消息夾帶的內(nèi)容了,Defold支持“夾帶”兩種內(nèi)容:Lua Table,還有nil——也就是不夾帶()
最后,Godot中時(shí)不時(shí)會(huì)用到的call_group()又怎么實(shí)現(xiàn)呢?—— 比較遺憾,一般是腳本里把URL之類(lèi)的預(yù)存好,然后需要的時(shí)候遍歷表手搓。。。。。。
空間、視口
Defold雖然劃分了不同的游戲世界,但各個(gè)游戲世界共用一套世界坐標(biāo)空間,內(nèi)容也是完全混放的。
一般只要注意到不同游戲世界對(duì)應(yīng)不同的物理層,就不會(huì)有問(wèn)題。只是,如果有Camera放到不同的組件、不同的集合,甚至不同的游戲世界,應(yīng)該怎么處理呢?答案是給相機(jī)組件發(fā)“acquire_camera_focus”消息,相機(jī)就會(huì)成為當(dāng)前的主相機(jī)。
(暫時(shí)還不確定Camera是可以拍到整個(gè)空間的對(duì)象,還是所在世界的對(duì)象。個(gè)人傾向于前者)
還有一點(diǎn)需要注意:GUI組件為了強(qiáng)制置頂渲染,不會(huì)在游戲世界內(nèi),建議單獨(dú)研究一下屏幕坐標(biāo)系、屏幕布局文件等內(nèi)容。
屬性、腳本內(nèi)的self、全局變量與運(yùn)行時(shí)、各種局部變量
有時(shí)候,游戲?qū)ο笮枰玫揭恍┳远x屬性。在Defold中,這些自定義屬性掛在腳本上。個(gè)人首推在腳本中直接用go.property()方法直接導(dǎo)出到檢查器內(nèi),可以確保各個(gè)腳本實(shí)例屬性的獨(dú)立性、可觸及性(API支持了用URL+屬性字段來(lái)遠(yuǎn)程修改屬性),并且易于編輯。
這里需要穿插一些Lua在Defold的工作模式:
對(duì)于Defold游戲,如果在項(xiàng)目設(shè)置啟用了Shared State,那么script腳本、render腳本和gui_script腳本三者會(huì)在同一個(gè)Lua運(yùn)行時(shí)工作。但關(guān)掉這個(gè)選項(xiàng)的情形也不是那么理想—— 三種腳本分別對(duì)應(yīng)三個(gè)Lua運(yùn)行時(shí)(
然而,每個(gè)腳本實(shí)例都需要管理自己的一套參數(shù),到這里就引出了幾種方法:
- ?如果只需要在幾個(gè)生命周期函數(shù)作更新的話,可以使用self,是專(zhuān)屬于腳本實(shí)例的userdata。
- ?在腳本文件的根部定義local變量,如果是內(nèi)部參數(shù)的話個(gè)人比較推薦這個(gè)方法,可以把作用域控制在一個(gè)實(shí)例內(nèi)。
- ?直接扔腳本屬性,推薦用于需要大量交互的情形,可以節(jié)約一些消息通信成本。此外,腳本屬性也會(huì)存入self,self能夠到的地方就用不著URL什么的了。
- ?邪道手搓!?。。。。?/p>
因?yàn)檎麄€(gè)游戲在一個(gè)(或者三個(gè))Lua運(yùn)行時(shí)玩大亂燉,所以如果要保存一些整個(gè)游戲都要用到的變量什么的,只需在聲明變量時(shí)不加local就行了,相當(dāng)之邪乎。。。作死的意義上遍歷下_G什么的倒也不是不可以(被打死)
最后補(bǔ)充點(diǎn)常識(shí):文件根部local適用于整個(gè)文件,代碼塊內(nèi)的local適用于當(dāng)前代碼塊在聲明之后的部分(含子級(jí)),重名情形作用域小的遮蔽作用域大的。
認(rèn)識(shí)輸入棧
之前一直說(shuō)(項(xiàng)目設(shè)置里配置的)啟動(dòng)集合會(huì)制造一個(gè)「主游戲世界」,但頂級(jí)集合構(gòu)造的游戲世界在URL上看不出區(qū)別,為什么需要分主次呢?
這就要說(shuō)到輸入棧了。Defold的輸入信號(hào)沿著輸入棧進(jìn)行傳遞,這種傳遞是逐個(gè)游戲世界進(jìn)行的,而不是并行,他們的做法是:
把集合代理壓入輸入棧,集合代理就能把傳給它的輸入消息“導(dǎo)向”另外一個(gè)游戲世界了。
而啟動(dòng)集合對(duì)應(yīng)的輸入棧正好接管著整個(gè)輸入系統(tǒng)的“入口”——從這個(gè)意義上來(lái)說(shuō),游戲世界是有主次之分的,嗯。
前面提到輸入信號(hào)沿輸入棧傳遞,那么怎樣讓腳本檢測(cè)到輸入呢?
—— ?對(duì)腳本所在的游戲?qū)ο蟀l(fā)“acquire_input_focus”消息,對(duì)象的各個(gè)組件就會(huì)被壓入輸入棧了。和之前的消息機(jī)制同理,其后用戶的輸入操作會(huì)引起對(duì)象上每個(gè)腳本的on_input()回調(diào)。
—— ?暫時(shí)不支持只把腳本壓入輸入棧。
—— ?這里補(bǔ)充一下輸入棧的傳遞次序:“后來(lái)居上”,后“壓入”的對(duì)象先得到輸入信息。
雖然順次接收輸入會(huì)極大增加(引擎)多線程優(yōu)化的難度,但這個(gè)機(jī)制也帶來(lái)了一個(gè)好處——輸入消耗:
在on_input()回調(diào)返回一個(gè)真值(一般用true,作死可以用0什么的),輸入棧對(duì)應(yīng)的一環(huán)就會(huì)把這個(gè)輸入直接吞掉,而在Godot里改變輸入狀態(tài)需要手搓各種set_input,從這個(gè)角度上講也算是互有優(yōu)缺。
至于各種輸入的具體實(shí)現(xiàn),可以查官方手冊(cè)這里就不展開(kāi)了()
雜項(xiàng)
Q:Godot很強(qiáng)大很好用,為什么要遷移到Defold?
A:Defold有非常多的優(yōu)點(diǎn),比如:
-- ?編輯器+全平臺(tái)打包模版控制在了300MB這個(gè)量級(jí),而Godot4是編輯器60-70MB但打包模版600-700MB
-- ?在安卓端支持了LuaJIT,比這玩更強(qiáng)的應(yīng)該就是AOT C#腳本或者用底層語(yǔ)言寫(xiě)的原生代碼了,比如il2cpp之類(lèi)的
-- ?打包時(shí)不需要裝外部開(kāi)發(fā)環(huán)境(但iOS打包還是離不開(kāi)macOS),甚至可以在項(xiàng)目里直接配置manifestion文件,打包時(shí)再削減點(diǎn)體積
-- ?面向整個(gè)游戲引入了最大項(xiàng)目數(shù)優(yōu)化,通過(guò)Factory?/ Collection Factory生成的實(shí)例有自動(dòng)的對(duì)象池優(yōu)化
-- ?編輯器內(nèi)建了圖集(Atlas)支持,對(duì)強(qiáng)迫癥友善(不是)
A:不過(guò)最關(guān)鍵的是個(gè)人更喜歡輕量而夠用的東西,他們的設(shè)計(jì)也是很不錯(cuò)的值得支持.
Q:Defold有沒(méi)有一些很坑的點(diǎn)?
A:暫時(shí)有這么一些:
-- ?Sprite必須使用Tile Source或者圖集,不能導(dǎo)入單張圖片
-- ?字體必須手動(dòng)轉(zhuǎn)為貼圖
-- ?自帶的JSON庫(kù)效率不高,并且不支持Stringfy,想解決這個(gè)問(wèn)題要給項(xiàng)目單獨(dú)綁定cJSON,但cJSON素質(zhì)過(guò)硬,其實(shí)是非常值得的
-- ?文本編輯器的自帶字體對(duì)空格的處理非常逆天,個(gè)人建議手動(dòng)換成JetBrains Mono Regular
-- ?沒(méi)有用戶數(shù)據(jù)目錄,不引入第三方插件就只能讀寫(xiě)存檔。
-- ?按鈕要手搓
-- ?......
Q:以后怎么辦?
A:個(gè)人的目標(biāo)是做一系列畫(huà)面比較素的音游,這種情況下Defold是非常合適的;Godot有非常強(qiáng)的開(kāi)箱即用性、相對(duì)完善的3D支持,和對(duì)PC GUI的超強(qiáng)布局能力,拿來(lái)做小工具,或者對(duì)性能要求不是那么高的項(xiàng)目的話應(yīng)該是個(gè)不錯(cuò)的選擇。
Q:為什么不用Unity?
A:這種洪水猛獸更適合業(yè)內(nèi)人士,不適合我。(