Posted on 

游戏编程模式-序列模式

最近在学

游戏有一个独有的苛刻的要求——有趣,玩家需要的是既新奇又有平衡性的的体验。
所以游戏的迭代更新是很重要的?如果一个游戏要持续吸引玩家,要持久的发展的话。
但是光是更新迭代发布版本,而不对游戏做全面的测试检查的话,是一种很不负责的行为。

双缓冲模式

用序列的操作模拟瞬间或者同时发生的事情。

定义缓冲类封装了缓冲区:一块能被修改的状态区域。
这个缓冲被增量地修改,但我们希望外部的代码对缓冲区的修改都视为单一的原子操作。
为了实现这点,类保存并维护两个缓冲的实例:下一缓冲当前缓冲

从缓冲区中读取信息,总是读取当前的缓冲区。 往缓存区写入信息,总是在下一缓冲区上经进行。
当改变完成后,一个交换操作会立刻将当前缓冲区和下一缓冲区交换,旧的缓冲区成为下一个重用的缓冲区。

使用场景

  • 我们需要维护一些被增量修改的状态。
  • 在修改到一半的时候,状态可能会被外部请求。
  • 我们想要防止请求状态的外部代码知道内部的工作方式。
  • 我们想要读取状态,而且不想等着修改完成。

使用须知

不像其他较大的架构模式,双缓冲模式位于底层。
它对代码库的其他部分影响较小——大多数游戏甚至不会感到有区别。

  • 交换本身需要时间
    交换操作必须是原子性的:在交换时,没有代码可以接触到任何一个状态。
    通常交换过程跟修改指针一样快,如果交换消耗时间长于修改状态时间那就很坏了。
  • 我们得保存两个缓冲区
    这个模式的另一个结果是增加了内存的使用,需要你在内存中一直保留两个状态的拷贝。
    在内存受限的硬件设备上,是个苛刻的要求,要付出惨痛的代价。
    如无法分配出两份内存,就需要保证状态在修改时不会被请求访问。

需要设计决策的地方

  • 缓冲区如何被交换?
    • 交换缓冲区的指针或者引用
      速度快,但外部代码不能存储对缓存的永久指针,
      缓冲区中的数据是两帧之前的数据,而不是上一帧的数据。
    • 在缓冲区之间拷贝数据
      交换也许更花时间,
      下一帧的数据和之前的数据相差一帧。
  • 缓冲的粒度如何?
    • 如果缓存是一整块
      交换操作更简单。由于只有一对缓存,一个简单的交换就完成了。
      如果可以改变指针来交换,那么不必在意缓冲区大小,只需几步操作就可以交换整个缓冲区。
    • 如果很多对象都持有一块数据
      交换操作更慢。 为了交换,需要遍历整个对象集合,通知每个对象交换。
      如果不需要缓存的状态,可以使用将“当前”和“下一个”指针概念,将其改为对象相关的相对偏移量。

游戏循环模式

将游戏的进行和玩家的输入解耦,和处理器速度解耦。
是“游戏编程模式”的精髓。 几乎每个游戏都有,而在非游戏的程序几乎没有。

关键

  • 处理用户输入,但是不等待它,游戏循环在运转、渲染
  • 让游戏在一个与硬件无关的速度变量下运行

帧率(FPS)

用实际时间来测算游戏循环运行的速度,就是游戏的“帧率”(FPS)。
如果游戏循环的更快,FPS就更高,游戏运行得更流畅更快。
可以理解成是图形处理器每秒更新的次数,例如60FPS,也就是每一秒更新60次,即0.016s更新一次。

决定帧率的两个因素

  • 一个是每帧要做多少工作
  • 底层平台的速度

定时步长

  • 原理:以固定时间间隔(如 60FPS 的 16.6ms)更新游戏逻辑,渲染帧率可以变化
  • 优点
    • 物理模拟稳定:适合需要精确物理计算的游戏,避免因帧率波动导致数值不稳定(如穿墙、速度异常)
    • 确定性:相同输入下行为一致,便于调试和网络同步(如多人游戏)
    • 逻辑与渲染分离:游戏逻辑不受渲染帧率影响
  • 缺点
    • 性能浪费或卡顿:若逻辑更新耗时超过步长时间,会导致帧率下降或需要跳帧
    • 不匹配高帧率设备:固定步长可能无法充分利用高刷新率显示器

