在游戏中管理事件:动画更新、对象碰撞等,如果没有清晰的理解事件是如何被组织和执行的,那么这将是一项艰巨的任务。这篇精华将解释调度器如何为你的游戏框架提供组织性和灵活性。
随着电脑游戏的日益复杂,实时事件和模拟几乎在今天的游戏架构中成为标准。所需要的是一种方式来管理和执行每帧多次或在一帧的时间步内多次的事件。调度器可以以非常灵活的方式管理游戏事件,同时也促进了一个模块化的方法以便扩展性。
可以有效利用调度程序的游戏技术包括物理模拟、角色动画、碰撞检测、游戏人工智能和渲染。所有这些技术中的一个关键方面就是时间。许多模拟技术都会变得非常复杂,数百个独立的对象和进程会以不同的时间间隔进行更新。例如,物理仿真会将每个对象的时间 c 分解成小的、离散的时间间隔,以便更新对象的运动[Bourg01]。通过提供更精细的时间分辨率,模拟的精确度将大大提高。在这种情况下,许多对象和时间间隔都由相同的调度代码管理,因此在预先防止调度瓶颈时,效率是一个至关重要的问题。
调度器的另一个重要方面是其能够即时添加和移除对象的能力。这允许新的实体进入游戏并参与模拟,与游戏的其他实体一起,不错过任何节奏,然后在不再需要时将它们从调度中移除。
Scheduler Concepts
调度程序的基本组件包括任务管理器、事件管理器和时钟(见图 1.1.1)。通过这些组件,调度程序可以生成基于时间或框架的事件并执行事件处理程序。在整个 gem 中,我们将把事件处理程序称为任务。
Task Manager
每个task 都有一个标准化接口,其中包含一个供任务管理器执行的回调函数。任务管理器维护一个任务列表,以及有关缓存的调度信息,如开始时间、执行频率、持续时间、优先级和其他信息。
Event Manager
事件管理器是调度程序的核心。例如,在图 1.1.2 中,任务 1 在时间 10 和 15 定义了事件。事件管理器根据需要生成事件,以触发任务的执行。
Real Time Versus Virtual Time 实时与虚拟时间
实时调度程序的概念相当简单--事件管理器坐在一个循环中,观察实时时钟,一旦达到目标时间,就触发一个事件。在实时系统中,延迟至关重要。如果一个任务运行时间过长,可能会影响下一个任务的启动。由于每个任务都是在预定时间内完成的,因此从调度程序的角度来看,任务之间的时间基本上是浪费掉了。
从任务的角度来看,时间只是一个数字。根据这个比较数字,可以对时间进行比较,并计算出经过的时间。调度员可以通过操作这个数字来模拟给定的时间或时间的流逝,而不受实时时间的影响--小时可以在眨眼间流逝,时间也可以停止。这就是虚拟时间的基础。
虚拟时间非常有用,因为它允许调度程序在最方便的时候执行任务,而不是在实时性要求的时候。它允许快速向前运行、停止、记录和重放一系列事件。虚拟时间调度器将时间划分为帧。虚拟时间调度器将时间划分为若干帧,任务在帧与帧之间分批执行,在 "虚拟时间 "中运行,然后在渲染每一帧时与实时时间同步。如果帧频足够高,就会产生实时的错觉。然而,帧与帧之间几十毫秒的时间对计算机来说是非常宝贵的,尤其是在有效管理的情况下。通过将所有任务集中到一个区块中,剩余的时间就可以用来做其他事情(参见可扩展性)。
在本 gem 中,我们将虚拟时间称为模拟时间,因为模拟中的所有对象都将其作为参考。如果模拟时间停止,那么仿真也会暂停。当模拟时间恢复时,模拟中的对象不会检测到任何中断。模拟时间从模拟开始时的零点开始。
例如,假设每帧的长度为 20 毫秒(见图 1.1.3),如果有事件发生在 51 毫秒和 54 毫秒处,那么它们将在第 2 帧期间被处理。事件管理器在第 2 帧结束前不知道第 2 帧有多长;因此在第 3 帧开始时,它查看实时时间,发现时间为 60 毫秒。任务 1 是第一个任务,时间为 51 毫秒;但模拟时钟仍处于帧 2 的起始位置,时钟前进到 51 毫秒后,任务 1 开始执行。如果使用的是离屏缓冲区,则该帧可能已经渲染完毕,此时只是将图像复制到显示屏上)。任何未使用的时间都可用于额外的处理(请参阅可扩展性了解如何使用这些额外的处理周期)。
在这种模式下,任务执行和帧渲染总是略微落后于实时速度。但这并不会被观看者察觉,而且我们还可以利用可变帧频进行工作。如果帧频变慢,调度程序可以进行补偿,使模拟看起来以恒定的帧频运行。如果帧频固定不变,调度程序就可以预测开始和结束时间,并提前执行事件处理。但是,如果机器严重超载,调度程序就无法补偿,游戏运行速度就会变慢。
Event Type
(Frame Event)帧事件是最简单的事件类型,每N帧发生一次,或者每帧发生一次(N=1)。它们也可以在渲染事件之前或之后发生。另一方面,时间事件发生在模拟时间中,并不特定于与帧同步。例如,一个时间事件可以每10毫秒发生一次,不管渲染帧率如何。也可以将时间事件和帧事件结合起来。例如,一个事件可以被安排在每个帧开始后的10毫秒发生,或者它可以在每个帧中执行五次,这在模拟时间中是均匀分布的。
Clock
调度器的时钟组件跟踪实时时间、当前模拟时间和帧计数。时钟的精确度将决定模拟的准确性——1纳秒分辨率的时钟将比1毫秒分辨率的时钟更加精确。对于大多数目的而言,1毫秒分辨率已经足够。如果需要更高的分辨率,可以使用1毫秒的硬件时钟,并根据需要细分实时滴答声以提高分辨率。也可以使用浮点数时钟,尽管需要小心处理舍入误差。
Sequencing 序列
事件管理器处理事件的序列和生成。由于任务是由事件触发的,因此自然而然地会发生正确的排序。例如,我们定义两个任务:
- 任务1:从5毫秒到15毫秒,每5毫秒运行一次,优先级为普通。
- 任务2:从11毫秒到19毫秒,每4毫秒运行一次,优先级为高(见表1.1.1)。
在某些情况下,任务可能被设置为同时执行。在这个例子中,任务1和任务2都在时间点15执行。由于任务2的优先级高于任务1,因此它首先执行。如果优先级相同,或者没有实现优先级系统,那么它们将按照轮询(round-robin)方式处理。优先级对于排序基于帧的任务也很有用。
Task Manager Details
面对数以百计的潜在任务,任务管理器必须进行智能化管理。虽然可以有多种方法,但光盘上的示例程序使用了有序列表。任务按照其下一个执行时间存储在列表中,列表的首部总是下一个要执行的任务。事件管理器只需查看第一个任务,就能确定下一个事件的发生时间。当事件发生时,最前面的任务会从列表中 "弹出 "并执行,它的下一次执行时间会被更新,并根据更新后的执行时间重新插入列表。
除了避免冗长的搜索外,这种方法还有一个优点,那就是经常执行的任务会保持在列表前列(几乎就像缓存一样),而不经常执行的任务则会在适当的时候自动 "弹出"。
通常情况下,必须对已注册的任务进行即时修改,这可能涉及调整其优先级、周期、持续时间,甚至在任务完成前将其删除。为了更新任务,必须有某种外部手段来定位它。可以分配一个唯一的注册 ID,以便在列表中定位任务。
A Simple Scheduler
在讨论了调度程序的各种概念和组件之后,我们将演示如何构建和使用一个简单的调度程序。所提供的示例代码可以在光盘中找到。
Design
调度程序的设计取决于两个组件--调度程序引擎本身和 ITask 插件界面(见图 1.1.4)。
为了让调度程序运行,需要有人调用它。在非图形用户界面的 prograrm 中,这就像编写一个循环并执行
while(app.IsRun())
scheduler.ExecuteFrame();
将调度程序集成到消息泵图形用户界面(如 Windows)中有两种方法。第一种是通过修改消息循环来处理消息并执行调度程序。这是最明显的方法,但在调整窗口大小时会出现调度程序 "冻结 "的问题。
第二种方法是创建一个 Windows 定时器,并用它来调用调度程序。由于定时器不会被窗口拖动中断,因此调度程序可以在后台持续运行。光盘中提供了一个示例应用程序(Win),展示了如何在 Windows 应用程序中使用调度程序。让我们来仔细研究一下这个应用程序。
Windows 核心代码非常简单。调度程序通过 WM_TIMER 消息运行,有两种类型的计划事件:一种是每 15 毫秒更新一次每个小球的位置,而渲染事件则是将所有小球写入屏幕外的缓冲区。其中一个事件每 15 毫秒更新一次每个小球的位置,而渲染事件则将所有小球写入屏幕外的缓冲区,然后 Win-dows 可以根据需要从屏幕外的缓冲区绘制屏幕。本示例演示了如何使用多个模拟任务,以及如何快速添加/删除任务。
通常情况下,游戏的帧速率会随系统容量和负载的变化而变化,但移动物体的速度却保持不变。虽然两个示例应用程序的帧频都是固定的,但所提供的调度程序确实支持这种恒速技术。关键在于 ciock .Update( ) 方法,该方法会对实际实时时间进行采样,并根据所用时间推进模拟。如果一个物体在 60 毫秒内移动了 N 个单位,那么系统是渲染两个 30 毫秒帧还是三个 20 毫秒帧并不重要--物体在相同的实时时间内移动了相同的距离,因此速度是恒定的。如果想让模拟对象的速度随帧速率增大或减小,请更改 clock.Update( ) 方法,使其按固定间隔前进,而不是读取实时时钟。
那么,调度程序到底是如何管理时间的呢?我们需要注册一些事件,看看它是如何工作的。
Scheduling
调度任务的第一步是将其指定为时间事件、帧事件或 渲染事件。这段代码将调度一个帧事件,从第 200 帧开始,每隔三帧运行一次,并在第 210 帧之前结束(开始时间 200 + 持续时间 10):
scheduler.Schdule(TASK_FRAME, 200, 3, 10,
pSomeHandler, pUserPointer, &id);
该任务将在帧 200、203、206 和 209 上运行。执行最终迭代后,该任务将过期并被任务管理器删除。持续时间为 o 的任务是永久任务,永远不会过期。在某些情况下,你可能想在任务完成前将其删除,或者想手动结束一个永久任务。为此,可以根据 任务ID终止他 。Terminate(taskID)
每次调用调度程序 ExecuteFrame () 方法时,首先会调用 c1ock.BeginFrame ( ) ,通过更新帧计数和计算新的帧开始和结束时间来启动新的帧。 更新帧计数后,执行所有时间事件,将模拟时间提前到帧结束时间,执行所有帧事件,最后执行渲染任务(在示例调度程序中,渲染任务是一个特殊的帧任务,没有起始时间、周期或持续时间--它总是每帧执行一次)。
可以使用 Run( ) 和 stop() 方法停止或重新启动整个模拟。当调度程序停止并重新启动时,它会计算已过时间并从总模拟时间中减去。停止时,调度程序仍会执行渲染和帧事件,但时间事件会暂停。
可以使用 Run( )和 stop( )方法停止或重新启动整个模拟。当调度程序停止并重新启动时,它会计算已过时间并从总模拟时间中减去。停止时,调度程序仍会执行渲染和帧事件,但时间事件会暂停。
Advanced Concepts
有许多方法可以改进或更好地利用调度程序:可扩展性、模拟和多线程就是其中的几种方法。
游戏应该充分利用所有可用的处理能力,以提供更丰富的体验,但它仍然需要在性能较弱的系统上运行良好。计算成本高的功能需要被节流或完全关闭。游戏还应该在多任务环境中 "运行良好"--占用大部分 CPU 的游戏可能会妨碍操作系统及时响应用户输入。此外,如果操作系统在后台启动任何内务管理任务,也会减慢游戏的运行速度。
收集性能数据是成功的一半。由于调度程序会处理所有进程,从而成为瓶颈,因此它是放置性能监控器的理想场所。
如前所述,时钟会获取当前时间的快照,并将其与上一帧进行比较,以确定已用时间。调度程序还可以通过比较开始时间和结束时间来确定每个任务的已用时间。这一信息既可传达给任务,也可用于决定是否运行任务和/或运行的频率。
提供可扩展性的一种方法是要求时间预算--功率越大,允许的时间越多。调度程序会跟踪累计预算和累计运行时间,只有在当前预算超过实际时间时才会执行任务。例如,一个任务每帧的时间预算可能是 2 毫秒,但在慢速 CPU 上的运行时间是 3 毫秒。调度程序会每隔三帧跳转一次,以便将任务控制在预算范围内(见表 1.1.2)。
一些任务可能有时间预算 "阈值"--如果超过预算,它们就不会运行,而是部分时间不运行。调度程序还可以通过汇总所有任务来确定执行整个模拟所需的时间。调度程序可以添加任务来填补空闲时间,或移除任务来减少负载(当然,必须有一些规定来指定哪些任务可以安全地添加或移除)。通过向任务提供整体系统使用统计信息,任务可以根据接收到的数据自行调整使用时间的长短。最好将空闲目标设定为 5%-10%,以便在实际处理时间上出现微小波动时也不会减慢速度。
提供可扩展性的其他选项包括增加或减少时间预算、安排空闲任务(只在空闲时间运行)、垃圾收集或其他家务劳动、图形增强或人工智能改进。在进行此类管理时,重要的是要避免在两个极端之间摇摆不定,具体方法是将调整限制为小幅增量变化,而不是大幅跳跃,或者对之前调整的效果进行统计分析,以提高预测能力。
Simulation
调度程序可用于驱动仿真系统。本 gem 中描述的调度程序非常适合这类仿真系统。
在月球着陆器的一个简单例子中,着陆器有垂直速度和前进速度。每个时间步都会增加重力。如果我们使用人工智能来控制着陆器的垂直推进器,那么在每个时间步中,人工智能都会对速度进行采样并调整推力以进行补偿,从而实现可控的下降。时间步必须足够小,以便让人工智能有足够的时间做出反应,否则着陆器会在做出有效反应之前撞上地面。对于碰撞检测,同样需要较小的时间步长,以便着陆器与地面相交,而不是完全穿过地面。
Multithreading
调度程序可以管理子线程的执行[Carter01, Daw-son01]。例如,某些任务作为一个连续的进程,而不是一系列离散的事件,可能效果更好[Otaegui01]。这样的任务可以写成一个线程,调度程序可以控制允许线程处理多少时间。
多处理器系统正慢慢变得越来越普遍,在不久的将来,多处理器很可能会成为一种标准功能。能够利用多处理器的游戏将能够超越为单个 CPU 编写的游戏。
多 CPU 调度器可以同时激活多个线程,使它们可以同时运行。它还可以在特定线程中生成事件处理程序,以便同时处理多个事件。
Conclusion
使用调度程序有多种原因--便携性、灵活性和支持模拟。高质量的调度程序必须灵活高效。本手册介绍了调度程序的一些基本概念,提供了一个调度程序示例,并展示了如何将其集成到传统应用程序和基于图形用户界面的应用程序中。在您的下一款游戏中使用调度程序,让您的活动更有条理。
References
[Bourg01] Bourg, David M.,Poysics for Game Developers, O'Reilly, 2001.
[Cartero1] Carter,Simon, "Managing Al with Micro-Threads,”Game Programming
[Dawson01] Dawson,Bruce“Micro-Threads for Game Object AI,”Game Programming Gems 2,Charles River Media,Inc.,2001.
[Llopis0i] Llopis,Noel,"Progranming with Abstract Interfaces,”Game Programming Gems 2,Charles River Media, Inc., 2001.
[Mirtichoo]Mirtich, Brian."Timewarp Rigid Body Simulation,”Computer Graphics Proceedings, SIGGRAPH 2000: pp.193-200.
[Oraeguio1] Otaegui,Javier,“Linear frogramming Model for Windows-Based Games,”Game Programming Gems 2,Charles River Media, Inc.,2001.