第二章 精灵的乐趣(1)

译注:精灵,原文为Sprite,本文译作精灵,Sprite指场景中用来表示角色或其他物体的2D或3D图像。

具体的论述可以看风海迷沙的文章以便得到关于Sprite一词准更准确的认识:

关于Sprite一词的译法

感谢风海迷沙的专业精神,让我受益良多。


在上一章中,我提到您创建的项目的简单蓝色背景下实际上有很多事正在发生。让我们更深入了解代码看到底发生了些什么。请打开您上一章创建的游戏项目。

幕后一览

Program.cs文件内容很简单。您的Main函数创建了一个类型为Game1的新对象game,并且执行了game的Run()方法。

您游戏的真正部分在Game1.cs文件中,文件内容看起来应该像这样:

 

在这段代码中您会注意到自动提供了几个类成员变量,还有Game1的构造函数和其他五个方法。第1个类成员变量是GraphicsDeviceManager.这是一个非常重要的对象,因为它为开发者提供了一种途径去访问PC,Xbox360或Zune上的图形设备。GraphicsDeviceManager有一个属性GraphicDevice代表了您机器上实际的图形设备。因为图形设备对象在XNA游戏和您的显卡之间一个中介作用(或更准确的说,显卡上的GPU),您的XNA游戏在屏幕上做的任何事情都要通过这个对象。

第2个成员变量是SpriteBatch类的实例,这是一个您将用来绘制精灵(Sprite)的核心对象。在计算机图形学术语中,一个精灵被定义为场景中的一个2D或3D图像。2D游戏由场景中各种各样的精灵组成(角色精灵,敌人精灵,背景精灵,等等)。在这一章中您将用到这个概念并且绘制自己的精灵。

Initialize方法用来初始化变量和Game1对象相关的其他对象。您的图形设备对象将在这时被实例化,然后可以在Initialize被用来帮助您基于设备对象的设置来初始化其他的对象。在将来的章节中您将用这个方法来初始化分数值和其他类似项目。

LoadContent方法在Initialize方法之后被调用,另外在任何需要重新载入游戏图形内容的时候也会被调用(例如,因为玩家改变显示设置而重置了图形设备,诸如此类)。LoadContent是您加载游戏需要的所有图形或其他内容的地方,包括图像,模型,声音等等。

因为目前的项目不会做什么令人激动的事,所以这个方法中也不会有什么。

当LoadContent方法调用完成后,Game1对象将进入游戏循环。几乎所有的游戏都使用某种形式的游戏循环,不管它们是否用XNA写成。这是游戏开发和常规应用程序开发不同的一个方面。对一些开发者来说需要一点时间来适应。

本质上来说,一个游戏循环由一系统方法组成,这些方法反复被调用直到游戏结束。在XNA中,游戏循环只包含两个函数:Update和Draw。眼下您可以这样理解游戏循环:所有影响游戏的逻辑都将在Update或Draw方法中完成。您应该设法在Draw方法中做最少的事情。游戏运行需要的所有东西(最终涉及到移动物体,碰撞检测,更新分数,游戏结束检测逻辑,等等)应该放到Update方法中。

游戏开发和轮询

另一个游戏开发和典型应用程序开发之间的关键差异是轮询和事件注册的概念。许多非游戏应用程序都仅仅编写成通过用户进行事件驱动。举个例子,如果你要为某些系统写一个窗体组件命名模块,你可能会创建一个窗口然后要求用户输入想要的名字,窗口有一个确定和取消按钮。不管这个程序用哪种编程语言写成,直到用户点击确定或取消按钮之前都不会做任何事。当用户点击其中一个按钮时,系统将产生一个程序能够捕获的事件。换句话说,应用程序只有在用户给它发送一个某个按钮被按下的事件标识后才会被唤醒并做一些处理。

相比之下,游戏程序由事件轮询(polling for events)驱动,而不是等待并监听是否有事件被激发。取而代之,游戏程序会主动询问系统鼠标是否被移动,同时程序会一直运行,不管有没有用户输入。

假设您开发的一个游戏中名叫Jimmy的男巫(哦,男巫Jimmy游戏拥有很大的市场)试图从恶魔珈蓝鸟军团的掌握中逃离。您必须要解析用户事件比如玩家移动Jimmy到左边或发动了一个破坏珈蓝鸟翼的咒语。但是并不是由XNA来告诉您玩家进行了这些动作,而是由您自己来轮询输入设备(鼠标,键盘,手柄,等等)来获得输入的改变。

在同一时间,不管用户是否与系统进行交互,游戏中的一切仍在进行。举个例子,可能珈蓝鸟军正在追逐Jimmy,而不管用户有没有激发任何事件,这个都会发生,并且游戏负责不断移动敌人的位置而不需要注册任何事件。这是需要游戏循环的主要原因:它提供了一种途径让游戏始终运行着而不管玩家在做什么。

当然,除了让敌人在屏幕上移动,还可以做更多的事情。要是珈蓝鸟军能够在空中抛出抗魔法的炸弹怎么样?或许会有1,2,5,50甚至更多的炸弹在天空飞过而需要不断的被移动。你还必须去不断检测看那些炸弹是否击中了什么而起作用。要是玩家一直不移动Jimmy让珈蓝鸟军抓住了Jimmy呢?在那种情况下或许要发生点什么。也许你需要设置一个计时器,Jimmy必须在3分钟之内逃脱--现在你又得去追踪某些类型的计时器,并且在计时器归零或Jimmy逃脱的时候会执行一些逻辑。

