游戏引擎架构笔记

各种运行时对象模型架构

以对象为中心
工具方游戏对象,运行时以单个类实例或数个相连的实例表示。每个对象含一组属性及行为。游戏世界是游戏对象的集合

深层次的类难以理解、维护、修改。需要理解全部父类
无法表达多维的分类
多重继承 (水陆两栖载具)
mix-in类,一个类可有任意数量 父类但只能有一个祖父类。更好的做法是合成、聚合,而非继承
冒泡效应:根类通常非常简单,然而当游戏加入更多的功能。可能尝试共享两个/多个无关类的代码,使其上移

把是一个改为有一个,使用组件负责单一、定义清除的服务。合成关系:A类必须有B类,即创建A时自动创建B,销毁同理。聚合关系:A类并不管理另一个类的生命周期

以属性为中心
每个工具方游戏对象仅以唯一标识符表示(整数、字符串散列标识符或字符串)。每个对象属性分布于多个数据表。每种属性类型对应一个表,以对象标识符为key(而非集中在单个类实例)。属性本身是实现为硬编码的类之实例,游戏对象的行为则隐含地由组成的属性集合定义。如,若某对象含血量属性,则对象能被攻击、扣血,并死亡。若对象含网格实例属性,则能在三维中渲染为三角形网格的实例

行为实现:
通过属性类实现行为。每种属性可以实现为属性类,通过成员函数来产生行为。如游戏有health属性,对象就能受损、被杀。对应任何攻击,health对象能适当 扣血作回应。属性对象也可于游戏对象的其他属性对象交流。
通过脚本实现行为。把属性值以原始方式存于一个或多个数据库的表里。用脚本实现行为。每个游戏对象可能有一个scriptId的特殊属性,用来指定管理对象行为的脚本函数(若脚本支持面向对象则指脚本对象)

属性于组件对比,都是单个逻辑对象由多个子对象组成。区别于属性里子对象定义游戏对象本身的某个属性,而组件里子对象表示某底层引擎子系统。最后都是相同的结果:由一组子对象合成逻辑游戏对象,并从子对象中获取所需行为。

世界组块的数据格式

世界组块通常包含静态和动态元素。
静态几何体可能使用一个巨型三角形网格或是许多教细小网格。一个网格可生成多个示例。融入一道门的网格重复用于组块所有门。
静态数据包含碰撞信息(三角形,凸形状集及其他简单几何),体积区域(用来侦测事件或 勾画不同地域),人工智能导航网格(一组线段)
动态数据包含组块内游戏对象的某种表示形式。对象属性的初始值和对象类型的某种规格(面向对象可能是字符串,位移标识符,面向数据可能是对象属性集合或显示储存的类型)

对象存储格式:
二进制对象映像
c++类储存为二进制映像需对指针和虚表特殊处理,也可能为字节序问题交换示例数据。难以对内容修改,通常不是最佳选择。虽然二进制更适合稳定数据结构,如网格数据或碰撞几何
游戏对象描述的序列化
将对象产生一个数据流,包含足够细节。便于重建原本对象时导入数据流,来初始化新对象的内部状态。
通常实现为:在基类加以一队虚函数,SerializeOut()和SerializeIn(),每个派生类实现。或者实现一个c++类的反射系统。然后开发一个通用系统自动序列化任何包含反射的c++对象

反射数据描述了类在运行时的内容。存储信息包括类名、类的数据成员、每个数据成员的类型、每个成员位于对象内存映像的偏移。也包含类所有成员函数信息。

游戏世界的加载和串流

简单的关卡加载
仅允许游戏每次加载一个游戏世界组块,过关时需等待关卡载入。使用堆栈分配器管理内存
在这里插入图片描述

往无缝加载进发:阻隔室
把游戏世界内存切割成两个同等大小的块。玩家进行关卡A时,在另一线程加载关卡B至第二块内存
或者切割成两个不同大小的块。大的块储存完整世界,小块称为阻隔室。阻隔室会有门闸或其他障碍物,防止看到或返回之前的完整组块。此时卸载之前的完整组块,并加载下一个完整组块。在阻隔室可以走过一条通道或做些任务
实现的关键是异步文件I/O

游戏世界的串流
无缝世界的技术称为串流。串流的两个目标:1.在玩家参与正常游戏性任务时加载数据 2.使玩家在游戏过程中不断加载卸载数据也不会导致内存碎片

把游戏中每个游戏资产(游戏世界组块、前景网格、纹理、动画等)切割成大小相同的数据块。使用一个块为单位,基于内存池的分配系统,按需加载及卸载资源数据
在这里插入图片描述
每个关卡加载区域(一个简单的凸体积,区域间可能有重叠部分)对应一个组块列表请求。任意时刻玩家位于一个或多个区域。求出区域组块列表的并集,并让关卡加载系统定期检查主控列表,决定内存中应有的世界组块集合

对象生成的内存管理
离线内存分配
完全禁止在游戏途中动态分配内存,当载入组块后立即生成所有动态游戏对象,不再创建和销毁游戏对象。以避免内存分配速度和碎片问题。因为世界组块所有游戏对象内存需求是先验得知且有界,所以可以用世界编辑器离线分配并置于世界组块的数据中。通过可视模拟生成,通过循环使用对象模拟无限。但是此法会严重限制策划

