裴小星的博客

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

层深度

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

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

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

spriteBatch.Draw(texture,
	new Vector2(
	(Window.ClientBounds.Width / 2) - (texture.Width / 2),
	(Window.ClientBounds.Height / 2) - (texture.Height / 2)),
	null,
	Color.White,
	0,
	Vector2.Zero,
	1,
	SpriteEffects.None,
	0);

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

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

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

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

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

参数描述
SpriteSortMode

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

  • Deferred:精灵不会被绘制直到SpriteBatch.End被调用.然后End以它们被调用的次序送到图形设备中.在这个模式下,操作多个SpriteBatch对象时可以让Draw调用不会产生冲突.这是默认模式.

  • Immediate:Begin调用会立即设置图形设备,Draw调用会立即进行绘制.同一时间只能有一个SpriteBatch对象被使用.这是最快的模式.

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

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

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

BlendState

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

  • None:不进行颜色混合.

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

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

  将SpriteBatch.Begin方法修改为为包含这两个参数的版本。设置第一个参数为SpriteSortMode.FrontToBack。这个模式将根据它们在Draw调用中被指定的层深度来绘制交错的精灵,层深度值接近0的精灵将排在层深度值接近1的精灵之上。因为这是一个浮点值,您拥有大量可供选择的层深度值(任何有效的在0.0和1.0之间的浮点数)。接着,因为其中的一张图片带有透明度信息,故设置第二个参数为BlendState.AlphaBlend。

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

protected override void Draw(GameTime gameTime)
{
	GraphicsDevice.Clear(Color.Black);
	// Begin drawing all sprites
	spriteBatch.Begin(
SpriteSortMode.FrontToBack, BlendSate.AlphaBlend);
	spriteBatch.Draw(texture,
		new Vector2(
		(Window.ClientBounds.Width / 2) - (texture.Width / 2),
		(Window.ClientBounds.Height / 2) - (texture.Height / 2)),
		null,
		Color.White,
		0,
		Vector2.Zero,
		1,
		SpriteEffects.None,
		0);
	spriteBatch.Draw(textureTransparent,
		new Vector2(
		(Window.ClientBounds.Width / 2),
		(Window.ClientBounds.Height / 2)),
		null,
		Color.White,
		0,
		Vector2.Zero,
		1.5f,
		SpriteEffects.FlipHorizontally,
		1);
	spriteBatch.End( );
	base.Draw(gameTime);
}

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

  然后试试不同的排序模式和混合模式,感受一下它们在不同的情况下所起的作用。

让我们动起来

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

请注意

  本章这部分代码在随书源码的第3章名为MovingSprite的文件夹中。

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

Vector2 pos1 = Vector2.Zero;
Vector2 pos2 = Vector2.Zero;

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

float speed1 = 2f;
float speed2 = 3f;

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

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

spriteBatch.Draw(texture,
	pos1,
	null,
	Color.White,
	0,
	Vector2.Zero,
	1,
	SpriteEffects.None,
	0);

spriteBatch.Draw(textureTransparent,
	pos2,
	null,
	Color.White,
	0,
	Vector2.Zero,
	1,
	SpriteEffects.None,
	1);

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

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

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

pos1.X += speed1;
if (pos1.X > Window.ClientBounds.Width - texture.Width ||
  pos1.X < 0)
	speed1 *= -1;

pos2.Y += speed2;
if (pos2.Y > Window.ClientBounds.Height – 
  textureTransparent.Height || pos2.Y < 0)
	speed2 *= -1;

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

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


图3-7 没有什么比活动、弹跳的XNA LOGO更让人兴奋的了


动画

  尽管坐下来欣赏XNA标志图像的移动和弹跳是件令人沉迷的事,但这并不是您阅读这本书的真正目的。让我们来做点更有趣的事情:制作精灵动画。

