【游戏性基础系统】
游戏性基础系统(gameplay foundation system):
1)运行时游戏对象模型(runtime game object model)
2)关卡管理及串流(level management and streaming)
3)更新实时对象模型(real-time object model updating)
4)消息及事件处理(messaging and event handling)
5)脚本 (scripting)
6)目标及游戏流程管理(objectives and game flow management)
其中1)运行时对象模型是最复杂的,通常它需要提供以下功能:
1)动态地产生(spawn)及消灭(destroy)游戏对象。
2)联系底层引擎系统
3)实时模拟对象行为
4)定义新游戏对象类型
5)唯一的对象标识符(unique object id)
6)游戏对象查询(query):根据ID查找对象,或是根据任意条件做高级查找,如寻找玩家20m以内的所有敌人
7)游戏对象引用(reference)
8)有限状态机(finite state machine, FSM)
9)网络复制(network replication)
10)存档及载入游戏、对象持久性(object persistence)
【运行时对象模型架构】
游戏对象模型的2种架构:
1)以对象为中心(object-centric):每个对象含一组属性及行为,这些都会封装在那些对象实例的类之中。游戏世界只不过是游戏对象的集合。容易产生单一庞大的类层次结构(monolithic class hierarchy)。一个类越是在类层次结构中越深的地方,就越难以理解、维护及修改。因为要理解一个类,就需要理解其所有父类。
此方式最大的问题是,当设计层次选择了某个标准,就很难甚至不可能用另一个完全不同的标准分类。比如按物种分类的生物,要按颜色分类就不好办。mix-in类可以改善改方式。一个类可以派生自主要继承层次结构中的一个且仅一个类,但也可以继承任意数量的mix-in类(无基类的独立类)。通常更好的做法是合成(composition)或聚合(aggregation),而不是继承他们。
把Window派生自Rectangle类。然而,一个视窗并不是一个长方形,它只拥有一具长方形,用于定义其边界。因此,这个设计问题的更好解决方法是把Rectangle的实例安置于Window类之中,或是令Window拥有一个Rectangle的指针或参考。面向对象设计中,“有一个”的关系称为合成(composition),合成的对象与主对象有同样的生命周期。对于主对象与子对象生命周期不同步的设计,称为聚合(aggregation)。
要降低游戏类层次结构的宽度、深度、复杂度,一个十分有用的方法是把“是一个”关系改为“有一个”关系。业界成熟的方式是使用组件模式:
1)“枢纽”HUB组件模式
2)通用组件模式
3)纯组件模式
2)以属性中心(property-centric):每个游戏对象仅以唯一标识符表示。我们先定义游戏对象可能含有的属性集合,然后为每个属性建表。此设计更能有效地使用内存,因为我们只需存储实际上用到的属性。SoA(struct of array)的性能优于AoS(array of struct)
【世界组块的数据格式】
1、二进制对象映像,把每个对象的二进制映像(binary image)写入文件,对于指针和虚函数表需要特殊处理。
2、序列化(serialization),XML解析之慢众所周知。
3、生成器(spawner)是游戏对象的轻量、仅含数据的表示方式,可用于运行时实例化及初始化游戏对象。
【游戏世界的加载和串流】
游戏加载系统主要有2个功能:
1)人磁盘加载游戏世界组件及其它用到的资产至内存中。
2)管理这些资源的内存分配及释放。
加载包括:
1)简单的关卡加载,玩家需要等待关卡载入,期间会显示静态或简单动画的二维加载画面。
2)串流加载,即预加载
3)关卡加载区域,区域之间可能会有重叠,每个区域配有一个表,列出玩家位于该区域时内存应该包含的世界组块。
内存管理采用池分配器,为每种类型对象生成一个池分配器,避免内存碎片。
游戏存档分为:
1)存储点存档,这法较为简单,因为可以避免对大量世界数据的记忆
2)任何地方皆可存档
【对象引用与世界查询】
几个实现对象引用的方式:
1)指针
2)智能指针
3)句柄。句柄会有引用过时对象的问题,解决此问题的方案之一是,在每个句柄中加入唯一的对象标识符。
游戏对象查询可以提供几下以种能力:
1)以唯一标识符搜寻游戏对象
2)对合乎某条件的所有对象进行迭代
3)搜寻射线路线以及范围内物体
【实时更新游戏对象】
“差一帧”问题引起的状态不一致是bug的主要来源。相比游戏对象模型,低阶引擎子系统才是性能关键。
【事件与消息泵】
1、为每种事件定义一个虚函数,如OnExplosion()。这种方式的坏处在于,所有的对象都必须实现OnExplosion,即使是不响应此函数的对象也要实现。
2、把事件封装成event,基类event提供类型,子类event提供具体内容,通过统一的OnEvent(Event *evt)来传递。
关于事件的类型,有2种方法:
1)enum枚举,此法有以下几个缺点,首先,所有的类型聚合在一起,破坏封装。其二,类型硬编码,其三,硬编码被改变后,对于存储于磁盘上的内容无法修改。
2)使用字符串,缺点是占用内存点,消耗大。
关于事件的参数,有以下几种方法:
1)统一的variant集合
2)键值对,此法可以解决variant的次序问题。
事件的参数必须深拷贝出去,采用池分配可以有效减少内存碎片。但是采用event模式,会加大调度困难,因为无法追溯事件的发送者。
3、事件处理器,即OnEvent的实现可以用switch来实现。另外,在设计上可以采取职责链模式。
4、使用事件注册机制可以减少需要响应事件在范围。
5、即时消息发送可能会导致栈开销过大,层次过深。
6、事件响应的逻辑可以通过世界编辑器开放给策划,由策划来完成。
7、事件机制可以提供排队的功能,以及延时响应功能。
【脚本】
游戏脚本语言通常有2种:
1)数据定义语言(data-definition language)
2)运行时脚本语言(runtime scripting language)
对于大多数工作室而言,更合适的方未能是选择一个知名且成熟的脚本语言。游戏引擎必须要能执行脚本代码,脚本代码也需要发志引擎中的操作。运行时脚本语言的虚拟机通常是嵌入游戏引擎中的。引擎启动虚拟机,需要时执行脚本代码,并管理脚本的执行情况。
脚本需要和游戏对象互动,其中一个方法是在脚本中以不透明的数值类型句柄(handle)来引用对象。