游戏开发与轮询
另一个游戏开发和典型应用程序开发之间的关键差异是轮询和事件注册的概念。许多非游戏的应用程序都采用用户事件驱动的方式来处理逻辑。举个例子,如果您要为某些系统写一个窗体组件命名模块,您可能会创建一个窗口然后要求用户输入想要的名字,窗口有一个确定按钮和一个取消按钮。不管这个程序用哪种编程语言写成,直到用户点击确定按钮或取消按钮之前都不会做任何事。当用户点击其中一个按钮时,系统将产生一个程序能够捕获的事件。换句话说,应用程序只有在用户给它发送一个某个按钮被按下的事件标识后,才会被唤醒并做一些处理。
相比之下,游戏程序由事件轮询(polling for events)驱动,而不是等待并监听是否有事件被激发。相反,游戏程序会主动询问系统鼠标是否被移动,同时程序会一直运行,不管有没有用户输入。
假设您开发的一个游戏中有一个名叫吉米(Jimmy)的男巫(相信我,许多游戏都会把魔法师命名为吉米)试图从恶魔珈蓝鸟(pelican)军团的掌握中逃离。您必须要解析用户事件,比如玩家移动吉米到左边或发动了一个破坏珈蓝鸟翼的咒语。但是并不是由XNA来告诉您玩家进行了这些动作,而是由您自己来轮询输入设备(鼠标、键盘、手柄等等)来获得输入的改变。
同时,不管用户是否与系统进行交互,游戏中的一切仍在进行。举个例子,可能珈蓝鸟军正在追逐吉米,而不管用户有没有激发任何事件,这个都会发生,并且游戏负责不断移动敌人的位置而不需要注册任何事件。这是需要游戏循环的主要原因:它提供了一种途径让游戏始终运行着而不管玩家在做什么。
当然,除了让敌人在屏幕上移动,还可以做更多的事情。要是珈蓝鸟军能够在空中抛出抗魔法的炸弹会怎么样呢?此时可能会有一个、两个、五个、五十个甚至更多的炸弹飞过空中,那么它们都需要持续地被移动才行;此外,还必须不断地检查这些炸弹是否击中任何物体,然后依此做出适当的回应。要是玩家一直不移动吉米,让珈蓝鸟军抓住了吉米呢?在这种情形下,肯定会有些事情发生才对。或许您需要设置一个计时器,让吉米必须在3分钟之内逃脱——这样一来,您又得去追踪某些类型的计时器,并且在计时器归零或吉米逃脱的时候会执行一些逻辑。
总之,在游戏开发中,一直会有事情发生(而且通常是很多事情),并且您要持续不断地更新动画、移动物体、进行碰撞检测、更新分数、核对游戏结束逻辑等等。
假如是在窗体组件程序中,要做到持续检测非用户事件这一点有些困难,但在XNA开发中,这一点用游戏循环的形式整合进了应用程序框架。所有这些任务都在游戏循环的Update方法中被处理,接着场景会在游戏循环的Draw方法中被绘制。
请注意
事实上,所有的应用程序都有和游戏循环功能相似的循环。Windows本身使用一个消息和事件系统,不断循环告知应用程序何时需要重绘它们自己和完成其它功能。对这些循环的访问通常是隐藏起来的,不过,大多数应用程序不需要访问这些非用户驱动事件。
好了,让我们回到之前看到的代码中。您会注意到在Update方法中有一些代码行在玩家按下手柄上的“返回”按钮时让游戏退出:
if(GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
这是在使用Xbox360手柄时结束Xbox360或Windows游戏的方法(否则,您可以在Windows中点击游戏窗口红色的X按钮或按Alt + F4组合键去结束它)。
就像之前提到的,Update方法是更新和游戏有关的一切东西的地方。您能更新物体在屏幕中的位置、分数、动画序列等。您也能检查用户输入,进行碰撞检测,并且调整AI 算法。
在Update方法中对游戏中变化的检测和依照这些变化进行的行动通常都与游戏状态有关。游戏状态是一个非常重要的概念:这是一种让游戏知道当前游戏状况的方法。游戏一般有多种完全不同的状态,比如启动画面、实际游戏画面和游戏结束画面。在同一状态下还会有些细微的变化,例如玩家得到某种法宝使角色无敌一段时间或其它一些游戏行为的改变。这通常需要先在Update方法中改变游戏状态,然后在Draw方法中使用这些状态来绘制不同的图像、场景或其它和特定状态相关的信息。
Draw方法中使用之前提到的图形设备对象,把游戏中所有的物体绘制到屏幕上。在目前的程序中,Draw方法做的唯一件事就是清除屏幕并设置背景色为浅蓝色(待会我们会深入讨论)。
图3-1展示了一个XNA游戏的生命周期,包括Update方法和Draw方法形成的一个游戏循环。
图3-1 一个XNA游戏的生命周期
请注意Update方法的执行有两个可能的结果:要么在游戏循环中持续执行,然后Draw方法被调用;要么游戏结束,退出游戏循环,并调用UnloadContent方法。当您调用Game类的Exit方法时,和您按下Xbox 360手柄上的“返回”按钮一样,游戏循环将结束。游戏循环也会在您按下Alt + F4组合键或点击红色的X按钮关闭游戏窗口时退出。
在您的游戏退出循环和结束游戏之间应该存在一些过程。比如说,如果恶魔珈蓝鸟捉住了男巫吉米,然后游戏直接退出并且游戏窗口就这么消失,将使玩家觉得很郁闷。实际上大多数玩家将会把这种行为看作某种Bug。为避免出现这种情况,您通常需要使用某种游戏状态逻辑,使Draw方法渲染游戏结束画面以接替游戏进行画面。然后在过了固定的时间之后,或者游戏玩家按下了某些特定的按键,游戏才会真正地退出。这些工作现在好像有些复杂而不容易理解,但是请不要过分担心。因为本书至始至终都会涉及到这些,很快您就能明白怎么实现它了。
一旦游戏退出循环,UnloadContent就会被调用。这个方法用来卸载所有在LoadContent方法里加载且需使用特殊手段进行卸载的内容。就像.NET一样,XNA也会自动进行垃圾回收。但如果您在处理某些需要特别处理的对象时修改了内存,UnloadContent方法就可以让您进行一些特殊处理。
修饰您的游戏
好,讲得够多了,您一定迫不及待地想要开始进行开发,并准备放一些很酷的东西到您的游戏中了。让我们开始吧。
先看一看Draw方法,现在方法中包含以下代码:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
base.Draw(gameTime);
}
这里需要注意的第一件事是Draw方法接受的参数。这个参数是GameTime类型的,用来表示游戏中经过的时间。为什么您需要一个追踪时间的的变量呢?因为并不是所有的计算机都以相同的速度运行。这个变量可以帮助您根据真实游戏时间而不是处理器速度来确定动画和其它事件发生的时机。gameTime将在整本书中用来度量诸如帧率、动画、声音和其它效果。同时这个参数也将会被传入Update方法,因为很多控制那些效果的方法需要在Update方法而不是Draw方法中执行。
在这个方法的末尾,调用了Game1对象的Draw方法。这是为了在GameComponents和其它对象中级联调用Draw方法所必不可少的。可能现在您还不能理解,但是您确实需要在代码中调用base.Draw方法,请不要移除它。
最后,让我们看看用图形设备对象GraphicsDevice属性来调用Clear方法。再次说明,这个属性代表PC、Xbox 360或Windows Phone 7上真实的图形设备并且允许您在屏幕上绘制各种物体。
Clear方法擦除屏幕上所有的物体,然后用指定的颜色填充背景(在这里是浅蓝色)。把颜色改变成如Color.Red之类然后通过选择Debug→Start Debugging来运行游戏,您将看到和之前相同的窗口,不过现在窗口的背景被填充成了红色。
记得我曾说过看起来无趣的蓝色背景下其实隐藏着许多细节吗?这就是指我刚才谈到的。当您只看到一个乏味的蓝色(或现在的红色)背景时,XNA已经做了许多工作来呈现它。它将游戏循环每秒种运行60次,擦除屏幕上所有的东西,然后填充为红色。
另外还每秒调用Update方法60次,并检测Xbox 360手柄上的“返回”按钮是否被按下。这看起来可能并不多,但是XNA确实在工作。XNA最棒的一点就是游戏的框架已经搭好,让您很容易进行定制和扩展。
那么,如果游戏循环每秒运行60次并且调用Update方法和Draw方法,又为什么要每次都清楚这个画面呢?尽管您可能觉得每一帧都清除屏幕然后重画整个场景和所有物体会降低游戏运行效率,但是这远比试图追踪场景中所有的东西,在新位置绘制出它们,然后绘制出之前被它们挡住的物体这样要高效得多。如果您移除了Clear调用,XNA在每帧绘制前将不会擦除屏幕,并会产生一些不可预期的结果。
帧和帧率
什么是帧?正如前面提到的,默认情况下XNA会在每次Draw调用的时候清除屏幕并且重绘场景。一次Draw调用所产生的场景就被称为一帧。您可以把XNA中的2D游戏想象成卡通连环画,您在一页中绘制一个角色,在下一页稍稍移动一点的位置绘制同样的角色,以此类推,当您快速翻动书页的时候,您就有角色在动的错觉。XNA事实上也做同样的事情。每隔16毫秒[1](或60次/秒),屏幕被清除然后新场景被绘制,当新场景中角色在稍微不同的位置时,那将会产生角色在动的假象。
多帧就形成了游戏中的动画,每秒绘制的帧数即被称为游戏帧率(例如,60fps=60帧/秒)。