第二章 游戏体系结构

本文探讨了游戏作为一种实时应用软件的特性,解释了实时软件的概念,强调了游戏的交互性和时间限制。文章指出,游戏由更新(Update)和绘制(Render)两部分组成,需要确保在不同硬件上同步运行。为了实现这一目标,提出了同步更新和绘制的方法,包括使用线程、定时器和固定循环。游戏逻辑部分包括玩家更新、世界更新和非玩家角色更新,涉及复杂的AI决策和限制处理。游戏表现部分则关注图像和声音的处理,以及游戏的多媒体效果。文章还讨论了游戏开发的前期、生产和维护阶段,强调了每个阶段的目标和挑战。
摘要由CSDN通过智能技术生成

第一节   实时软件

计算机游戏是一种应用软件,它是一种实时应用软件。这是我们必须要清楚的,因为这样分类能帮助我们理解游戏程序是怎样运行和编码的。为了还对这些概念还不时很清晰的读者,我下面停顿一下,简短地解释一下它们。

正式定义上,实时软件的意思是有时间界限的计算机应用软件,或者更一般说,应用软件的数据响应必须在给定的一个时间段内。例如,在机场大显示屏幕显示乘客信息的计算机程序:用多行文本显示飞机数量、状态、降落时间等。明显地,软件反映的是实时信息——如飞机晚点时,广播延时等。像这样的信息是不可预知的,应用软件必须作出处理和正确的反应。此外,这些因时而定的信息必须以视觉的方式将数据呈现在屏幕上。上述就是实时软件相关解释。

现在,我们考虑一个比较棘手的例子问题——一个设计用来协助空中交通管制的应用软件。如图:

 

 

软件通过雷达来获得空中信息,在屏幕上显示飞机以及它们的轨道,使地面人员能引导飞行员及时、安全地到达目的地。我们可以看到,软件由下面几部分组成:

     一个数据获得模块——本例中,应当加上雷达。

     一个显示/计算模块,帮助地面人员了解数据。

     发送信号引导飞机的交互模块。

这里,我们比上例更进一步,假设我们正在看的是一个实时交互应用程序,它能反应任何时间点的事件并显示相关信息,还允许操作人员处理这些事件。这种模型正和游戏相差无几。想象一下,我们除去雷达,用软件模拟出空中交通,并告诉使用者:他必须让飞机安全着陆。最后增加一个记分牌和结束画面,这正不成了游戏吗?

所有的游戏都是交互式的实时应用软件,操作人员(以后我们称为玩家)和用软件模仿的实时行为相交互。敌人追赶我们,电梯上下运动,来回射击,都是游戏里的虚拟实时成分。

但是在游戏里,你可能有足够的时间来思考。游戏同样也是时间限制;它们必须以每秒高于25帧的速度显示。这就限制了游戏的实时模拟器和表现层。我们不可以使用超过机器硬件分给我们的时间片。然而,游戏就像是魔术。它好像让看上去不可能发生的事情发生了。技巧超乎玩家意料地让多媒体效果超出硬件呈现限制。

概括说来,游戏是感知实时数据的虚拟模拟器组成的实时交互应用软件。它让显现模块显示游戏,用控制机制让玩家和游戏交互。

因为极高的交互频率,游戏所能模拟的东西受到了限制。但是游戏编程将挑战这种限制并且在表现和模拟上超越平台提供力。这就使游戏编程的关键和本书的主题。

第一部分,“Gameplay Programming”,讲述实现游戏世界的实时模拟器的编写。

第二部分,“Engine Programming”,覆盖的是表现层。

 