在游戏开发中,程序会不断运作,并且您要持续不断的更新动画,移动物体,进行碰撞检测,更新分数,检测游戏结束逻辑,等等。

在假设的窗体组件命名程序中,要做到持续检测非用户事件这一点有些困难,但在XNA开发中,这一点用游戏循环的形式整合进了应用程序框架。所有这些任务都在游戏循环的Update方法中被处理,接着场景绘制在游戏循环的Draw方法中完成。

clip_image001事实上,所有的应用程序都有和游戏循环功能相似的循环。Windows本身使用一个消息和事件系统,不断循环告知应用程序何时需要重绘和完成其他功能。对这些循环的访问通常是隐藏起来的,不过,大多数应用程序不需要访问这些非用户驱动事件。

好了,让我们回到之前看到的代码中。您会注意到在Update方法中有一些代码行在玩家按下手柄上的“返回”按钮时让游戏退出:

 

这是在使用Xbox360控制器时如何结束Xbox360或Windows游戏的方法(否则,您可以在Windows中点击游戏窗口红色的X按钮或按Alt + F4组合键去结束它)。

如早先提到的,Update方法是您更新和游戏有关的一切东西的地方。您能更新物体在屏幕中的位置,分数,动画序列等等。您也能检查用户输入,进行碰撞检测,并且调整AI算法。

在Update方法中对游戏中变化的检测和依照这些变化的行动通常都和游戏状态有关。游戏状态是一个非常重要的概念:这是一种让游戏知道当前游戏状况的方法。游戏一般有多种完全不同的状态,比如启动画面,实际游戏画面,游戏结束画面。还会有一些细微的状态改变,比如玩家得到某种宝物使角色无敌一段时间或其他一些游戏行为的改变。通常您需要在Update方法中改变游戏状态然后在Draw方法中使用这些状态来绘制不同的图像,场景或其他和特定状态相关的信息。

Draw方法是把游戏中所有的物体绘制到屏幕上的地方,使用之前提到的图形设备对象。在目前的程序中,Draw方法做的唯一件事就是清除屏幕并设置背景色为CornFlowerBlue(待会我们会深入讨论)。

图2-1展示了一个XNA游戏的生命周期,包括Update和Draw方法形成一个游戏循环。

clip_image004

图2-1 一个XNA游戏的生命周期。

请注意Update方法的执行有两个可能的结果:要么持续执行然后Draw方法被调用,要么游戏结束,退出游戏循环并且调用UnloadContent方法。当您调用Game类的Exit方法时,游戏循环将结束,就像您按下Xbox360控制器上的“返回”按钮一样。游戏循环也会在您按下Alt + F4组合键或点击红色的X按钮关闭游戏窗口时退出。

在您的游戏退出循环和结束游戏之间应该存在一些过程。比如说,如果恶魔珈蓝鸟捉住了男巫Jimmy,然后游戏直接退出并且游戏窗口就这么消失,将使玩家觉得很郁闷。实际上大多数玩家将会把这种行为看作某种Bug。相应的,您通常使用某种游戏状态逻辑使Draw方法调用去渲染游戏结束画面代替游戏进行画面。然后过了固定时间后或您检测到玩家按下某些键,游戏才会真正的退出。这些工作现在好像有些复杂不容易理解,但是请不要过分担心。因为本书至始至终都会接触到这些,很快您就能明白怎么做到。

一旦游戏退出循环,UnloadContent会被调用。这个方法用来卸载所有在LoadContent方法里加载的内容。就想.NET一样,XNA会进行自动垃圾回收,但如果你在某些对象中进行需要特别处理的内存操作,UnloadContent方法就可以让您进行这些处理。

修饰您的游戏

好,讲得足够多了,您一定心痒痒想要开始进行开发然后准备放一些很酷的东西到您的游戏中了。让我们开始吧。

看一看Draw方法,现在方法中包含以下代码:

 

这里需要注意的第一件事是Draw方法接收的参数。这个参数是GameTime类型的,用来表示游戏中经过的时间。为什么您需要一个追踪时间的的变量呢?因为并不是所有的计算机都以相同的速度运行。这个变量帮助您使用真实游戏时间而不是处理器速度来确定动画和其他事件发生的时机。gameTime将在整本书中用来度量诸如帧率,动画,声音和其他效果。同样这个参数也传入Update方法,因为很多控制那些效果的函数需要在Update而不是Draw方法中执行。

在这个方法的末尾,调用了Game1基类的Draw方法。这是为了在GameComponents和其他对象中级联调用Draw方法所必不可少的。现在您可能不理解,但是您需要在代码中调用base.Draw方法,并且请不要移除它。

最后,让我们看看用图形对象的GraphicsDevice属性来调用Clear方法。再次说明,这个属性代表您的PC,Xbox360或Zune上的真实图形设备并且允许您绘制各种物体到屏幕中。

Clear方法擦除屏幕上所有的东西然后用指定的颜色填充背景(在这里是CornFlowerBlue)。把颜色改变成如Color.Red然后再运行您的游戏,您将看到和之前同样的窗口,不过现在背景被填充成了红色。

记得我曾说过看起来无趣的蓝色背景下其实隐藏着许多细节吗?这就是指我刚才谈到的。当您看到一个乏味的蓝色背景(或现在的红色),XNA做了许多工作来将它呈现给您。它将游戏循环每秒种运行60次,擦除屏幕上所有的东西然后填充为红。另外还每秒调用Update方法60次,并检测Xbox360控制器上的“返回”按钮是否被按下。