变时步长

  • 原理:每帧根据实际耗时(ΔT)更新逻辑,渲染和逻辑步长同步变化。
  • 优点
    • 流畅渲染:充分利用硬件性能,适应不同帧率设备(如高刷显示器)
    • 资源高效:避免不必要的逻辑更新,适合性能波动大的平台(如移动端)
  • 缺点
    • 物理不稳定ΔT 过大会导致碰撞检测失效或数值爆炸
    • 非确定性:相同输入可能因帧率不同产生不同结果,难以调试和同步
    • 依赖帧率:逻辑代码处理需要 ΔT

使用情景

  • 库:自己把握游戏循环,并在其中调用库函数
  • 游戏引擎:引擎掌握游戏循环并调用你的代码

需要设计决策的地方

  • 如何管理能量消耗?

    需要考虑的不仅仅是让游戏看上去很棒,同时尽可能少地使用CPU。
    需要设置一个性能的上限:完成一帧之内所需的工作后,让CPU休眠。

    • 尽可能快地运行
      游戏循环永远不会显式告诉系统休眠,相反,空闲的循环被划在提升FPS或者图像显示效果上了。
      这会给玩家最好的游戏体验,但也会尽可能多地使用电量。

    • 固定帧率

      比如移动游戏更加注意游戏的体验质量,而不是最大化图像画质。 很多这种游戏都会设置最大帧率。
      如果游戏循环在分配的时间片消耗完之前完成,剩余的时间它会休眠。
      这给了玩家“足够好的”游戏体验,也让电池轻松了一点。

  • 如何控制游戏速度?

    • 固定时间步长,没有同步
      简单,游戏速度直接受到硬件和游戏复杂度影响。
    • 固定时间步长,有同步
      较简单,可能总需要做一些同步,游戏不会运行得太快,但可能运行的太慢。
    • 动态时间步长
      不建议,虽然能适应并调整,但是让游戏不确定而且不稳定。
    • 固定更新时间步长,动态渲染
      最有适应性,能适应并调整,避免运行得太快或者太慢,也更复杂,需要在实现中写更多东西。

更新方法模式

游戏世界管理对象集合。
每个对象实现一个更新方法模拟对象在一帧内的行为。每一帧,游戏循环更新集合中的每一个对象。
因此每个游戏实体应该封装它自己的行为,使得游戏循环保持整洁,便于增加和移除实体。

适用场景

  • 游戏中含有一系列对象或系统需要同步运转
  • 各个对象之间的行为基本相互独立
  • 对象行为与时间相关

使用须知

  • 将代码划分到一帧帧中会让它变得更加复杂
  • 需要在每帧结束时存储游戏状态来让下一帧继续
  • 所有对象逐帧模拟,但并非真的同步
  • 在更新期间修改对象列表必须谨慎

需要设计决策的地方

  • 将更新方法 update() 放在哪个类中?

    • 实体类
    • 组件类:更新方法模式和组件模式享有同样的功能——让实体/组件独立更新自己。
    • 委托类:比如状态模式通过改变它委托的对象来改变行为,类型对象模式可以在同类实体间共享行为。
  • 如何处理隐藏的对象?

    指的是一些暂时不需要更新的对象,比如被禁用的、在屏幕外的、或者未解锁的对象。

    • 使用单个存储所有对象的集合
      处理不活跃对象会浪费时间,要么检查其“是否启用”的标识,或调用空方法。
    • 两个集合,一个单独维护活跃对象,另一个维护全部实体
      使用了额外的内存管理第二个集合,当需要所有实体时,这个单独活跃对象的集合是多余的。
      但在游戏对速度比内存要求更高时是值得的。
    • 两个集合,一个活跃对象集合,另一个只包含不活跃实体
      得保持集合同步。当对象创建或完全销毁时(不是暂时停用),你得修改全部对象集合和活跃对象集合。

    方法选择的度量标准是不活跃对象的可能数量。
    数量越多就尽可能用分离的集合,避免在核心游戏循环中用到它们。

一些需要注意的

  • 更新方法模式,游戏循环模式和组件模式,是构建游戏引擎核心的三位一体。
  • 优先使用“对象组合”,而非“类继承”
    • 继承难以维护,没人可以不拆解它们来管理庞杂的对象层次。
    • 可以适度使用也不必完全禁止使用,执着于避免使用和执着于使用一样糟糕。


Copyright © 2022 - 2023 BlindArbiter

Powered by Hexo | Theme - Stellar