译者:林公子
出处:木木的二进制人生
转载请注明作者和出处,谢谢!
第二章 精灵的乐趣(2)
透明度及其他
当往屏幕上绘制多个精灵的时候,您可以(而且也应该,处于速度的考虑)在一个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成员变量:
注意新加入图像文件的资源名,并用它来把图像文件加载到textureTransparent变量中。记住加载图像到变量中是使用Content.Load方法。在LoadContent方法中做这些,加入以下代码:
然后复制之前的SpriteBatch.Draw,另起一行粘贴,将其改为使用textureTransparent变量,改变位置坐标为屏幕中心点,这样两个图像就交错在一起了。
改变背景色为Color.Black使得透明效果比较突出(做到这一点要修改Draw方法中Clear调用的颜色参数)。
现在Game1类看起来应该像这样:
编译并运行游戏(调试->运行),您会看到两个图像重叠在一起—— 一个有透明的背景,一个有白色的背景,如图2-5.
图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)。
图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方法看起来应该像这样:
运行您的程序,透明背景的图像仍然在不透明的图像之前。下一步,将两个图像的层深度值交换,现在透明图像就会在不透明图像之后了。
然后试试不同的排序模式,渲染模式和层深度,把握它们在不同的情况下所起的作用。
开始动吧
在不同的排序模式和层深度下绘图的确比较有趣,但是确实不那么激动人心。现在,我们让两幅图像动起来并在碰到屏幕边缘时反弹回来。要移动图像,您需要改变图像绘制的位置,而现在两幅图像的位置都是始终不变的,一幅在窗口的正中,另一幅在窗口正中稍稍往右下偏移的位置。
本章这部分的代码在随书代码的\Chapter2\MovingSprite文件夹中。
要在屏幕上移动物体,您需要在帧与帧之间修改物体的位置。因为,您要做的第一件事就是用一个位置变量来代替先前代码中的常量。为Game1类顶部添加两个Vector2类型的成员变量(称为pos1和pos2),并初始化为Vector2.Zero:
您还需要为每个精灵添加一个速度变量。这个变量用来决定每个精灵在帧间的移动距离。添加两个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所示:
图2-7 没有什么像活动,弹跳的XNA logo能让人兴奋了。
动画
坐下来并且越来越“入迷”的观看移动,弹跳的XNA logo们并不是您读本书的真正能原因。现在我们让精灵产生动画以使事情更加有趣一点。
|
这部分的代码在\Chapter2\AnimatedSprite\文件夹下。
像在本章稍早的时候谈到的,2D XNA游戏中的动画很像卡通活页书。动画由许多单独的图像组成,然后快速切换产生动画效果。一般来说,精灵动画序列是保存在一个图像文件中的,然后您取出单独的精灵然后以特定的次序绘制它们。这样的图像文件称为精灵图(Sprite sheets),这章的源码中包含了一张精灵图的示例,在AnimatedSprite\AnimatedSprite\Content\Images\文件夹下。文件名为threerings.png,如图2-8:
译注:Sprite sheets本文译作精灵图,表示包含若干精灵的一张大图。
图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。
图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轴移动并且在屏幕的四个边缘反弹。