XNA最棒的一点就是游戏的框架已经搭好,让您很容易进行定制和扩展。

那么,如果游戏循环每秒运行60次并且调用Update和Draw,为什么每一帧都需要清屏?尽管您可能觉得每一帧都清除屏幕然后重画整个场景和所有物体会效率低下,但是这远比试图追踪场景中所有的东西,在新位置画出它们,然后画出之前被它们挡住的物体这样高效得多。如果您移除了Clear调用,XNA在每帧绘制前将不会擦除屏幕,并会产生意外的绘制结果。

帧和帧速率

什么是帧?像以前提到的一样,默认情况下XNA会在每次Draw调用的时候清除屏幕并且重绘场景。这样一次Draw调用所产生的场景就称为一帧。您可以把XNA中的2D游戏想象成活页卡通书,您在一页中绘制一个角色,在下一页稍稍移动一点的位置绘制同样的角色,以此类推,当您快速翻动书页的时候,您就有角色在动的假象。XNA也做了同样的事情。每16微秒(或60次/秒),屏幕被清除然后新场景被绘制,当新场景中角色的位置和之前稍有不同的时候,就会产生角色活动的假象。

多帧就形成了游戏中的动画,每秒绘制的帧数就被称为游戏的帧速率(例如,60fps=60帧/秒。

在您的项目中增加一个精灵

OK,我说过不再多说,这一次我是认真的。让我们开始。您的项目到目前为止是令人乏味的。现在,让我们绘制一副图像到屏幕上。

XNA中所有的图形,声音,特效和其他东西都要通过一个称为内容管线的东西加载。本质上内容管线将诸如.Jpg文件,.Bmp文件,.Png文件和其他格式的文件在编译过程中转换成一种XNA很容易使用的内部格式,对于其他类型的资源也是如此。如声音文件,3D模型,字体等,后来的章节会深入探讨。XNA框架很大的一点好处就是文件类型对于XNA是透明的,如果您在游戏中添加了一个图像文件,内容管线在编译过程中能够识别文件格式。您不需要为图像格式担心。(稍后的章节会更深入讨论内容管线)。

下载本章的源码到您的硬盘上。这样您就可以得到本章剩下部分需要用到的图像文件,并把它们加入到您的项目中。

打开Visual Studio的解决方案管理器(Solution Explorer)看看您的项目,您会看到一个Content节点,这就是您将要往项目中添加资源(图形,声音,模型等)的地方。因为我喜欢一切都井井有条,所以我建议在Content节点下为每种内容类型创建一个子文件夹,把资源分类存放。要做到这一点右键点击Content节点,选添加(Add)->新建文件夹(New Folder)。将新建的文件夹命名为Images.然后在Images文件夹上右键选泽添加(Add)->现有项目(如图2-2)。

clip_image006

图2-2 添加一张图片到您的解决方案中

在打开的文件查找对话框中,导航到您存放刚才下载的源码的文件夹,定位到BasicSprite/Collision/Conten/Images目录下。选择logov.png文件,然后点击添加(Add)按钮关闭对话框,您选择的文件就会出现在解决方案浏览器的Content/Images文件夹下,同时文件也拷贝到您项目的Content/Images/文件夹中。现在生成您的项目,点击生成(Build)->生成解决方案(Build Solution),将使内容管线尝试编译您刚才添加的图像文件。如果没有生成错误的话,就说明内容管线可以识别文件格式并能将它转换成XNA内部格式,并且XNA做好准备去加载和使用您的图像文件。

内容管线使用一个资源名称(asset name)来访问内容资源。另一种确认您的图像文件可以被内容管线识别的方法是查看新添加项目的属性,右键点击解决方案管理器中刚才添加的图像文件,选择属性,如图2-3.

clip_image008

图2-3 图像文件的属性

如图所示,在图2-3中,您添加的logo.png文件默认的资源名是logo或者说就是是不包括扩展名的图像文件名,默认情况下资源名都是这样命名的。

如果您可以在属性窗口中看到Asset Name属性,就说明内容管线可以识别您的图像文件。当然您可以改变资源名,项目中的资源名要求是唯一的,不过只有在同一个文件夹下才做这样的要求。这是在Content节点中使用子文件夹组织的另一个好处——您可以让多个资源拥有同样的资源名,只要它们存在于Content节点下不同的文件夹中。这个似乎是个只会把事情搞复杂的坏主意,但这实际上很常见而且很有用。举例来说,您有一个字体文件,一个特效文件,和一个图形文件,用作一个爆炸效果,那么如果把它们都命名为"Eexplosion"并分别放到所属类型的文件夹下会让事情变得容易,

您也许还注意到图2-3中Asset Name属性下面的两个属性:Content Importer和Conten Processor,它们被设置成Texture-XNA Framework,表明内容管线验证过您添加到项目中的图像文件:并且它们准备被内容管线作为纹理对象来使用。在计算机图形学中纹理指应用到3D物体表面的2D图像,在本书的3D部分我们会做这些。不过现在我们直接将这些纹理绘制到屏幕上。

加载并绘制精灵

现在您的解决方案中加载了一副图像并且能够被内容管道所识别,已经准备好将它绘制到屏幕上。不过在您能够在代码中访问它之前,您需要将资源从内容管道加载到变量中以便操作它们。

用来存储图像的缺省对象是Texture2D。在Game1.cs代码文件中的GraphicDeviceManager和SpriteBatch变量声明下面添加一个Texture2D变量:

 

现在,您需要将实际的图像文件加载到Texture2D变量中。为了访问内容管道中的数据,使用Game类的Content属性。Content属性是ContentManager类型,可以用来访问所有加载到内容管道中的对象。ContentManager类有一个Load方法可以让您将内容加载到不同类型的XNA对象中。

就像之前所说,所有内容资源的加载都在LoadContent方法中完成,所以添加以下代码到LoadContent方法中:

 

传入Content.Load方法的参数是图像文件的路径,根目录是解决方案中的Content节点。字符串前面加上@符号表示逐字字符串,忽略字符串中的转义序列,所以以下两行代码产生相同的字符串:

 

同样请注意参数中使用的资源名而不是文件名。

ContentManager类的Load方法是一个泛型方法,需要一个类型参数来指定您想要访问哪种类型的变量。目前的情况下,您正在处理一个图像文件并且期望返回一个Texture2D对象。

现在图像文件已经加载到texture变量中并可以使用了。XNA中所有的绘制工作都要在

 

这三行代码将图像绘制到屏幕的右上角。选择调试(Debug)->运行(Run),您将会看到和图2-4类似的画面。

clip_image011

图2-4 XNA logo图像出现在屏幕的左上角

让我们看看这三行代码,注意到的第一件事是三行代码都用到了一个叫spriteBatch的SpriteBatch类型对象。这个变量在您创建项目的时候被声明,然后在LoadContent方法中被初始化。

本质上这里发生的事情就是XNA用SpriteBatch对象的Begin和End调用告诉图形设备将要向它发送一个精灵(或2D图像)。在XNA游戏过程中图形设备会接收大量的数据,而且数据会有不同的格式和类型。无论何时您向图形设备输送数据,都要让它知道数据类型以便它正确进行处理。因为,您不能只是随意调用SpriteBatch.Draw多次,您首先要调用SpriteBatch.Begin告诉显卡精灵数据已经发送。

Draw方法有三个参数,如表格2-1所述。

表2-1 Draw方法参数

参数 类型 描述

Texture  Texture2D         持有您想要绘制的图像的Texture2D对象。

Position Vector2          您想要开始绘制图像的位置(2D坐标)。通常图像从左上角 开始绘制。

Color    Color            染色颜色。指定为White将不会为图像染色,否则将图像染色为 指定颜色。

______________________________________________________________________________

尝试改变一下Draw调用的参数--具体的说是位置和染色颜色参数。在2D中,XNA使用Vector2结构体来定义坐标。Vector2.Zero是将Vector2的X,Y坐标置零的简化方式(和new Vector2(0, 0)是一样的作用)。

在2D XNA游戏中,X,Y屏幕坐标(0,0)是屏幕的左上角,X轴正方向向右,Y轴正方向向下。

如果您想要使图像在游戏窗口中居中,您需要找到窗口的中心点并且适当的偏移左上角坐标。您可以通过访问Game类的Window.ClientBounds属性来获得窗口的尺寸。当游戏运行于窗口模式时,Window.ClientBounds.X和Window.ClientBounds.Y相当于游戏窗口的左上角坐标,而Window.ClientBounds的Width和Height值总是等于窗口的宽度和高度,在窗口或全屏模式下都是如此。在Xbox360和Zune上Window.ClientBounds.X和Y总是为0,而Width和Height属性总是等于画面的宽和高(因为Zune和Xbox360游戏总是运行于全屏模式)。

将Window.ClientBounds.Width和Window.ClientBounds.Height值除以2将得到屏幕中心点的坐标。要准确的将图像居中,需要将屏幕中心点坐标偏移图像宽和高的一半。因为传入Draw方法的位置参数并不代表绘制图像的中心,而是左上角。您可以通过Texture2D变量(texture)的Width和Height属性来获得图像的尺寸。将您的Draw调用用以下的代码替换来将图像居中:

 

透明度及其他

当往屏幕上绘制多个精灵的时候,您可以(而且也应该,处于速度的考虑)在一个SpriteBatch.Begin和SpriteBatch.End调用对中绘制尽可能多的精灵。实际上您应该做的是在游戏中用一个SpriteBatch对象然后在一个调用对绘制所有的2D图像。

就像您在图2-4中看到的那样,我使用的XNA logo有很难看的白色背景,如果它变为透明的话或许看起来要好很多。

有两种方法可以使图像部分透明:要么图像文件本身有透明的背景,要么图像中您希望透明的部分是洋红色(255, 0, 255)——XNA将自动将洋红色渲染成透明的。透明度可以存储到某些文件格式中(如.png文件)用作alpha通道。这些格式不仅仅包含RGB值,每个像素有一个额外的alpha通道(RGBA中的A)来确定像素的透明度。

如果您有一个图像处理工具,您可以用它为您的图像创建透明背景。Paint.Net是一个用.NET编写的很棒的免费图像处理工具,您可以从http://www.getpaint.Net 获得它。

接下来的例子中我将使用另一个带透明背景的XNA Logo,也包含在本部分的源码中,文件名是logo_trans.Png。

用同样的方法将透明XNA Logo图像文件加入到您的项目中:右键点击解决方案管理器中的Content/Images节点,选择添加(Add)->现有项(Existing Item),然后导航到第2章的源码BasicSprite的文件夹,logo_trans.Png在BasicSprite/Collision/Content/Images文件夹下。不要移除您项目中的另一个XNA Logo文件,因为您还要用它做多图像绘制试验。

在您将透明logo加入到项目中之后,为新的logo添加另一个Texture2D成员变量:

1clip_image002[8]Texture2D textureTransprent;

注意新加入图像文件的资源名,并用它来把图像文件加载到textureTransparent变量中。记住加载图像到变量中是使用Content.Load方法。在LoadContent方法中做这些,加入以下代码:

1clip_image002[9]textureTransparent = Content.Load(@"Images/logo_trans");

然后复制之前的SpriteBatch.Draw,另起一行粘贴,将其改为使用textureTransparent变量,改变位置坐标为屏幕中心点,这样两个图像就交错在一起了。

改变背景色为Color.Black使得透明效果比较突出(做到这一点要修改Draw方法中Clear调用的颜色参数)。

现在Game1类看起来应该像这样:

 

编译并运行游戏(调试->运行),您会看到两个图像重叠在一起—— 一个有透明的背景,一个有白色的背景,如图2-5.

clip_image013

图2-5 两个精灵,一个有透明背景,另一个没有

还有一些其他的绘制选项值得一提。首先,用一个重载的SpriteBatch.Draw方法可以很容易实现图像的翻转和缩放。为了试验这些选项,将第二个SpriteBatch.Draw调用修改成这样:

 

重载函数的参数列在表2-2中。

表2-2 重载Draw方法的参数

参数 类型 描述

Texture Texture2D 要绘制的纹理。

Position Vector2 绘制图像的左上角坐标。

SourceRectangle Rectangle 允许您绘制原始图像的一部分,这里使用null。

Color Color 染色颜色。

Rotation float 旋转图像,现在使用0。

Origin Vector2 指定旋转的参照点。现在使用Vector2.Zero。

Scale float 缩放比例,使用1代表按照原始尺寸绘制,1.5f表示放大图 像到150%。

Effects SpriteEffects 使用SpriteEffects枚举来垂直或水平翻转图像。

LayerDepth float 允许您指定图像的层叠次序(哪张图像在其他图像之上。现 在使用0.

_______________________________________________________________________________

现在您修改了第二个Draw将图像放大到原始尺寸的150%(1.0f = 100%)。您还使用SpriteEffects.FlipHorizontally将图像水平翻转。编译并运行程序,您将会看到第二个XNA logo水平翻转过来并且比另一个XNA logo稍微大一点(图2-6)。

clip_image015

图2-6 第二个XNA logo被翻转并放大。

层深度

您可能注意到第二个XNA logo重叠在原来的logo上。默认情况下,XNA会在之前绘制的图像上面绘制新的图像,但是您可以改变图像在屏幕上的层叠次序。图像层叠的次序和图像Z次序有关,或者说层深度。

虽然您现在可能不在意哪个XNA logo在上面,但有时您需要某些图像总是在其他图像的上面。举个例子,在大多数游戏中,您希望角色在所有的背景图像之上移动。有一种方法是把您想要放在顶层的图像最后绘制。这个方法可行,但是随着您游戏中图片的增多,组织Draw调用达到您想要的结果将会是非常痛苦的事情。

谢天谢地,XNA可以让您为每个图像指定一个层深度使图像总是能有正确的Z次序。要修改层深度,您需要将两个SpriteBatch.Draw都换成之前例子中的重载版本。下面将您的第一个Draw调用改成下面这样:

 

这段代码将会用和修改之前的Draw调用相同的方法绘制第一个精灵,因为您没有给额外的参数传递除了默认值外的任何值。

不过这个重载函数的最后一个参数能够接受一个值作为层深度。层深度参数由一个0~1的float值表示。0相当于Z次序的最前面,1表示Z次序的最后面。

如果您改变了层深度参数然后运行程序,会发现运行结果没有任何改变。这是因为您需要告诉XNA希望按照图像的层深度来绘制图像,为了让XNA使用图像的层深度,您需要SpriteBatch.Begin方法的另一个重载版本。

到目前位置您都是使用无参数SpriteBatch.Begin版本。为了使用层深度对您的图像进行排序,要使用带一个SpriteSortMode类型参数的SpriteBatch.Begin方法。这里最好的选择是使用一个带三个参数的SpriteBatch.Begin重载,如表2-3所示:

表2-3 SpriteBatch.Begin重载方法参数列表

参数 描述

SpriteBlendMode   决定精灵颜色怎样和背景色混合,有三个模式:

•None:不进行颜色混合。

•AlphaBlend:使用alpha值进行混合。这是默认模式,并开启透明效果,像之前提到的,如果您有带透明背景的图像,就应该使用 AlphaBlend。

•Additive:将精灵颜色和背景颜色进行混合。

SpriteSortMode   定义渲染精灵的排序模式,有五个模式:

•Deferred:精灵不会被绘制直到SpriteBatch.End被调用。然后End以它们被调用的次序送到图形设备中。在这个模式下,

操作多个SpriteBatch对象时可以让Draw调用不会产生冲突。这是默认模式。

•Immediate:Begin调用会立即设置图形设备,Draw调用会立即进行绘 制。同一时间只能有一个SpriteBatch对象被使用。

这是最快的模式。

•Texture: 和Deferred模式一样,但是精灵在绘制之前按照纹理进行排 序。

•BackToFront:和Deferred模式一样,不过精灵按照层深度参数从前往后 排序。

•FrontToBack:和Deferred模式一样,不过精灵按照层深度从后往前排序。

SaveStateMode 定义保存图形设备状态的模式,有两个模式:

•None:不保存设备状态。

•SaveState:图像设备状态的状态在Begin调用后被保存,在End调用后 被恢复。

_______________________________________________________________________

修改您的SpriteBatch.Begin为包含这三个参数的版本。因为您的一个图像使用了透明度,设置第一个参数为SpriteBlendMode.AlphaBlend,然后设置第二个参数为SpriteSortMode.FrontToBack。这个模式将精灵按照它们在Draw调用中被指定的层深度以从前往后的顺序绘制,层深度值小的精灵排在层深度值大的精灵之上。

您需要做的最后一件事情就是将两个Draw调用中的最后一个参数修改成不同的值,记住只能使用0~1范围之内的浮点值。因为您的排序模式是FrontToBack,拥有较小深度值的物体会先绘制。保持第一个Draw中的最后一个参数值为0,然后修改第二个Draw调用中的最后一个参数值为1。现在Draw方法看起来应该像这样:

 

运行您的程序,透明背景的图像仍然在不透明的图像之前。下一步,将两个图像的层深度值交换,现在透明图像就会在不透明图像之后了。

然后试试不同的排序模式,渲染模式和层深度,把握它们在不同的情况下所起的作用。

开始动吧

在不同的排序模式和层深度下绘图的确比较有趣,但是确实不那么激动人心。现在,我们让两幅图像动起来并在碰到屏幕边缘时反弹回来。要移动图像,您需要改变图像绘制的位置,而现在两幅图像的位置都是始终不变的,一幅在窗口的正中,另一幅在窗口正中稍稍往右下偏移的位置。

clip_image017

本章这部分的代码在随书代码的/Chapter2/MovingSprite文件夹中。

要在屏幕上移动物体,您需要在帧与帧之间修改物体的位置。因为,您要做的第一件事就是用一个位置变量来代替先前代码中的常量。为Game1类顶部添加两个Vector2类型的成员变量(称为pos1和pos2),并初始化为 

您还需要为每个精灵添加一个速度变量。这个变量用来决定每个精灵在帧间的移动距离。添加两个float型的变量(称为speed1和speed2)到您刚刚添加的位置变量下:

 

现在分别用pos1和pos2取代两个Draw方法中的位置常量。将第二个Draw调用中的SpriteEffects参数设为SpriteEffects.None,然后将缩放系数(倒数第三个参数)从1.5f改为1.0f。这将移除先前的图像翻转和放大效果。

现在两个Draw调用看起来应该像这样:

 

编译并运行程序,现在两个精灵都被绘制到窗口的左上角,您需要做的就是让精灵动起来。

当Game1类中的Draw方法负责绘制的时候,所有物体的更新操作(包括位置,速度,碰撞检测,人工智能算法,等等)都应该在Update方法中进行。

为了更新物体的位置,您需要修改pos1和pos2的值。用以下代码替换调Update方法中的TODO注释:

 

这里没有什么太复杂的东西。您用pos1.X加上速度值更新了X坐标。接下来的if语句判断更新后的精灵位置是否处于屏幕的左边缘或右边缘之外,如果是,那么将speed1乘以-1,结果是反转精灵的移动方向。对另一个精灵也做同样的处理,只不过相应变量换成pos2.Y和speed2。

编译并运行程序,您将看到两个精灵都开始移动,一个水平移动,一个垂直移动,当它们触及屏幕边缘时会反弹回来。如图2-7所示:

clip_image019

图2-7 没有什么像活动,弹跳的XNA logo能让人兴奋了。

动画

坐下来并且越来越“入迷”的观看移动,弹跳的XNA logo们并不是您读本书的真正能原因。现在我们让精灵产生动画以使事情更加有趣一点。

clip_image017[1]

这部分的代码在/Chapter2/AnimatedSprite/文件夹下。

像在本章稍早的时候谈到的,2D XNA游戏中的动画很像卡通活页书。动画由许多单独的图像组成,然后快速切换产生动画效果。一般来说,精灵动画序列是保存在一个图像文件中的,然后您取出单独的精灵然后以特定的次序绘制它们。这样的图像文件称为精灵图(Sprite sheets),这章的源码中包含了一张精灵图的示例,在AnimatedSprite/AnimatedSprite/Content/Images/文件夹下。文件名为threerings.png,如图2-8:

译注:Sprite sheets本文译作精灵图,表示包含若干精灵的一张大图。

clip_image021

图2-8 精灵图示例(threerings.png)

在之前的每个例子中,您都是将一张图像文件加载到Texture2D对象中然后绘制整张图像。使用精灵图您需要能够将整个图加载到Texture2D对象中,然后取出单独的精灵帧来绘制动画。之前例子中您使用的SpriteBatch.Draw重载版本有一个参数(第三个)允许您指定一个源矩形,使原图只有这部分被绘制。到目前为止您都是为该参数指定null值来告诉XNA绘制整幅图像。

让我们新建一个项目来实现动画效果(文件(File)->新建(New)->项目(Project)...),在新建项目窗口,选择左边窗口中的Visual C#->XNA Game Studio 3.0节点后,在右边项目模板窗口选择Windows Game(3.0, 将项目命名为AnimatedSprites。

项目建好后,为解决方案管理器中的Content节点建立一个名为Images的子文件夹(在解决方案管理器中的Content节点上右键->新建->新建文件夹)。接着您要讲之前图2-8中的图像文件添加到项目中,在解决方案管理器中的Content/Images文件夹上右键->添加->现有项...,导航到threerings.png所在的目录(Chapter2/AnimatedSprites/AnimatedSprites/Content/Images)。使用与之前相同的方法将图像文件加载到Texture2D对象中。首先,为Game1类添加一个成员变量:

 

然后在Game1类的LoadContent方法中添加以下代码:

 

现在您已经加载精灵图到Texture2D对象中,可以开始想办法弄明白怎样在精灵图上轮流获得单独的精灵,为了编写这样的一个算法,您需要知道一些事情:

• 精灵图中每个单独图像(或称为帧)的宽和高(frameSize)。

• 精灵图的行与列的总数(sheetSize)。

• 将要绘制的图像在精灵图中所在行与列的索引(currentFrame)。

就现在这张精灵图而言,长与宽都为75像素,有8行6列,而且您将从第一帧开始绘制。往Game1类中添加一些成员变量来反映这些数据:

 

对于这几个变量Point结构体都可以工作得很好,因为它们都需要一种能表示2D坐标的类型(X和Y位置)。

现在您可以添加SpriteBatch.Draw调用了。您将使用之前几个例子中的Draw版本,有一点不同的是您需要为第三个参数传递一个基于currentFrame和frameSize的源矩形。以下代码可以做到这一点,将它们添加到Game1类的Draw方法中的base.Draw之前:

 

如果你对构造源矩形的逻辑有些不清楚,那么像这样想想看:用一个基于0的当前帧——意味着您初始化currentFrame为(0, 0)而不是(1, 1),换句话说,精灵图左上角的精灵索引表示为(0,0)而不是(1,1)——当前帧的左上角X坐标总是等于当前帧索引的X值(currentFrame.X)乘以当前帧的宽度。同样的,当前帧左上角的Y坐标总是等于当前帧索引的Y值(currentFrame.Y)乘以当前帧的高度。

在这里源矩形的宽度和高度值总是相同的,所以您可以使用frameSize.X和frameSize.Y代表源矩形的宽和高。

接下来,修改Game1类的Draw方法中的GraphicsDevice.Clear中的颜色值为Color.White,使背景填充色为白色。然后编译并运行程序,您应可以看到精灵图中的第一个精灵被绘制到了游戏窗口的左上角,如图2-9:

精灵现在还没有动画效果,因为您不停的在绘制精灵图中的第一个精灵。为了产生动画,您需要更新currentFrame索引去轮流绘制精灵图中的精灵。那么应该把从当前帧索引移动到下一帧的代码放到哪呢?记住在Draw方法中完成绘制,在Update方法中完成更新。因此将下面代码添加到Update方法的base.Update调用之前:

 

这段代码所做的就是使currentFrame索引的X值增加1,然后检查这个值是否大于等于精灵图的列数,如果大于列数,就将值置零,并使索引的Y值增加1,开始绘制下一行的精灵。最后,如果Y值超过了精灵图的行数,将它置零使整个动画序列回到起点。编译并运行程序,您应该可以看到3个圆环的图像在窗口的左上角旋转这,如图2-9。

clip_image023

图2-9 三个旋转的环...没什么比这个更好啦!

现在是时候瞧瞧您在XNA中努力的成果了。

尽管旋转的环并不是下一个“伟大的”游戏,但是它看起来真的不赖,对吧?并且您应该开始感觉到XNA是如何的易用和强大。

就像您看到的,轮流绘制精灵图中的精灵相当容易实现任何一种可以用精灵图格式表现的动画。

调整帧速率

尽管三环动画看起来相当不错,但可能有时您会觉得动画的速度太快或者太慢而想要去改变它。

前面我提到过帧速率的概念,这里顺便提一下:帧速率表示一秒钟游戏重绘场景的次数。在XNA中,帧速率默认为60fps。除非您在一台非常慢的机器上运行这个程序,否则您很有可能看到动画以60fps绘制。

还有一种不同类型的帧速率,和单独的动画相关,这种帧速率(通常称为动画速度)反映了给定动画帧序列绘制一次的速度,或者说一秒钟绘制的动画帧数。目前您的动画的速度是60fps,因为您在每次重绘场景的时候绘制一幅精灵图中的图像(每秒60帧)。

有几种方法可以改变您三环动画的速度。XNA的Game类有一个叫做TargetElapsedTime的属性用来告诉XNA在每次Game.Update调用之间要等待多久。本质上这个属性表示每个帧之间的时间间隔。默认情况下这个值被设为1/60秒,也就是帧速度为60fps。

要改变您程序的帧速率,添加以下代码到Game1类构造函数的末尾:

 

这个告诉XNA每50微妙调用一次Game.Update,相当于帧速率20fps。编译游戏并运行它,您会发现动画以低得多的速度运行。在TimeSpan构造函数中尝试不同的值(比如说,1毫秒)来看看动画循环的速度。

理想的情况是您应该保持帧速率在60fps左右,就是说可以不用管默认帧数。为什么60fps是个标准呢?这是显示器或电视机不会让人眼察觉到闪烁的最低刷新率。

如果您将帧速率调得太高,XNA不保证您能获得期望的性能。GPU的速度,处理器的速度,您消耗的资源和代码的效率决定了您的游戏是否能达到最好的性能。

幸运的是,XNA提供了一种方法来检测您的游戏是否存在性能问题。Update和Draw方法都有的GameTime对象参数,有一个叫做IsRunningSlow的布尔类型的属性。您能在任何时候在这两个方法中检查IsRunningSlow的值。如果值为true,XNA不能跟上您指定的帧速率。在这种情况下,XNA会进行跳帧尽力达到您期望的速率。这也许不是您愿意在任何游戏中看到的结果。所以如果出现这样的情况,您或许应该提醒用户她的机器配置运行您的游戏很困难。

调整动画速度

尽管调整游戏本身的帧速率可以影响动画的速度,但是这样做并不是理想的方法。为什么呢?当您改变了游戏的帧速率,将会影响到所有精灵的动画速度,比如移动物体的速度之类。如果您希望一个动画的速度为60fps而另一个为30fps,您就不应该通过改变整个游戏的帧速率来实现。

移除之前修改TargetElapsedTime的代码,让我们试试其他的途径。

当调整一个精灵动画的速度的时候,一般您应该只针对该精灵做这样的调整。这可以通过只在指定时间过后才移动到精灵图的下一帧的方法达到目的。要做到这个,添加两个用来追踪动画帧之间时间间隔的成员变量:

 

timeSinceLastFrame变量用来追踪自上一帧之后经过了多少时间。millisecondsPverycdFrame变量用来指定在移动当前帧索引之前您想要等待多长时间。

动画帧的实际循环发生在Update方法中。所以下一步就是检查动画帧之间的时间间隔然后在指定时间达到的时候运行移动索引到下一个动画帧的代码。修改之前添加到Update方法中的代码为下面这样(修改的部分用粗体表示):

 

就像您看到的,这里使用gameTime.ElapsedGameTime属性来测定自上一帧之后经过了多少时间。这个属性表示上一次调用Update方法后经过的时间。您用timeSinceLastFrame变量加上这个时间增量,当变量的值大于您想要在帧间等待的时间后,进入到if语句中,用timeSinceLastFrame减去millisecondsPerFrame来调整timeSinceLastFrame的值,然后移动到下一个动画帧。

编译并运行游戏,您将会看到圆环动画速度变慢。要注意到最重要的是圆环动画的运行帧速率(20fps)不同于游戏的帧速率(60fps)。用这个方法您可以使任意数量的动画在不同的帧速率运行而不用牺牲游戏整体的帧速率。

刚刚您做了些什么

现在是时候做适时的停留,因为您现在已经知道如果在XNA中实现2D动画了!让我们花几分钟回顾一下您在本章学习到的东西:

•您研究了XNA游戏场景之下发生的事情,包括XNA程序流和XNA游戏循环。

•您在屏幕上绘制了第一个精灵。

•您学习了一些内容管线的知识和它的用途。

•您在屏幕上移动了一个精灵。

•您接触到了精灵的透明度,垂直翻转和其他一些操作。

•您用不同的Z次序绘制了精灵。

•您使用精灵图绘制了精灵动画。

•您调整了游戏的帧速率。

•您调整了单独的精灵动画速度。

摘要

•当您创建了一个XNA项目,它内建有一个游戏循环和程序流程。游戏循环由Update/Draw调用组成。程序流程包含初始化,加载内容资源(LoadContent)和执行特别卸载操作(UnloadContent)几个步骤。

•要在屏幕上绘制一个精灵,您需要一个Texture2D对象来在内存中保存精灵。内容管线在编译期将精灵编译成内部格式来准备好绘制。然后您使用一个SpriteBatch对象将精灵绘制到屏幕上。

•所有的绘制操作都应该在SpriteBatch.Begin和SpriteBatch.End调用对之间完成。这些调用通知图形设备精灵信息已经送到显卡中。Begin方法有些重载的版本让您能够改变处理透明度的方式和精灵的排序方式。

•XNA游戏默认的帧速率是60fps。改变这个值将影响游戏中所有动画的速度。

•为了改变单独的精灵动画速度,您可以设置一个计数器来追踪自上一帧后经过的时间,并仅在经过了X毫秒的时间后才更新帧。

•XNA每16毫秒绘制一帧,这和Chuck Norris的拳速比起来算不上什么。平均上Chuck的拳击每4毫秒给予对手一次致命伤害。

==============================================================================

知识问答:

1.XNA游戏循环的有哪几步?

2.如果您想要加载一个Texture2D对象,需要在哪个方法中做这件事?

3.要把XNA游戏的帧速率改为20fps应该用怎样的代码?

4.在加载一个Texture2D对象时,您应该向Content.Load传递什么参数?

5.真还是假:如果您添加到项目中的内容管线不能解析,它会在编译期告诉您。

6.您绘制一个精灵,并且想要背景为透明。需要哪些步骤?

7.您有两个精灵你给(A和B),当它们互相碰撞时您总希望A在B之上绘制,您应该怎么做?

8.在循环遍历一个精灵图的时候,您需要追踪哪些变量?

9.在美国,哪个月是“全国山葵月”?

练习

1.在这一章中,您开发了一个两个XNA logo在屏幕上移动并在边缘反弹的例子。现在把本章末尾的动画动画例子改成和它一样的移动和反弹方式--不过这一次要让动画精灵同时沿着X轴和Y轴移动并且在屏幕的四个边缘反弹。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值