请注意

  本章节的此部分代码需要用到第三章的源代码,这部分的代码名称叫做AnimatedSprites。

  就像本章之前所探讨的那样,2D XNA游戏中的动画的制作过程很像卡通手翻书。动画制作包含着大量的独立图像,通过在一个周期内图像间的快速切换来使它们显示为动画。

  通常精灵动画存放在图片文件中,您需要用某种顺序把图片上独立的图像提取出来,然后绘制在屏幕上。本书把这些图片文件称为“精灵位图(Sprite Sheet)”。精灵位图的示例可以在本章的源代码中找到,位于AnimatedSprites\AnimatedSprites\AnimatedSpritesContent\Images文件夹下,文件名为threerings.png,如图3-8所示。

  在之前的每个示例中,您都是将一张图像文件加载到Texture2D对象中然后绘制整张图像。如果使用精灵位图的话,您需要能够将整张图片加载到Texture2D对象中,然后在动画循环中取出单独的精灵帧来绘制动画。之前例子中您使用的SpriteBatch.Draw重载版本有一个允许您指定源矩阵的参数,使原图只有这部分被绘制。到目前为止您都将该参数指定null,这会使XNA绘制完整的Texture2D图像。

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

  创建完项目后,就在解决方案资源管理器的AnimatedSpritesContent项目下通过右键单击项目名称并选择Add→New Folder来创建一个子文件夹,命名为Images。下一步,您需要通过在解决方案资源管理器中右键单击新建的Content\Images文件夹并选择Add→Existing Item…向您的项目中导入上文中图 3-8的图像。目标文件指向之前下载的本书中第三章源代码附带的threerings.png(图像文件位于AnimatedSprites\AnimatedSprites\AnimatedSpritesContent\Images文件夹下)。

  就像您之前载入其它的图像那样,将此图像载入Texture2D对象。首先,在您的Game1类中增加一个类成员变量:

Texture2D texture;

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

texture = Content.Load<Texture2D>(@"images\threerings");

  现在您已经将精灵位图加载到Texture2D对象中,可以开始考虑如何在精灵位图上轮流获得独立的精灵帧了。为了编写这样的一个算法,您需要了解以下信息:

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

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

  • 指示接下来精灵位图中将要绘制的精灵帧在精灵位图中所处的行与列的位置的索引(currentFrame)。


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


  在本例的精灵位图中,长与宽都为75像素,有8行6列,而且您将从第一帧开始绘制。往Game1类中添加一些类成员变量来表示这些数据:

Point frameSize = new Point(75, 75);
Point currentFrame = new Point(0, 0);
Point sheetSize = new Point(6, 8);

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

  现在您可以添加SpriteBatch.Draw调用了。您将使用与之前几个例子中相似的版本,唯一的不同点是:不再向源矩阵传递null值,而是为第三个参数传递一个基于currentFrame和frameSize的源矩形。以下代码可以实现这一点,将它们添加到Game1类的Draw方法中,位于base.Draw之前:

spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend);
 spriteBatch.Draw(texture, Vector2.Zero,
		new Rectangle(currentFrame.X * frameSize.X,
		currentFrame.Y * frameSize.Y,
		frameSize.X,
		frameSize.Y),
	Color.White, 0, Vector2.Zero,
	1, SpriteEffects.None, 0);

spriteBatch.End();

  如果您对构造源矩形的逻辑有些不清楚,不妨以这样的方式进行思考:用一个基于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,使背景填充色为白色。然后编译并运行程序,您应可以看到精灵位图中的第一个精灵帧被绘制到了游戏窗口的左上角。

  精灵现在还没有动画效果,因为您一直重复地绘制着精灵位图中的第一帧。为了产生动画,您需要更新currentFrame索引来循环遍历精灵位图中的每一帧。那么应该把从当前帧索引移动到下一帧的代码放到哪呢?记住,在Draw方法中完成绘制,在Update方法中完成更新。因此请将下面代码添加到Update方法中,位于在base.Update的调用之前:

++currentFrame.X;
if (currentFrame.X >= sheetSize.X)
{
	currentFrame.X = 0;
	++currentFrame.Y;
	if (currentFrame.Y >= sheetSize.Y)
	currentFrame.Y = 0;
}

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

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

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

  就像您看到的那样,循环绘制精灵位图中的精灵帧可以很轻松地实现任何一种可以用精灵位图格式表现的动画。


图3-9 三个旋转的环…没有比这更棒的了!

posted on 2011-01-14 20:51  裴小星  阅读(1168)  评论(0编辑  收藏  举报