实时循环(Real-Time Loops

和以前提及的一样,所有实时交互软件的三个组成部分并发地运行。首先,场景的状态必须不断地产生(如雷达对空间飞机的感知和游戏中模拟场景的更新)。第二,操作人员能和它交互。第三,模拟场景和玩家输入所应该引起的结果必须呈现给玩家。最后,玩家只不过是游戏实体的一部分。为了便于理解,我将根据这条规则把游戏当作由更新程序Uptate”和绘制程序“Render”组成的应用程序。

当我们用具体游戏代码实现这两个程序的时候,问题随之而来了。我们怎么才能保证它们能同时地运行,用一个小小的窗口来反映现实世界呢?在理想状态,这两个程序将运行在由许多并行处理器组成的超级机器上。这样,两个程序都能无限制的使用硬件资源。但是,实时技术消除了很多局限性。现实中的许多计算机只由有限的内存和一个CPU组成。诚然,CPU不能在任何时间同时执行两个任务。所以,人们不得不想尽各种办法来解决。

起初方法是执行在一个循环中执行它们。如图:

所以,每一个“Uptate”后面都跟着一个“Render”调用,这就保证两者都同样重要。用这种方法,逻辑和表现大幅度增加了。但是如果复杂场景中敏感变化所引起的帧率改变时会发生什么呢?想象一下,如果复杂场景中10%的变化而导致引擎变慢,明显,逻辑圈循环的数量也会因此改变的。甚至更糟的是,如果一场游戏中,运行速度快的机器为第五个因素能赢过旧的机器作,那将会发生什么呢?难道AI在这些较小的机器上运行得慢吗?明显,使用第一种方法引起了一些有关游戏因执行变化而受影响的有趣问题。

为了解决这些问题,我们必须分析每一个代码的组成。一般来讲,“Render”部分必须尽量在硬件上执行。较新较快的机器能提供更流畅,更好的帧率等。但是场景内容不能被机器速度的提高所影响。实体也必须按照游戏设计的速度来变化,否则,游戏只能被扔掉。想象一下,如果你购买了一个足球游戏,因为硬件原因,游戏要么太快要么太慢。但是,让“Render”和“Update”同步的话使编码复杂化了,因为“Update”有固定的频率,而“Render”没有。

解决这个问题的一个办法是同步保持“update” 和“render”,但是根据连续调用的共用时间改变“update”例程的间隔。我们计算出实时单元的共用时间,“update”部分就能用它来决定事件的速度,因此来保证事件发生在合适的时候,且与硬件无关。无疑,“update”和“Render”会在一个循环里面。但是“update”部分依赖硬件的速度-硬件速度越快,每次调用完成得越好尽管在特殊的情况下,这是一个有效的办法,但是一般来说它是没有价值的。随着速度和频率的升高,提高“update”的速度已经没有意义。“character AI”真的需要每秒搜索50次吗?决策系统是一个复杂的过程,如果执行超过需要的话,那简直是浪费时钟周期。

另外一个解决同步的办法是用一个双线程方法:一个线程执行“Render”部分,另外一个线程负责“Upate”。通过控制每个线程的调用频率,我们确保“Render”部分尽可能持续的被调用和“Uptate”的硬件无关性。大多数游戏中,AI每秒执行10-20次已经够多了。

如果一个动作游戏以60 fps的速度运行,而AI在另外一个线程中以15 fps的速度运行。明显,只有四分之一的画面实现AI更新。尽管这是一个保证定速逻辑的好的实践,但是必须小心对待。例如,我们怎么确定四个画面有效地共用同一个AI周期,流畅地显示活物和图像呢?

如果所有在同一个AI 周期里的画面看上去完全一样,那么多画片也就没什么意义了,活物行速将达到15 fps。为解决这个问题,AI被分成两个部分。实时AI代码按固定步骤执行,反之,一些简单的程序如活物分类和弹道更新程序根据每个画面来处理。用这种方法,每秒额外的画面对玩家来说至关紧要。但是,基于线程的方法有一些问题需要解决。基本上,这个方法很好,但是在一些硬件上不能很好实现。一些单CPU机器不能很好地处理线程,特别是一些精确的时间函数。在频率周期中发生的变化会降低玩家的可体验性。问题并不在于产生线程的函数调用,而是精确的系统时间函数。因此,我们必须找出一个区域来在单CPU上模拟线程。

 最流行的可选方法是同过在单线程程序中使用规则的软件循环和定时器来实现线程,这并不支持并发机制。它的主要思想是连续地执行“Update”和“Render”调用,为保持固定的调用频率而跳过“Update”调用。我们减弱“Update”中的“Render”程序。“Render”被尽可能快地调用,而“Update”和时间同步。为达到这一效果,我们必须为 “Update”中的每一个调用记录时间邮戳。然后,再以后的反复循环中,我们从上次调用计算共用时间,并和期望频率相比。同过这样,我们测试是否需要一个“Update”调用来保持正确的调用频率。例如,如果你想以20次每秒的速度运行AI,你就必须每50毫秒调用一次“Update”程序。因而,你要做的就是保存你每次调用“Update”程序的时间点,在共用50毫秒后再执行它。很多时候,这种机制比基于线程和简单程序的方法提供了更好的控制。你不必去担心共享内存,同步等等。实际上,它是一个原始的基于线程的方法,如下图:

这是它的C代码;

 Long timelastcall=timeGetTime();

  While (! end)

  {

     If (( timeGetTime()-timelastcall)>1000/frequency)

         {

             Game_logic();

             Timelastcall=timeGetTime();

} 

}

   注意看看上面我们是怎样使用Win32 API timeGetTime()调用来作为定时器的。它毫秒级返回的Windows自启动以来的时间。因此,通过两次timeGetTime()调用结果的相减,我们可以得到精确到毫秒级的时间差。

上面的代码部分说明了我们所关注的东西,它是一个好的起跑点。然而,它似乎离专业游戏还很远:我们假设逻辑时钟0次就完成了,我们没有处理Alt-Tab游戏关等。为完整说明,我下面提供一个专业级别的游戏循环,思想是一样的,只多用了一步来实现更好的控制。

代码如下:

time0 = getTickCount();

while (!bGameDone)

   {
   

   time1 = getTickCount();

   frameTime = 0;

   int numLoops = 0;

 

   while ((time1 - time0) > TICK_TIME && numLoops < MAX_LOOPS)

      {
   

      GameTickRun();

      time0 += TICK_TIME;

      frameTime += TICK_TIME;

      numLoops++;

      }

   IndependentTickRun(frameTime);

 

   // If playing solo and game logic takes way too long, discard

   // pending time.

   if (!bNetworkGame && (time1 - time0) > TICK_TIME)

      time0 &#
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值