第4章 用户输入和碰撞检测
虽然看到一组由您亲自实现的漂亮的旋转圆环是很酷的事情,但在完全掌握XNA之前您依然有很长的一段路要走。尽管动画看起来很漂亮,但是它没有干任何事情,并且它并不受您的控制。一个和玩家没有交互的游戏有什么乐趣可言呢?在这一章里,我们会研究用户输入和碰撞检测来使您的游戏除了看起来很漂亮外再干点有意义的事情。
这一章将会用到第3章末尾编写的代码(三环动画),本请打开项目并根据本章的说明进行改写。
更多精灵
如果您想要有一个受用户控制的物体并且加入和其它物体之间的碰撞检测,您至少还需要一个物体。让我们在项目中加入另一个精灵动画。
我们将为第二个精灵动画使用一个不同的图像,而非与之前相同的三环图像。在本章的源代码中的Chapter2\AnimatedSprites\AnimatedSprites\Content\Images文件夹下您可以找到一个名为skullball.png(印有骷髅图像的球)的图片文件。像之前那样,将图片添加到项目中 (鼠标右键点击解决方案资源管理器中的Content\Image文件夹,选择Add→Existing Item…,然后找到skullball.png并把它导入到解决方案中)。
接下来,您需要创建大量的变量来为您的骷髅球精灵绘图和制作动画。这些变量您看起来应该似曾相识了,因为它们和第3章中用来绘制三环动画的变量很相似。在Game1类的顶部添加这些成员变量:
Texture2D skullTexture; Point skullFrameSize = new Point(75, 75); Point skullCurrentFrame = new Point(0, 0); Point skullSheetSize = new Point(6, 8); int skullTimeSinceLastFrame = 0; const int skullMillisecondsPerFrame = 50;
骷髅球图像帧的尺寸是75×75像素,在精灵位图中有8行6列。现在您应该改变绘制三环动画用到的变量名以免产生混淆,为每个变量添加“rings”前缀,并修改所有的变量引用——这会帮助您使事情变得有条理。圆环动画相关的变量声明现在应该是这样:
Texture2D ringsTexture; Point ringsFrameSize = new Point(75, 75); Point ringsCurrentFrame = new Point(0, 0); Point ringsSheetSize = new Point(6, 8); int ringsTimeSinceLastFrame = 0; int ringsMillisecondsPerFrame = 50;
编译项目以确保重命名变量没有引入任何编译错误。如果有错误,记住变量名应该同之前的项目保持一致;只不过您在每个变量名的前面增加了“rings”前缀。修正所有错误直到编译正确。
第5章将带您了解一些基本的面向对象设计原则来使精灵的添加变得更容易。不过目前您只是想实现用户输入处理和碰撞检测,所以让我们继续吧。
与之前一样,在LoadContent方法中将骷髅球图像加载到skullTexture变量中:
skullTexture = Content.Load<Texture2D>(@"images\skullball");
接下来添加在精灵帧序列中移动索引的代码。记住这是在Update方法中完成的。因为之前已经在三环动画里做过这些,您可以直接将那段代码复制过来,然后将变量名修改一下就可以了:
skullTimeSinceLastFrame += gameTime.ElapsedGameTime.Milliseconds; if (skullTimeSinceLastFrame > skullMillisecondsPerFrame) { skullTimeSinceLastFrame -= skullMillisecondsPerFrame; ++skullCurrentFrame.X; if (skullCurrentFrame.X >= skullSheetSize.X) { skullCurrentFrame.X = 0; ++skullCurrentFrame.Y; if (skullCurrentFrame.Y >= skullSheetSize.Y) skullCurrentFrame.Y = 0; } }
最后,您要将精灵绘制到屏幕上,记住所有的绘制都在Draw方法中完成。同样地,您已经有了绘制三环动画的代码,将它复制过来,修改一下变量名。为了不让这两种精灵重叠在一起,修改骷髅球图像的Draw调用的第2个参数将图像绘制到(100, 100)而不是(0, 0),修改后的Draw方法看起来应该是这样:
spriteBatch.Draw(skullTexture, new Vector2(100, 100), new Rectangle(skullCurrentFrame.X * skullFrameSize.X, skullCurrentFrame.Y * skullFrameSize.Y, skullFrameSize.X, skullFrameSize.Y), Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0);
现在编译运行程序,您会看到两幅图像都有动画了。如图4-1。
还不赖!在很短的时间里您就在游戏中加入了一个全新的动画精灵。软件开发时常带来令人兴奋和有成就感的时刻,不过在游戏开发中因为视觉感官(稍后还有听觉)的引入,使这种感觉放大了许多。这两个动画看起来相当酷,不过有趣的事情才刚刚开始。现在您将要学习怎样控制屏幕上的物体并给您的程序提供一些用户交互。
XNA中的用户输入可以由多种设备组合而成:键盘、鼠标、Xbox 360手柄、Xbox 360周边设备和Windows Phone 7触摸屏和加速器。其中鼠标输入在Xbox 360和Windows Phone 7上不可用。
在这一章里,您将为游戏添加对键盘,鼠标和Xbox 360手柄的支持。
在之前的章节中,我们讨论了轮询和事件注册。两者之间的不同在处理输入设备的时候最明显。传统的Windows程序员习惯于对事件进行注册,比如某个键被按下或着鼠标移动。在这个编程模型中,应用程序执行一些功能,然后当它空闲时,消息泵将消息送入应用程序并且事件被处理。
图4-1 两个干着自己的事的动画精灵
在游戏开发中,没有所谓的空闲时间,所以允许开发者进行事件注册的开销过大。取而代之是由开发者对输入设备进行轮询以得知玩家是否对这些设备进行了操作。
这是从一个稍微不同的角度看待输入和其它消息与事件的方式,不过一旦您掌握了这种思维方式,游戏的开发将会简单明了许多。
键盘输入
键盘输入是通过Microsoft.XNA.Framework.Input命名空间中的Keyboard类来处理的。Keyboard类有一个叫做GetState的静态方法,用KeyboardState结构的形式返回键盘目前的状态。
KeyboardState结构中包含三个能够满足您大部分的功能需求的关键方法,如表4-1所示。
表4-1 KeyboardState结构体中的关键方法
方法 | 描述 |
---|---|
Keys[] GetPressedKeys() | 返回一个在方法被调用时被按下的键的数组 |
bool IsKeyDown(Keys key) | 返回true或false,取决于方法调用时参数所代表的按键是否被按下 |
bool IsKeyUp(Keys key) | 返回true或false,取决于方法调用时参数所代表的按键是否被释放 |
举个使用Keyboard类的例子,如果您想要检查“A”键是否被按下,需要使用如下代码:
if(Keyboard.GetState( ).IsKeyDown(Keys.A)) // BAM!!! A is pressed!
在这个游戏中,您要修改代码来使用户能够控制三环精灵,使用方向键使精灵上下左右移动。
目前三环精灵被硬编码绘制到(0, 0),想要在屏幕上移动它,您要能够改变它绘制的位置。您需要一个Vector2变量来表示精灵当前的位置,还需要一个表示精灵移动速度的变量。因为在游戏过程中精灵移动的速度不会变,所以您可以将这个变量声明为常变量。在Game1类的顶部添加这些类成员变量:
Vector2 ringsPosition = Vector2.Zero; const float ringsSpeed = 6;
另外,确定您修改了用来绘制三环精灵的SpriteBatch.Draw函数的第二个参数,以使用您的新变量ringPosition而不是Vector2.Zero标示的硬编码位置。Draw函数的第二个参数代表了绘制精灵的位置。
现在,您需要添加代码来检查上、下、左或右键是否被按下,如果任何一个被按下,就通过改变ringsPosition变量的值来移动精灵。
您应该把检测输入的代码放在什么地方呢?记住,决定在什么地方放置逻辑到游戏循环中时,只有两个地方可以选择:Draw方法,用来绘制所有的物体;以及Update方法,用来做绘制之外的其它事情(计算维护分数、移动物体、进行碰撞检测等等)。所以,只管将以下代码添加到Update方法中,位于base.Update方法的调用之前:
KeyboardState keyboardState = Keyboard.GetState( ); if (keyboardState.IsKeyDown(Keys.Left)) ringsPosition.X -= ringsSpeed; if (keyboardState.IsKeyDown(Keys.Right)) ringsPosition.X += ringsSpeed; if (keyboardState.IsKeyDown(Keys.Up)) ringsPosition.Y -= ringsSpeed; if (keyboardState.IsKeyDown(Keys.Down)) ringsPosition.Y += ringsSpeed;
这段代码怎么样呢——难道一个if/else语句不比4个if语句更有效率吗?对,的确是更有效率。但是一个if/else语句让您在同一时间只能向一个方向移动,而用4个单独的if语句让您可以沿着对角方向移动(例如,同时按住上和左两个键)。
还要注意您只在Update方法中调用了一次Keyboard的GetState方法,然后在if语句中重复使用这次调用的结果,而不是在每个if语句中调用GetState。这是因为GetState方法调用的开销相当大,用这种方式您可以减少调用这个方法的次数。
编译并运行程序。您应该可以在屏幕上移动三环精灵了,如图4-2。
图4-2 看——圆环在移动
鼠标输入
XNA提供了一个和Keyboard类行为很相似的Mouse类来和鼠标进行交互。Mouse类也有一个GetState方法,能以MouseState结构的形式从鼠标返回数据。Mouse类还有另一个值得一提的方法:void SetPosition(int x, int y)。这个方法能——您猜对了——允许您设置鼠标的位置。这个位置是相对于游戏窗口的左上角而言的。MouseState有一些属性将会帮助您了解到当您调用GetState时鼠标在特定时刻发生了什么。在表4-2中,有针对这些属性的细节的说明。
表4-2 MouseState结构体中重要的属性
属性 | 类型 | 描述 |
---|---|---|
LeftButton | ButtonState | 返回鼠标左键的状态 |
MiddleButton | ButtonState | 返回鼠标中键的状态 |
RightButton | ButtonState | 返回鼠标右键的状态 |
ScrollWheelValue | int | 返回自游戏开始后鼠标滚轮滚动刻度的累加量.要想知道滚轮滚动了多少,把当前帧的ScrollWheelValue和上一帧的进行比较. |
X | int | 返回鼠标光标相对于游戏窗口左上角的水平位置(坐标).如果鼠标光标在游戏窗口的左侧,这个值是负值;如果在游戏窗口右边,这个值大于游戏窗口的宽度. |
XButton1 | ButtonState | 返回某些鼠标上额外的按键的状态 |
XButton2 | ButtonState | 返回某些鼠标上额外的按键的状态 |
Y | int | 返回鼠标光标相对于游戏窗口左上角的垂直位置(坐标).如果鼠标光标在游戏窗口的上方,这个 值是负值;如果在游戏窗口下方,这个值大于游戏窗口的高度. |
您也许注意到了默认情况下当鼠标划过XNA游戏窗口时鼠标指针是隐藏的。如果您想在窗口中显示鼠标,只要设置Game类的IsMouseVisible属性为true就可以了。
不管鼠标光标是否可见,GetState调用返回的MouseState结构体都表示鼠标当前的状态。
让我们用鼠标的移动来控制三环精灵在游戏窗口中的四处移动。留着之前添加的键盘控制代码不动,最后您将可以使用多种方式来控制精灵。
因为MouseState的X和Y属性告诉您当前鼠标指针的位置,您可以将精灵的位置设置成鼠标的当前位置。
不过,因为您允许玩家还可以使用键盘,您不能只是把精灵的位置设置成鼠标的位置。否则,不管玩家是否移动鼠标,精灵都会一直停留在鼠标所在的位置。
为了确定鼠标是否被移动,在Game1类顶部添加一个类成员变量:
MouseState preMouseState;
这个变量将会追踪上一帧的鼠标状态。您将使用preMouseState在每一帧和当前帧的鼠标状态做比较。如果X和(或)Y属性的值不同,您就知道玩家移动了鼠标并且您可以移动精灵到新的鼠标位置。
将以下代码添加到Update方法中,位于base.Update方法的调用之前:
MouseState mouseState = Mouse.GetState( ); if(mouseState.X != prevMouseState.X || mouseState.Y != prevMouseState.Y) ringsPosition = new Vector2(mouseState.X, mouseState.Y); prevMouseState = mouseState;
这段代码将精灵移动到鼠标所在的位置,但只是在鼠标被移动的情况下才这么做。如果您此时编译并运行程序,会发现您现在可以用鼠标或键盘来控制三环精灵了。
游戏手柄输入
如果您在进行Windows游戏开发,您仍然能够为一个Xbox 360手柄编程。您需要一个有线手柄或者花大约20美元购买一个Xbox360无线手柄,这将使您能够把最多4台无线手柄连接到一台电脑。
请注意
如果您购买了无线手柄的充电套装,将附有一根连接线,不过并没有数据通过线缆传输,所以即使您插上它还是一个无线手柄。充电套装的线缆用来输送电流进行充电,仅此而已。
XNA在提供获取为鼠标输入的Mouse类和获取键盘输入的Keyboard类的同时,也提供了从Xbox 360手柄获得输入的GamePad类。没错,正如其它设备类那样,GamePad类也有一个GetState方法。关于标准我还要多说两句,Microsoft XNA Framework(这里包括框架和API)是如何通过兼容多种平台的标准来获得巨大利益的极好例子。大多数时候,您可以仅仅通过对象类型和相似的成员方法来使用对象。这是XNA小组杰出设计的贡献——向他们致敬。GamePadState结构体的主要属性列在表4-3中。
表4-3 GamePadState结构的重要属性
属性 | 类型 | 描述 |
---|---|---|
Buttons | GamePadButtons | 返回一个表示当前哪些按键被按下的结构体.每个按键由一个ButtonState枚举来表示,用来确定按键是否被按下. |
DPad | GamePadDPad | 返回表示十字键上的哪个键被按下的结构体.DPad结构体有4个按键(上,下,左,右),每个按键由一个ButtonState表示,用来确定按键是否被按下. |
IsConnected | boolean | 指示手柄目前是否连接到Xbox 360. |
ThumbSticks | GamePadThumbSticks | 返回一个结构体用来确定模拟摇杆的方向.每个摇杆(左边的和右边的)是一个X和Y成员取值范围为-1到1的Vector2对象(拿左摇杆来说,如果您一直向左推动,它的X值就为-1;如果您不动它,它的X值就为0;如果您一直向右推,X值就为1). |
Triggers | GamePadTriggers | 返回表示扳机键是否被按下的结构体.Triggers结构体包含两个浮点数(左和右).值为0代表扳机键没有被按下,值为1代表扳机键被按下. |
GamePadState结构体中包含两个能够满足您大部分需求的方法。方法在表4-4中列出。
表4-4 GamePadState结构体中的主要方法
方法 | 描述 |
---|---|
bool IsButtonDown(Buttons) | 传入一个或多个键的按位或运算值.如果所有传入的按键被按下,返回true,否则返回false. |
bool IsButtonUp(Buttons) | 传入一个或多个键的按位或运算值.如果所有传入的按键被释放,返回true,否则返回false. |
观察表4-3中的属性,您会注意到有些控制表示为布尔或者双态值(开或关),与此同时其它的控制值则表示为一个范围内数字的波动(0到1,或者-1到1)。 这些值和模拟手柄有关,因为它们不是简单的拥有开或关状态,它们在游戏中提供了更多的准确性和精确度。您可能会注意到在Xbox 360的一些游戏中您可以通过改变扳机或摇杆来用不同的速度移动——这是因为您在您在指定的方向按下其中一个按键的时候,手柄将会向应用程序发出一个长短不一的信号。这是一个为Xbox 360手柄编码时应该记住的概念和一个应该加入到您的游戏中的特色。在本章的内容中,我们将会介绍如何实现这些。
好了,让我们加入一些代码来让您可以用您的Xbox 360手柄来控制您的精灵。像以前一样,把您的鼠标和键盘的控制代码也留在那里,如此一来,您就有三种方式来控制您的精灵了。
由于摇杆可以包括从-1到1范围的X和Y的值,您会想到用ThumbStick属性的这些值来乘以ringsSpeed变量。用这种方式,如果摇杆一直朝向同一个方向,精灵将会在此方向全速移动;如果按钮只是被轻轻推向了一个方向,它将在这个方向移动得相对缓慢。
下面的代码会按照第一个玩家的摇杆的推向左边的程度和方向来调整您的精灵位置。把这段代码插入Update函数,在键盘和鼠标输入代码的下面:
GamePadState gamepadState = GamePad.GetState(PlayerIndex.One); ringsPosition.X += ringsSpeed * gamepadState.ThumbSticks.Left.X; ringsPosition.Y −= ringsSpeed * gamepadState.ThumbSticks.Left.Y;
编译并运行程序,您可以用Xbox 360手柄控制三环精灵了。
让我们再加入一些有趣的点子。既然是使用Xbox 360手柄,那么就应该能挖掘出更多的乐趣才对。让我们添加一个加速功能来使精灵的移动速度达到原来的两倍。当然,加速模式下精灵在屏幕迅速移动的时候,您应该由于精灵的狂飙而从手柄感觉到震动。您或许之前在Xbox 360手柄上感受过震动。这种机制被称为力回馈,它能极大地提升游戏体验,因为它甚至增加了另一种感官来使玩家更有代入感。
SetVibration方法可以设置手柄震动马达的速度。这个方法返回一个布尔值用来指示调用是否成功(false意味着手柄没有连接或有一些其它问题)。这个方法接受一个玩家编号 和一个为手柄马达设置的浮点值(0到1)。将这个值设为0时停止马达的震动。任何大于0的值都会使手柄以不同的速度震动。修改之前添加的用Xbox 360手柄移动精灵的代码,加入以下内容:
GamePadState gamepadState = GamePad.GetState(PlayerIndex.One); if (gamepadState.Buttons.A == ButtonState.Pressed) { ringsPosition.X += ringsSpeed*2*gamepadState.ThumbSticks.Left.X; ringsPosition.Y -= ringsSpeed*2*gamepadState.ThumbSticks.Left.Y; GamePad.SetVibration(PlayerIndex.One, 1f, 1f); } else { ringsPosition.X += ringsSpeed * gamepadState.ThumbSticks.Left.X; ringsPosition.Y -= ringsSpeed * gamepadState.ThumbSticks.Left.Y; GamePad.SetVibration(PlayerIndex.One, 0, 0); }
这段代码首先检查手柄上的A键是否被按下。如果是,加速模式被激活,这意味着您将以两倍于正常情况的速率来移动精灵并触发手柄的震动机制。如果A键没有按下,震动功能无效并且精灵将以正常速率移动。
编译并运行游戏,实际感受一下它的运作方式。
如您所见,游戏手柄为增加了一种输入方式,并相对于游戏本身提供了另一种不同的体验。它是一个很有用的工具,但并非适合所有类型的游戏。确保您考虑好哪种输入方式对于您的游戏来说是最好的选择,因为输入机制对于提高游戏的娱乐性大有裨益。
保持精灵在游戏窗口中
您可能已经注意到了,三环精灵会在您将它移动得足够远时消失在屏幕的边缘。让玩家控制的物体能够离开屏幕并且消失不见永远不会是一个好主意。要纠正这个问题,您需要在Update函数的结尾更新精灵的位置。如果精灵已经向左、向右、向上或向下移动得太远,更正它的位置来使其保持在游戏窗口中。将下面的代码添加到Update方法的末尾,位于base.Update方法的调用之前:
if (ringsPosition.X < 0) ringsPosition.X = 0; if (ringsPosition.Y < 0) ringsPosition.Y = 0; if (ringsPosition.X > Window.ClientBounds.Width - ringsFrameSize.X) ringsPosition.X = Window.ClientBounds.Width - ringsFrameSize.X; if (ringsPosition.Y > Window.ClientBounds.Height - ringsFrameSize.Y) ringsPosition.Y = Window.ClientBounds.Height - ringsFrameSize.Y;
编译并运行程序,您可以像以前一样在屏幕上移动精灵;不过它会一直保持在游戏窗口中而不会从屏幕边缘消失。
碰撞检测
到目前为止,游戏的开发进行得相当不错。玩家已经能够与游戏互动并且在屏幕上四处移动三环精灵——但是仍然有许多工作要做。要进行下一步,您需要增加一些碰撞检测。
碰撞检测在几乎任何游戏中都是很关键的一个部分。您试过在玩一部射击游戏时您看起来已经打中了目标但是什么也没发生吗?或者在一部赛车游戏中您似乎已经远离墙壁了但您还是撞上了它?这种游戏体验会惹恼玩家,导致这种结果的原因是碰撞检测实现得很糟糕。
无疑地,碰撞检测决定了游戏进行时的流畅性;它对流畅性的影响如此大的原因,在于碰撞检测算法越是精确到位,游将就会运行得越缓慢。在碰撞检测方面,很明显需要在准确性和性能之间进行权衡。
实现碰撞检测最简单和快速的方式是通过包围盒算法。本质上,当用一个包围盒算法时,您需要在屏幕上的每个物体周围“画”一个盒子,然后检查这些盒子是否相交。如果相交,就有碰撞。图4-3显示了三环精灵和骷髅球精灵周围隐藏的包围盒。
要在目前的游戏中实现包围盒算法,您需要为每个精灵创建一个基于精灵位置和精灵帧的宽和高的矩形。如同对精灵的处理一样,如果您将骷髅球精灵的位置改为变量会更容易理解代码。添加下列类成员变量,用来存放骷髅球精灵的位置,并且将变量初始化为您现在绘制骷髅球精灵的位置(100, 100)。
Vector2 skullPosition = new Vector2(100, 100);
图4-3 两个精灵周围的包围盒
接下来,用这个变量取代绘制骷髅球精灵的Draw调用的第二个参数。
好了,现在已拥有一个代表骷髅球精灵位置的变量,您可以用这个变量以及骷髅球精灵的尺寸来创建一个矩形并查看它是否与用相似方式创建的三环精灵的矩形相交。
添加下面的方法到Game1类中,它可以用XNA框架的Rectangle结构体为每个精灵创建一个包围矩形。结构体有一个叫做Intersects的方法可以用来检测两个矩形是否相交:
protected bool Collide( ) { Rectangle ringsRect = new Rectangle((int)ringsPosition.X, (int)ringsPosition.Y, ringsFrameSize.X, ringsFrameSize.Y); Rectangle skullRect = new Rectangle((int)skullPosition.X, (int)skullPosition.Y, skullFrameSize.X, skullFrameSize.Y); return ringsRect.Intersects(skullRect); }
接下来您需要用这个Collide方法来检测两个物体是否发生碰撞。如果是,您应该执行一些操作。眼下您只是在精灵发生碰撞时调用Exit方法关闭游戏。显然,在真实游戏中您不会这么做,因为仅仅发生了类似碰撞这样的事情游戏就退出了对玩家而言像是一个Bug。但是由于我们只是想测试碰撞检测的效果,目前就这样进行。
在Update方法的末尾添加下面的代码,位于base.Update方法的调用之前:
if (Collide( )) Exit( );
编译并运行游戏,如果您移动三环精灵到太靠近骷髅球精灵的地方,应用程序将会关闭。
您也许会注意到三环精灵和骷髅球精灵并没有真正地接触。为什么会这样呢?如果您仔细看看精灵位图(图4-4),您会看到每个图像帧之间有些间距。这个距离在最大的三环水平旋转的时候会变得更大(仔细看运行时的程序),空白也被作为碰撞的有效区域,这是因为您使用的是帧尺寸而不是物体(圆)的真实尺寸来创建包围矩形。
一种修正这种情况的方法是调整精灵位图以减少空白。另一种方法是创建一个小一号矩形来进行碰撞检测。这个小矩形必须以精灵为中心,因此需要从每个帧的边缘做少许偏移(译注:可以理解为缩小矩形的尺寸以适应精灵的大小)。
要创建一个稍小的矩形,您需要为每个精灵定义一个偏移量,用来表示在每个方向上包围矩形要比精灵帧小多少(偏移多少)。增加以下两个类成员变量到项目中:
int ringsCollisionRectOffset = 10; int skullCollisionRectOffset = 10;
下面,您将使用这些变量来构造一个比实际帧尺寸稍微小一点的矩形。修改Collide方法为下面这样,您会得到更加精确的碰撞检测:
请注意
译者附注:偏移量指的是外围大矩形和里面的小矩形对应边之间的距离。
protected bool Collide( ) { Rectangle ringsRect = new Rectangle( (int)ringsPosition.X + ringsCollisionRectOffset, (int)ringsPosition.Y + ringsCollisionRectOffset, ringsFrameSize.X - (ringsCollisionRectOffset * 2), ringsFrameSize.Y - (ringsCollisionRectOffset * 2)); Rectangle skullRect = new Rectangle( (int)skullPosition.X + skullCollisionRectOffset, (int)skullPosition.Y + skullCollisionRectOffset, skullFrameSize.X - (skullCollisionRectOffset * 2), skullFrameSize.Y - (skullCollisionRectOffset * 2)); return ringsRect.Intersects(skullRect); }
编译并运行程序,试试新的碰撞检测效果。使用这个方法将会得到更精确的碰撞检测。有一个相近的算法用包围球来代替包围盒。您也可以用这个算法,特别当物体是圆形的时候。但是在将来的章节中会使用一些非圆形的物体,所以现在还是继续用包围盒好了。
尽管您已经使算法完善了一些,但运行程序时还是会发现碰撞检测不是100%的精确。在这个简单的测试中并不明显。游戏中的目标并不需要100%精确的碰撞检测,而是达到一定程度上的精度使得玩家感觉不到差别。
听起来像是作弊,但是实际上这归结为性能的问题。举例来说,假设您使用一个非圆形的精灵,像是飞机之类的。在飞机周围画一个单一的包围盒将产生非常不精确的碰撞检测结果。为避免这样您可以为飞机添加多个小的矩形然后用小矩形和游戏中的物体进行碰撞检测。这样的包围盒布局如图4-5所示。
左边的例子会很不精确,而右边的能大幅提升算法的精确度,但是,您会遇到什么问题?假设您的游戏中有两架飞机并且您想看看它们是否发生碰撞。您现在需要将一 架飞机上的每个包围盒和另一架飞机上每个包围盒进行比较。两架飞机要进行25次比较计算!想象一下如果您加入更多的飞机到游戏中——计算量将会呈指数增长并且最后影响游戏的速度。
有一种办法可以将两种算法相结合来提高性能。就是首先对整个物体的包围盒进行碰撞检测,如图4-5左边的例子。如果返回结果有潜在的碰撞可能(译注:就是说大的包围盒发生了碰撞),您可以继续深入对子包围盒进行碰撞检测,如图4-5右边的例子。
这样的碰撞检测,其实可以说是兼顾性能与准确性之间的平衡的做法。尽管做了诸多努力,使用了图4-5中右边例子中所有的包围盒进行碰撞检测,结果也不会是100%的精确。再次说明游戏中碰撞检测的目标是尽量精确而又不影响用户体验或性能。
要想提升碰撞检测的速度,其实还有另一种方法,也要这里也说明一下。实际上,将一个游戏窗口划分成基于网格的坐标系,就能让您可以通过很简单的测试,来判断两个物体是否离得足够近而需要进一步进行碰撞检测。如果您追踪每个物体目前所在的格子,就可以先通过检查确保这两个物体处于同一个格子中,然后再为它们执行碰撞检测算法。这种方法可以在每帧中有效地减少计算量并显著提升游戏的性能。
图4-4 在使用帧尺寸进行碰撞检测时图像中的空白会产生低精度的碰撞检测结果
图4-5 带一个包围和的飞机(左)和带多个包围和飞机(右)
您刚刚做了些什么
干得漂亮!您实现了一些很酷的动画,并且您可以在屏幕上移动精灵时进行碰撞检测,这些都令人印象深刻。下面是对您在本章做的事情的概述:
• 您实现了让用户通过键盘、鼠标和Xbox 360手柄控制精灵。
• 您用Xbox 360手柄实现了力回馈。
• 您实现了对两个动画精灵进行碰撞检测。
• 您学习了怎样让碰撞检测在性能和精确度之间取得平衡。
总结
• XNA支持的输入设备有键盘、鼠标和Xbox360手柄。
• Xbox 360有一些模拟输入允许每个按钮有不同的输入力度。
• 碰撞检测始终是性能和精确度之间的平衡。通常算法的精确度越高,性能就越低。
• 包围盒算法是最简单直接的碰撞检测算法中的一种。如果您在每个物体周围“画”一个假象的包围盒,您可以很容易知道哪个包围盒和另一个碰撞。
• 您可以用不同的方法相结合来提高碰撞检测的精确度。用一个大的包围来检测是否值得花时间对物体各组成部分的包围盒进行碰撞检测,或者实现一个基于网格的系统来避免对没有互相靠近的物体进行多余的碰撞检测。
知识测试:问答
1. 从鼠标读取输入用哪个对象?
2. 真还是假:XNA程序中读到的X,Y坐标表示鼠标自上一帧移动了多少。
3. 模拟输入控制和数字输入控制有什么不同?
4. 描述包围盒碰撞检测算法。
5. 描述包围盒算法的优缺点。
知识测试:练习
让我们把这一章和之前一章的一些元素整合在一起。修改本章末尾的的代码,使它包含另一个不受用户控制的精灵(使用plus.png图片,在本章源代码的AnimatedSprites\AnimatedSprites\AnimatedSpritesContent\Image文件夹下)。让两个非用户控制精灵(译注:即原本的骷髅球精灵和新添加的精灵)都移动起来,像第二章里做的那样,使每个精灵都沿X、Y方向移动并在屏幕边缘反弹。为新的精灵也增加碰撞检测。结果您得到了一个躲避移动精灵的游戏。当您碰到其它的精灵的时候,游戏结束。
为清楚起见,plus.png的帧尺寸是75×75像素,有4行6列(注意三环和骷髅球的精灵位图有8行6列)。