杂谈——从Godot迁移到Defold可能用得到的一些东西
运行结构、定位系统
先从游戏世界说起。在项目设置中规定一个启动集合,进入游戏时这个“启动集合”会形成一个「主游戏世界」。运行时,通过集合代理创建各种各样的子游戏世界。
运行时,游戏世界下只有对象层级和组件层级。集合形成的嵌套只改变对象在定位系统的Path,集合内的对象间嵌套仅仅形成了针对部分属性的相对绑定关系,【因此,对象间嵌套后,父子对象URL实际上是同层级分立的】
因此,忘掉SceneTree的自由嵌套和get_node(path)吧!在Defold中我们使用URL来进行组件的定位,先从绝对地址讲起,完整的绝对地址比较像这个样子:
a_world:/a_sub_collection/an_object#a_component
有一点需要注意:一个集合可能被加载成一个新的游戏世界,也有可能只是用集合工厂产生的一般集合。因此,编辑器Properties面板内显示的URL是相对于你所编辑的根集合而言的,不包含根集合的名称。
工厂和集合工厂产生的对象会直接放在当前的游戏世界根部,它们的绝对地址会比较像这个格式:
a_world:/my_game_object0
a_world:/my_collection0/sth_game_object
Defold定位系统还有个称作ID的概念,但ID的语境义比较多:
- 对象名称,如Properties面板里给出的id
- 去除socket(游戏世界标识)的绝对地址。例如,把factory.create(sth_factory_url)的返回值打印出来,会得到形如“hash: [/instance42]”这样的输出,而其中的“/instance42”就是去除socket的绝对地址。
- 消息/输入动作的哈希值
有点混乱,因此个人的建议是API里看到id什么的要反应过来,平时就避开这个说法吧(
接下来就是各种速记符了,先介绍最简单的两种:
“.” 根据上下文评估,定位到当前对象上
“#” 根据上下文评估,定位到当前脚本
需要注意,这两种速记符【不是通配符】,因此“.#a_component”这种写法是定位不上的。
有时需要保证我们的脚本组件具有一定的泛用性。Defold用相对地址来解决这个问题,相对地址也是两种:
- 组件级的相对地址,如“#a_component”,用于控制同属一个对象的其他组件。
- 对象级的相对地址,如“an_object”、“an_object#a_component”,可以控制同一集合层级的其他对象,以及它们的组件。
相对地址没有提供跨集合定位的能力,针对这种需求,使用绝对地址是比较稳妥的选择。
最后,URL有三种形式:字符串urlstring、哈希值hash,以及msg.url()产生的URL包。调用需要使用URL的API时,如果传入的是urlstring和hash,Defold就会走一次将urlstring/hash转换为URL包的流程,因此预存URL包也是一种行之有效的优化手段。
消息系统、实现分组
个人而言,在Godot4(GDScript2.0)引入了Signal Callable概念,支持“信号连接到函数”写法之前,在Godot里用信号来实现组件间沟通是比较痛苦的。
而Defold的想法和Godot不太一样。依托定位系统的“超能力”(不是),他们选择了点对点“私聊”的方法解决问题 —— URL能触及到的地方就是消息的可达之地。
一般通过msg.post()方法发消息,消息可以发到一个对象,也可以发到特定组件。
这里特别讲一下脚本接收消息:除开一部分“引擎预先规定的消息”,发到对象的消息会引发对象下每个脚本组件的on_message()回调。
接下来关注一下消息相关API:
msg.post方法给出了三个参数:receiver、message_id、[message]
on_message回调中有四个参数:self、message_id、message、sender。
(这里的receiver其实就是URL,三种形式都可以)
从这里可以窥见消息的组成:message_id和message。游戏内相互作用情况繁杂,因此需要区分不同种类的消息。在Defold中,用来区分消息种类的手段就是message_id(消息的哈希值)。
方便起见,msg.post()允许直接输入string充当message_id的参数值,也可以用hash,但on_message()的message_id只可能是hash,这意味着如果msg.post的message_id传入的是字符串,系统在幕后会进行一次转换。
因此,务必养成用hash来表示消息id的习惯。一般使用hash()方法将字符串显式转换为符合条件的hash值,为了避免反复调用hash()方法,建议将hash的返回值预存下来。
不过,URL包优于urlstring/hash,因此如果要转换/预存URL,建议直接转到URL包。
那么message_id旁边message又是什么呢?message就是消息夹带的内容了,Defold支持“夹带”两种内容:Lua Table,还有nil——也就是不夹带()
最后,Godot中时不时会用到的call_group()又怎么实现呢?—— 比较遗憾,一般是脚本里把URL之类的预存好,然后需要的时候遍历表手搓。。。。。。
空间、视口
Defold虽然划分了不同的游戏世界,但各个游戏世界共用一套世界坐标空间,内容也是完全混放的。
一般只要注意到不同游戏世界对应不同的物理层,就不会有问题。只是,如果有Camera放到不同的组件、不同的集合,甚至不同的游戏世界,应该怎么处理呢?答案是给相机组件发“acquire_camera_focus”消息,相机就会成为当前的主相机。
(暂时还不确定Camera是可以拍到整个空间的对象,还是所在世界的对象。个人倾向于前者)
还有一点需要注意:GUI组件为了强制置顶渲染,不会在游戏世界内,建议单独研究一下屏幕坐标系、屏幕布局文件等内容。
属性、脚本内的self、全局变量与运行时、各种局部变量
有时候,游戏对象需要用到一些自定义属性。在Defold中,这些自定义属性挂在脚本上。个人首推在脚本中直接用go.property()方法直接导出到检查器内,可以确保各个脚本实例属性的独立性、可触及性(API支持了用URL+属性字段来远程修改属性),并且易于编辑。
这里需要穿插一些Lua在Defold的工作模式:
对于Defold游戏,如果在项目设置启用了Shared State,那么script脚本、render脚本和gui_script脚本三者会在同一个Lua运行时工作。但关掉这个选项的情形也不是那么理想—— 三种脚本分别对应三个Lua运行时(
然而,每个脚本实例都需要管理自己的一套参数,到这里就引出了几种方法:
- 如果只需要在几个生命周期函数作更新的话,可以使用self,是专属于脚本实例的userdata。
- 在脚本文件的根部定义local变量,如果是内部参数的话个人比较推荐这个方法,可以把作用域控制在一个实例内。
- 直接扔脚本属性,推荐用于需要大量交互的情形,可以节约一些消息通信成本。此外,脚本属性也会存入self,self能够到的地方就用不着URL什么的了。
- 邪道手搓!!!!!!
因为整个游戏在一个(或者三个)Lua运行时玩大乱炖,所以如果要保存一些整个游戏都要用到的变量什么的,只需在声明变量时不加local就行了,相当之邪乎。。。作死的意义上遍历下_G什么的倒也不是不可以(被打死)
最后补充点常识:文件根部local适用于整个文件,代码块内的local适用于当前代码块在声明之后的部分(含子级),重名情形作用域小的遮蔽作用域大的。
认识输入栈
之前一直说(项目设置里配置的)启动集合会制造一个「主游戏世界」,但顶级集合构造的游戏世界在URL上看不出区别,为什么需要分主次呢?
这就要说到输入栈了。Defold的输入信号沿着输入栈进行传递,这种传递是逐个游戏世界进行的,而不是并行,他们的做法是:
把集合代理压入输入栈,集合代理就能把传给它的输入消息“导向”另外一个游戏世界了。
而启动集合对应的输入栈正好接管着整个输入系统的“入口”——从这个意义上来说,游戏世界是有主次之分的,嗯。
前面提到输入信号沿输入栈传递,那么怎样让脚本检测到输入呢?
—— 对脚本所在的游戏对象发“acquire_input_focus”消息,对象的各个组件就会被压入输入栈了。和之前的消息机制同理,其后用户的输入操作会引起对象上每个脚本的on_input()回调。
—— 暂时不支持只把脚本压入输入栈。
—— 这里补充一下输入栈的传递次序:“后来居上”,后“压入”的对象先得到输入信息。
虽然顺次接收输入会极大增加(引擎)多线程优化的难度,但这个机制也带来了一个好处——输入消耗:
在on_input()回调返回一个真值(一般用true,作死可以用0什么的),输入栈对应的一环就会把这个输入直接吞掉,而在Godot里改变输入状态需要手搓各种set_input,从这个角度上讲也算是互有优缺。
至于各种输入的具体实现,可以查官方手册这里就不展开了()
杂项
Q:Godot很强大很好用,为什么要迁移到Defold?
A:Defold有非常多的优点,比如:
-- 编辑器+全平台打包模版控制在了300MB这个量级,而Godot4是编辑器60-70MB但打包模版600-700MB
-- 在安卓端支持了LuaJIT,比这玩更强的应该就是AOT C#脚本或者用底层语言写的原生代码了,比如il2cpp之类的
-- 打包时不需要装外部开发环境(但iOS打包还是离不开macOS),甚至可以在项目里直接配置manifestion文件,打包时再削减点体积
-- 面向整个游戏引入了最大项目数优化,通过Factory / Collection Factory生成的实例有自动的对象池优化
-- 编辑器内建了图集(Atlas)支持,对强迫症友善(不是)
A:不过最关键的是个人更喜欢轻量而够用的东西,他们的设计也是很不错的值得支持.
Q:Defold有没有一些很坑的点?
A:暂时有这么一些:
-- Sprite必须使用Tile Source或者图集,不能导入单张图片
-- 字体必须手动转为贴图
-- 自带的JSON库效率不高,并且不支持Stringfy,想解决这个问题要给项目单独绑定cJSON,但cJSON素质过硬,其实是非常值得的
-- 文本编辑器的自带字体对空格的处理非常逆天,个人建议手动换成JetBrains Mono Regular
-- 没有用户数据目录,不引入第三方插件就只能读写存档。
-- 按钮要手搓
-- ......
Q:以后怎么办?
A:个人的目标是做一系列画面比较素的音游,这种情况下Defold是非常合适的;Godot有非常强的开箱即用性、相对完善的3D支持,和对PC GUI的超强布局能力,拿来做小工具,或者对性能要求不是那么高的项目的话应该是个不错的选择。
Q:为什么不用Unity?
A:这种洪水猛兽更适合业内人士,不适合我。(