写在前面
2022年虎年假期在百般不情愿下,还是结束了。不知道同学们这个假期过得怎么样?
老李我今年春节除了必要的走亲访友和休闲娱乐以外,空闲时间基本都用来折腾godot这个游戏引擎了。
最开始只是抱着尝鲜的态度试着玩儿了两天,没想到这个只有几十兆的游戏引擎异常好用(我只要使用2D部分)
以至于我已经很久没有启动Unreal引擎了(启动一次确实也比较慢)
未来一段时间我应该会更多精力在godot这款引擎上,争取创作一批项目实践课程,感兴趣的可以关注一下。
我(游戏策划)所理解的基于组件开发
注:以下内容只作为我作为一名游戏策划,而非专业的程序开发人员的主观理解,如有错误欢迎批评指正,不吝赐教,我将虚心改正。
很多文章会将面向过程、面向对象和面向组件开发相提并论,难免有混淆是非之嫌。
面向过程编程和面向对象编程是一种编程范式,很多编程语言在语言层面采用这种编程范式,因此在使用这些语言时需要时刻考虑面向对象的编程原则,也因此产生了很多基于面向对象的设计模式
面向对象试图将世界中的对象抽象归类,关于面向过程与面向对象的异同我们这里不过多论述。
事实上面向对象中就有“组合优于继承”的设计原则,进而上升到软件工程的层面,就成为我们今天所说的“基于组件开发”
回到游戏开发这个场景下,
面向对象试图将游戏对象归类,比如猫、狗,然后再给这些类型赋予能力,猫能抓老鼠,狗能看家护院。
但具体到实际游戏开发场景中,可能我们需要一个可以抓老鼠的狗。这时候我们开始发愁要不要让狗继承猫这个“父类”
我们发现随着电子游戏的发展,游戏规则的复杂导致开发者不得不考虑比继承更合适的代码组织方式。
如果引入类似CPP中的多父类继承,大概率会使代码结构混乱,难以维护,导致开发人员付出成倍的心智负担。
类似这种问题变多之后,静态类型语言创造出接口Interface这个概念,来满足这种需求
动态类型语言方面,因为其动态特性则灵活的多,也就是我们所说的“鸭子类型”:如果一个对象能游泳,能发出鸭子叫声,则可以认为这个对象是鸭子。
当然,这种灵活性也要付出代价,这里我们也不多谈。
更多人意识到此类问题,并尝试解决,于是google
推出了golang
,试图在语言层面解决一些工程问题,其中就包括上文提到的接口interface
golang
中的接口interface
究竟有什么不同,我们这里依然不展开讨论,但到此为止,我们可以理清编程语言的发展历程
编程语言的发展一定不是人为的主观干预,而是面对实际的工程问题时的自然演化。
理解了这一点,再看后来的基于数据开发、面向服务架构等概念就不会感到摸不到头脑了。
主流游戏引擎中的应用
在主流商业引擎上,Unity3D
毫无疑问是最优秀的基于组件开发的游戏引擎。
这也是Unity
引擎迅速占领游戏研发市场并且迄今为止仍占有很大市场份额的原因之一。
在Unity
引擎中,游戏对象被抽象为GameObject
,但GameObject
并不包含逻辑。而是作为若干个Component
的集合体。
Unity
引擎为我们提供了部分Component
,而开发者也可以很方便的创建自己的Component
来自定义游戏逻辑。而引擎则提供了方便的增删改查Component
的方法.
多个GameObject
可以组装成树状结构,并序列化为Prefab
(预制体)保存在硬盘中,在后来的版本中,Unity
还提供了Prefab
编辑器。
可以说组件Component
是Unity
引擎的基石。
Unity
引擎的成功很有启发性,以至于Unreal4
引擎也紧随其后宣布支持组件。
但Unreal
引擎的问题在于它作为拥有多年历史的老牌游戏引擎,这里表现出“先发劣势”
Unreal引擎积累的很多实用功能的同时也保留了很多臃肿的代码内容,
以至于·Unreal·不得不在保留了面向对象编程的前提下同时支持组件。
这就导致开发者在编写游戏逻辑的时候同时要考虑这个游戏对象是什么和有什么,以及一段逻辑应该写在组件中还是采用继承的方式这类问题。
Unreal
引擎官方的说法是蓝图blueprint
类比于Unity
的Prefab
,但熟悉二者的开发者应该都明白其中的差异。这里也不展开来讲了。
不过话说回来,Unity
中依然有将继承思想引入Component
中的编程方式,
直接编写一个Cat
猫的组件赋予Gameobject
这种也属于常规操作,
因此究竟应该如何组织代码也算是见仁见智的事情了。
而到了我最近这段时间研究的Godot
引擎上,则更为纯粹或者说激进的——整个游戏组织成一个Node Tree
节点树,
可以说godot
引擎是基于节点开发的游戏引擎。玩家可以通过继承现有节点来编写自定义节点。也可将继承自节点的脚本挂在节点上
通过继承的方式实现节点的功能扩展。为了对节点树实例化,godot
提出了scene
的概念。
我主观认为godot
的scene
概念并不准确,应该直接叫node_tree
更准确一些,因为可以有别于Unity
中的Scene
或Unreal
中level
的概念
godot
官方对于scene
的解释是类似于Unity
中的scene
和prefab
。
仔细想想Unity
中的scene
和prefab
又何尝不都是对于GameObject tree
的序列化方案呢?
虽然godot
中的node
依然和unity
中的component
有一些细微差别,但其本质上都是对于“组合优于继承”这一涉及原则的开发实践。
很难主观判断孰优孰劣。
很难主观判断孰优孰劣。
市面上已知游戏的应用
Klei Entertainment 开发的《饥荒》为了获得更好的mod制作能力,而将包括游戏脚本Script在内的多数游戏资源公布给玩家。
让我们得以窥见这款游戏的逻辑代码是如何编写的,让人惊喜的是,其使用的基于组件的开放方式。
《饥荒》使用lua作为其脚本语言,得益于lua动态类型脚本语言的特性,因此很容易实现基于组件的开发,因此我们发现除去一些AI因为逻辑相对复杂的原因而采用了状态机和行为树,以及一些通用的或者框架类的脚本,其他脚本都被抽象为component
《饥荒》中引入一个prefab 脚本 的概念,在这个脚本中完成一个 gameobject的“组装”,这个过程包括资源文件和逻辑组件。
但并不是说《饥荒》中基于组件开发的实践就是完美的,component和prefab之间以及component和component之间使用标签、事件和回调通信,
这事实上已经不能满足我们一直强调的“高内聚,低耦合”的原则,依然要在prefab中处理大量的component回调。
但我们依然可以借此,来理解我试图表达的含义。
《饥荒》的开发者们,甚至我们mod制作者(很多非专业游戏开发者的玩家)可以很容易通过组合现有component来创建新的prefab
这种思想尤其适用于《饥荒》这类核心玩法在于处理游戏世界中各类对象的游戏。
Klei Entertainment 后来推出的《缺氧》,作为一款资源整合类游戏,可以说从根本上就适合这类基于组件(甚至是基于数据)的游戏开发方式。
基于组件引发的游戏设计思考
但我这里更多还是想从我的身份——一名游戏策划(或者说游戏设计者)的角度看淡基于组件的开发方式,
因为这和后来提到的基于数据开发(ECS可以看做是一种实践方式)有着不谋而合的共同点。
在游戏设计领域,随着开放世界游戏的兴起和火爆,近年来最热的概念应该就是所谓的“涌现式设计”了
了解我的同学应该知道,我主观上很反感所谓“涌现式”这种略显“事后诸葛”的设计方式。
更大的原因是很多从业者将涌现作为其设计漏洞的遮羞布,这个我们未来有机会在讨论。
但我也在思考,在开放世界游戏中,有别于传统线性叙事游戏。这类游戏要到达的其实是一种重复可玩性。
这种重复可玩性来源于两部分:空间上的(关卡)和时间上的(叙事)
空间上的很好理解,很多roguelite游戏已经在这方面探索了很多,但开放世界游戏并不能随机地图,或者说只能在小范围内随机关卡。
随机关卡的另一部分表现就是随机敌人或者随机谜题,进而衍生出随机属性,这部分可以参考暗黑破坏神为代表的装备词条系统。
叙事上的随机近年来也有很多尝试,从早几年的《环世界RimWorld》,到最近这段时间的《漫野奇谭》我也在关注
当然,类似《十字军之王》、《太吾绘卷》这类策略游戏中也会有一些随机事件。
这类随机事件如果设计合理会大大增强玩家的游戏代入感,但如果设计不够严谨则会破坏游戏的角色扮演氛围。
这里我想说的是,作为游戏设计师,在面对诸如开放世界游戏这种如此庞大、复杂的游戏系统时。
如果依然是着眼于游戏内的某个系统,将其作为相对独立的部分,势必导致整个游戏被割裂。从而影响玩家的游戏体验。
这实际上就是在用面向对象甚至是面向过程的思路在做游戏。这种思路在面对休闲游戏、传统单局制游戏甚至MMORPG游戏时都不会产生多大问题。
但在以开放、自由度为卖点的开放世界游戏中,则会产生浪费。
那么如何以基于组件的思维来设计游戏呢?其实上文中已经给出答案。
从关注一个游戏对象是什么,变为关注一个游戏对象能做什么?
以游戏内道具为例,我们不在给道具下定义为装备、消耗品、材料或任务道具这些。
而是赋予道具能力:可装备的、可饮用的、可作为合成材料的。然后再针对每一种能力编写具体的功能逻辑。
这就从游戏的设计层面、或者说软件工程层面支持了“玩法涌现”的可能性。否则所谓”涌现式设计“只能是空谈。