动态内存管理
为每个对象类型设内存池:如果每个游戏对象类型实例能保证占用相同内存量。即设计不同的池分配器,让相同大小的对象共享一个。但需估算每个对象有多少实例,若池过大,浪费内存,若池过小,则无法在运行时满足所有生成需求
小块内存分配器:在上述方法基础上,允许游戏对象使用元素大小大于对象大小的池,可减少内存池数量,但每个池会浪费些内存
内存重定位:要移动现场已分配对象。因此需非常小心地更改指向被移动内存块的指针

对象引用与世界查询

指针,智能指针,句柄

实时更新游戏对象

简单但不可行的方法
在这里插入图片描述
游戏对象里更新子系统,游戏循环只需要更新游戏对象

多少低阶引擎子系统有严峻的性能限制。因此改为批次式更新游戏对象
在这里插入图片描述
游戏循环改为
在这里插入图片描述
批次式更新的效益:最高缓存一致性,最少重复运算,减少资源分配(子系统要管理内存/其他资源),高效的流水线(子系统执行近似计算)

对象子系统会相互依赖,因此也不能简单更新逐个对象。例如动画系统产生局部空间骨骼姿势,关节转换会变换到世界空间。物理系统把他们设定为一个类似的刚体,随时间模拟刚体,模拟结果设置最后的骨骼关节。最后动画计算最终世界空间姿势及蒙皮矩阵

分阶段更新 维护多个对象链表,每个链表代表一个更新阶段,在1帧的不同阶段更新

桶式更新
当存在对象间依赖时要调整阶段式更新。对象间的依赖可想象为多个依赖树。没有父的游戏对象视为根,依赖根的游戏对象为第1阶度子节点。
把对象编成独立群组,第一个桶全为根对象,第2个同全为第1阶度子节点对象。每次更新一个桶的对象
在这里插入图片描述

对象状态及差1帧延迟
所有游戏对象在更新循环前、后是一致的,但更新途中可能不一致

因此如果一个对象的更新需要查询其他对象的状态。必须注意清楚需要的是其他对象之前状态,还是更新状态。否则可能导致对象的状态延后其他对象1帧

解决办法之一是桶更新。限制了游戏对象被能查询对象的范围。若希望查询已更新状态的对象,则该对象一定在已更新的桶里,若希望查询之前状态的对象,则对象一定在之后更新的桶里。并且不可查询同一桶的对象状态

另一个办法是状态缓存。更新时,不覆写新状态到原来的矢量,而是写到另一个矢量。这样任何对象可以安全查询其他对象之前的状态。并保证更新过程中永远有一个完全一致的状态。同时可以通过线性插值前后两个状态,得出该时段任何时刻的近似状态。

还有一个简单办法是为对象加上时间戳,分辨对象状态是当前还是现在。任何查询其他对象的代码,要断言明确地检查对方的时间戳

事件与消息泵

把事件封装成对象
事件由两部分组成,类型和参数
在这里插入图片描述
事件类型
唯一整数或字符串
事件参数
可以为每种事件类型从Event基类派生一个独立的类。使用硬编码将事件参数作为类的数据成员
在这里插入图片描述
另一种方法是将参数存储为variant集合。variant通常存储当前数据类型和数据本身
在这里插入图片描述
以键值对为参数:可避免依赖次序的问题
事件处理器
通常为一个原生虚函数或脚本函数(如onEvent),通过switch或if/else-if处理所有类型事件。
另一种是实现一系列处理器函数,每个负责一种事件
职责链
把游戏类型间的关系想象为一个关系图
在这里插入图片描述
在这里插入图片描述
登记对事件的关注
每个事件类型维护一个链表,内含关注该事件类型的对象。或者每个游戏对象维护一个位数组,每一位代表对象是否关注某种事件

事件排队的好处

  1. 使用事件队列延后处理事件,可以控制事件处理的时机。确保在游戏循环哪个时机处理
  2. 发出一个事件时,发送者可以设置传递事件。希望往未来投递事件(如一个闹钟,一些周期性任务)。每个事件进入队列前需指定送达事件。队列中事件按送达事件排序。每帧先检查首个事件送达事件,若未满足,则终止处理
    在这里插入图片描述
  3. 设置事件的优先次序

事件排队的问题

  1. 增加事件系统复杂度
  2. 深度复制事件及参数。即时处理事件,参数只需在事件处理函数中持续。可以存于调用堆栈在这里插入图片描述
    事件排队,需要深度复制整个事件对象至队列在这里插入图片描述
  3. 深度复制事件对象需要动态内存分配。除了潜在成本,还会造成空间碎片
  4. 调用堆栈无法告知事件从何而来,无法检查发送者的状态。调试更困难。排队事件可能导致一些竞态条件bug(如更新动画时,检查旧动画播完,抛出一个事件,令处理器播放新动画)在这里插入图片描述
    即时传递事件的问题
    可能导致非常深的调用堆栈。如A向B传递一个事件,B事件处理器又发出另一个事件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值