Godot或许合格的程序架构
在写最近的项目Illness时,我在这个游戏中同时使用了两种架构(也不能说是架构?应该说是编程模式),一种是我写以前的项目常用的,另一种是突发奇想的,我们重点讨论后者
它的核心在于对global脚本和变量set函数的使用
众所周知,在游戏中大量使用global脚本是并不提倡的,因为它在程序开始时就会被加载且在退出程序前不会被free,那么在游戏的大部分时候,global脚本都是只占内存但不发挥作用,而且它还会拖慢游戏的启动时间
诚然如此,但在某一天我看到了这个视频:
Best Code Architectures For Indie Games
这个视频是《王座陨落》开发者在油管投的视频,b站上有很多熟肉,随便搜搜也能看到
这里说的独立系统+胶水的架构中,独立系统可以看作是每个对象的manager,而胶水在godot中就可以对应global脚本,这就是我们将要讨论的架构
global脚本的逻辑
假如你在编辑器中测试你的游戏时细心点的话,可以看到"远程"栏位中除了当前场景的节点外还有一个Node节点,这些Node节点就是编辑器自己创建的附加了global脚本的可以全局访问的Node节点(所以godot的核心仍没有变,依旧是node-base)
你可以理解为编辑器在创建了这个"全局Node"后,再拉了一根飞线连到所有节点上,让场景中的任何节点都可以直接访问这个"全局Node"
框架实践
现在我们假设要设计一个系统,当鼠标悬浮在卡牌上时,在一个面板中显示卡牌的细节内容
在此约定卡牌父类为Card,继承于Control,内含细节变量detail : String,面板为DetailPanel
在往常,或许我们的思路是:
- 卡牌父类中存有对DetailPanel的引用(这个写法很屎山,真要写项目的时候不要这么写,我们这里只是简化条件)
- 卡牌父类中有一函数连接到信号mouse_entered
- DetailPanel中有一函数,可传入卡牌的引用或直接传string来展示卡牌细节
- 当mouse_entered触发时,调用DetailPanel的展示函数并将卡牌的引用或细节变量的值传入
是的,这个流程是合理的,而且卡牌将细节变量的值传过去的情况是开销最小的流程
那么,假如这时候我加一个要求,要让展示卡牌细节时还要连带着展示卡面呢(这种情况是有的,详见《游戏王》)
这个时候直接传细节变量的值进去就不管用了,这时候直接传卡牌的引用明显是最简单且开销最小的
好的,那么更刁钻的要求就来了,假如我现在有一种卡牌,它使用后的效果是将某个指示物放在场上某处,要求是鼠标悬浮在指示物上时,显示卡牌的细节(这种情况也是有的,详见《三国杀》)
哦,那确实难办起来了,因为指示物显然是不能继承于Card父类的,那该怎么办?
或许这样也能继续写下去,在指示物里存有对创造者卡牌的引用和对DetailPanel的引用,当鼠标悬浮在指示物上时,将创造者卡牌传给DetailPanel…但是假如我又要求指示物的指示物呢🌚上面说了存有对DetailPanel的引用已经很折磨了,这样嵌套下去只会更折磨
好的,那么就得把上面的框架都掀了,来试试最新的框架吧
- 有一global脚本,存有变量current_hover_card,类型是Card,有一set函数,当值改变时,更改DetailPanel中的相应变量(这里为了简化就同名)
- DetailPanel中有一变量current_hover_card,有一set函数,当值改变时,更改展示内容为Card(两个set函数或许有些冗余了,但这样最终写出来代码也是很简洁的,当然,你想直接在global里调DetailPanel中的函数也行,只是越界了不太好)
- 卡牌父类中有一函数连接到信号mouse_entered,触发时更改global脚本中的current_hover_card
- 指示物中存有对创造者卡牌的引用,有一函数连接到mouse_entered,触发时更改global脚本中的current_hover_card
如此这般,或许你要问了,不是说存有DetailPanel的引用是很折磨的写法吗,为什么我还是在global里这样干了
哦,这里可不同了,由于所有node都可以随时随地修改global脚本,那么我们只需要在DetailPanel的_ready时将本身传给global脚本就行了(受依赖注入写法的熏陶,我会更习惯于专门写个initialize,但这无关紧要),所以说,要global存有对节点的引用只需要一行代码就行了
以上