Windows Phone 7范例游戏Platformer实战8——精灵动画的绘制实现
Windows Phone 7范例游戏Platformer实战1——5大平台支持
Windows Phone 7范例游戏Platformer实战2——游戏设计初步
Windows Phone 7范例游戏Platformer实战3——游戏资源和内容管道
Windows Phone 7范例游戏Platformer实战4——冲突检测的实现
Windows Phone 7范例游戏Platformer实战5——多点触控编程
Windows Phone 7范例游戏Platformer实战6——加速度传感器解读
Windows Phone 7范例游戏Platformer实战7——简单动画的绘制实现原理
本文参考了木木二进制翻译的Learning XNA 3.0文章,以及CSDN上的 XNA基础一文。非常感谢他们做出的杰出贡献。
在上一小节介绍了宝石的简单动画编程后,现在我们开始学习如何实现一个真正的动画,也就是如何将一连串的图片形成连贯的动作。下面是僵尸怪的精灵图片,我们可以看到它包含10个动作,在XNA中正是将这些动作连贯起来才形成我们看到的僵尸鬼的跑动。本小节我们就是围绕如何将该图片实现为僵尸怪的跑动展开的。
上面的10个小图片表现了僵尸怪跑动的所有动作,在实际制作时,我们通常将这些图片按顺序制作在一张大图中,并且保证大图中每个小图的尺寸是完全一样的。我们称这样的大图为精灵帧序列图Sprite Sheets。
上节我们提过,我们是使用循环的方式实现游戏界面的绘制,因此我们可以对包含这10个动作的精灵图片进行解析,在用户视角停留范围内绘制出僵尸怪的分解动作就可以了。那么我们如何实现这10个动作的分解呢,这就需要程序来进行解析了。
首先我们需要将僵尸怪精灵图片加载到内容管道中,因此一个Texture2D纹理对象是必不可少的。此外,为了让僵尸怪跑动的连贯,因此每帧显示的时间也是要考虑的,这个值要选择的符合游戏的呈现效果,太小的话动画效果好像一闪而过,太大有让用户觉得僵尸怪被卡死。
我们在僵尸怪精灵图片看到一共有10个动作,而这个精灵图片的大小为640*64像素,也就是说每个动作的宽度为64,高度也为64像素。
我们知道动画存在几种可能,一种就是当动画解析到最后一个动作时直接停止。一种就是当最后一个动作解析完毕又从头开始播放,这两种也是我们Platformer游戏中使用的动画呈现方式。这些都清楚了,那么我们就可以很轻松实现一个对精灵图片进行解析的动画类了。下面是动画类Animation的完整代码:
2 {
3 /// <summary>
4 /// 动画中的所有帧均按水平排列,僵尸怪的动画一共10帧
5 /// </summary>
6 public Texture2D Texture
7 {
8 get { return texture; }
9 }
10 Texture2D texture;
11
12 /// <summary>
13 /// 每帧显示的时间,
14 /// </summary>
15 public float FrameTime
16 {
17 get { return frameTime; }
18 }
19 float frameTime;
20
21 /// <summary>
22 /// 动画解析到最后一帧时,是否从头开始再次播放
23 /// </summary>
24 public bool IsLooping
25 {
26 get { return isLooping; }
27 }
28 bool isLooping;
29
30 /// <summary>
31 /// 获得帧数,使用僵尸怪精灵图片的宽度/每帧的宽度即可得出,这里为10
32 /// </summary>
33 public int FrameCount
34 {
35 get { return Texture.Width / FrameWidth; }
36 }
37
38 /// <summary>
39 /// 动画中每帧的宽度
40 /// </summary>
41 public int FrameWidth
42 {
43 // Assume square frames.
44 get { return Texture.Height; }
45 }
46
47 /// <summary>
48 /// 动画中每帧的高度
49 /// </summary>
50 public int FrameHeight
51 {
52 get { return Texture.Height; }
53 }
54
55 /// <summary>
56 /// 动画类构造函数
57 /// </summary>
58 public Animation(Texture2D texture, float frameTime, bool isLooping)
59 {
60 this.texture = texture;
61 this.frameTime = frameTime;
62 this.isLooping = isLooping;
63 }
64 }
OK,这一切都完成之后,我们还需要对动画进行不同类型的播放控制。比如说游戏程序检测到僵尸怪动画从未播放过,那么好,动画就第一帧开始。如果动画已经在播放,那么动画从上个动作继续进行绘制和解析。下面就是判定动画是否已经播放的代码
{
//如果动画已经在运行,无需重新开始
if (Animation == animation)
return;
// 开始新的动画
this.animation = animation;
this.frameIndex = 0;
this.time = 0.0f;
}
前面我提到过帧速率的概念,这里再次说明一下:帧速率表示一秒钟游戏重绘场景的次数。在WP7的XNA中,帧速率默认为30fps。一般来说达到了30fps就可以在WP7获得较好的画面流畅度了。
还有一种不同类型的帧速率,和单独的动画相关,这种帧速率(通常称为动画速度)反映了给定动画帧序列绘制一次的速度,或者说一秒钟绘制的动画帧数。
有几种方法可以改变僵尸怪动画的速度。XNA的Game类有一个叫做TargetElapsedTime的属性用来告诉XNA在每次Game.Update调用之间要等待多久。本质上这个属性表示每个帧之间的时间间隔。WP7上默认情况下这个值被设为1/30秒,也就是帧速度为30fps。
要改变您程序的帧速率,添加以下代码到PlatformerGame类构造函数的末尾:
这个告诉XNA每50微秒调用一次Game.Update,相当于帧速率20fps。编译游戏并运行它,您会发现动画以低得多的速度运行。在TimeSpan构造函数中尝试不同的值(比如说,1毫秒)来看看动画循环的速度。
理想的情况是你应该保持帧速率在30fps左右,就是说可以不用管默认帧数。为什么30fps是个标准呢?这是让手机屏幕不会让人眼察觉到闪烁的最低刷新率。如果您将帧速率调得太高,XNA不保证您能获得期望的性能。GPU的速度,处理器的速度,您消耗的资源和代码的效率决定了您的游戏是否能达到最好的性能。
幸运的是,XNA提供了一种方法来检测您的游戏是否存在性能问题。Update和Draw方法都有的GameTime对象参数,有一个叫做IsRunningSlow的布尔类型的属性。您能在任何时候在这两个方法中检查IsRunningSlow的值。如果值为true,XNA不能跟上您指定的帧速率。在这种情况下,XNA会进行跳帧尽力达到您期望的速率。这也许不是您愿意在任何游戏中看到的结果。所以如果出现这样的情况,您或许应该提醒用户,他的机器配置在运行游戏时非常困难,应该释放其它的程序占用的资源以便获得最佳的性能。
调整动画速度
尽管调整游戏本身的帧速率可以影响动画的速度,但是这样做并不是理想的方法。为什么呢?当您改变了游戏的帧速率,将会影响到所有精灵的动画速度,比如英雄和僵尸怪的移动速度会变得非常不自然。如果您希望一个动画的速度为30fps而另一个为20fps,您就不应该通过改变整个游戏的帧速率来实现。所以说实现一个动画并非难事,真正困难的地方在于如何控制每个动画可以有自己不同的刷新速度,如何使同一个动画在不同配置的机器上表现相同。
移除之前修改TargetElapsedTime的代码,让我们试试其他的途径。
当机器的配置不够时,XNA会自动跳过某些次绘制——即不调用Draw()方法。我们可以通过GameTime(你还记得Update和Draw方法都有一个该类型的参数)的IsRunningSlowly属性来检测实际的帧率是否比我们设定的要小。通过修改TargetElapsedTime属性来设置帧率,会使所有的动画都受到影响,因为它实际修改的是调用Update()和Draw()的频率。
那么如何使一个动画以自己恒定的速度刷新了?包括这个动画的刷新速度不受主帧率(即TargetElapsedTime设定的值)和机器配置的影响,当然,前提条件是我们动画的刷新率不能大于主帧率,也不能超出WP7配置允许的最大帧率。
我们可以用类似下面的代码来控制每个动画以自己的刷新率运行:
2 /// 根据时间的推进,在合适的位置绘制动画中帧
3 /// </summary>
4 public void Draw(GameTime gameTime, SpriteBatch spriteBatch, Vector2 position, SpriteEffects spriteEffects)
5 {
6 if (Animation == null)
7 throw new NotSupportedException("No animation is currently playing.");
8
9 //查看是否满足跳帧的条件,是的话,则直接绘制下一帧
10 time += (float)gameTime.ElapsedGameTime.TotalSeconds;
11 while (time > Animation.FrameTime)
12 {
13 time -= Animation.FrameTime;
14
15 //查看动画是否支持循环
16 //是的话,动画在最后帧结束后从头开始
17 if (Animation.IsLooping)
18 {
19 frameIndex = (frameIndex + 1) % Animation.FrameCount;
20 }
21 else
22 {
23 frameIndex = Math.Min(frameIndex + 1, Animation.FrameCount - 1);
24 }
25 }
26
27 // 计算当前帧在精灵图片中的位置,比如说僵尸怪第一个动作的矩形区域为(0,0,64,64)
28 Rectangle source = new Rectangle(FrameIndex * Animation.Texture.Height, 0,
29 Animation.Texture.Height, Animation.Texture.Height);
30
31 // 绘制当前帧
32 spriteBatch.Draw(Animation.Texture, position, source, Color.White, 0.0f, Origin, 1.0f,
33 spriteEffects, 0.0f);
34 }
通过上述代码,我们就可以控制目标Sprite的动画速率为Animation.FrameTime的设定值,比如说20fps,如果两次Update()之间的绘制时间超过了Animation.FrameTime的设定值,那么游戏会发生跳帧的现象,也就是直接跳到原本应该绘制帧的下一帧。在实际的应用中,我们可以将上述控制帧率的代码放到Sprite的基类中,这样就可以控制不同的Sprite以各自的速率运行了。
我们来看前面Draw()方法中28-33行代码的含义:
spriteBatch.Draw(Animation.Texture, position, source, Color.White, 0.0f, Origin, 1.0fspriteEffects, 0.0f);
前面说了我们可以将僵尸怪的精灵图元分解为一个个的动作,在僵尸怪精灵序列图片中,一共包含10个分解动作的小图片。每个小图片的大小都为64*64像素,宽和高都是一样的。我们可以将这10个小图片的索引安装顺序定位1-9。这里程序中将索引定位从0开始,而不是1,这一点要注意下。
这样的话,我们就知道索引为0的图片其矩形区域为(0,0,64,64),索引为1的小图片矩形区域为(64,0,64,64)。如果不发生跳帧的现象,每调用Draw()代码一次图片的索引值就会发生增加1,又或者索引重新变为0(在动画允许循环的情况下)。这样动画每次绘制的僵尸怪动作都不一样,这样就依次读取图片区域并绘制就形成了连贯的动画。因为轩辕不太懂GIF动画图片的实现,下面用了个类似的图片替代僵尸怪的跑动动画:
下面是动画播放结构体AnimationPlayer的完整代码:
2 //-----------------------------------------------------------------------------
3 // AnimationPlayer.cs
4 //
5 // Microsoft XNA Community Game Platform
6 // Copyright (C) Microsoft Corporation. All rights reserved.
7 //-----------------------------------------------------------------------------
8 #endregion
9
10 using System;
11 using Microsoft.Xna.Framework;
12 using Microsoft.Xna.Framework.Graphics;
13
14 namespace Platformer
15 {
16 /// <summary>
17 /// Controls playback of an Animation.
18 /// </summary>
19 struct AnimationPlayer
20 {
21 /// <summary>
22 /// Gets the animation which is currently playing.
23 /// </summary>
24 public Animation Animation
25 {
26 get { return animation; }
27 }
28 Animation animation;
29
30 /// <summary>
31 /// Gets the index of the current frame in the animation.
32 /// </summary>
33 public int FrameIndex
34 {
35 get { return frameIndex; }
36 }
37 int frameIndex;
38
39 /// <summary>
40 /// The amount of time in seconds that the current frame has been shown for.
41 /// </summary>
42 private float time;
43
44 /// <summary>
45 /// Gets a texture origin at the bottom center of each frame.
46 /// </summary>
47 public Vector2 Origin
48 {
49 get { return new Vector2(Animation.FrameWidth / 2.0f, Animation.FrameHeight); }
50 }
51
52 /// <summary>
53 /// Begins or continues playback of an animation.
54 /// </summary>
55 public void PlayAnimation(Animation animation)
56 {
57 // If this animation is already running, do not restart it.
58 if (Animation == animation)
59 return;
60
61 // Start the new animation.
62 this.animation = animation;
63 this.frameIndex = 0;
64 this.time = 0.0f;
65 }
66
67 /// <summary>
68 /// Advances the time position and draws the current frame of the animation.
69 /// </summary>
70 public void Draw(GameTime gameTime, SpriteBatch spriteBatch, Vector2 position, SpriteEffects spriteEffects)
71 {
72 if (Animation == null)
73 throw new NotSupportedException("No animation is currently playing.");
74
75 // Process passing time.
76 time += (float)gameTime.ElapsedGameTime.TotalSeconds;
77 while (time > Animation.FrameTime)
78 {
79 time -= Animation.FrameTime;
80
81 // Advance the frame index; looping or clamping as appropriate.
82 if (Animation.IsLooping)
83 {
84 frameIndex = (frameIndex + 1) % Animation.FrameCount;
85 }
86 else
87 {
88 frameIndex = Math.Min(frameIndex + 1, Animation.FrameCount - 1);
89 }
90 }
91
92 // Calculate the source rectangle of the current frame.
93 Rectangle source = new Rectangle(FrameIndex * Animation.Texture.Height, 0, Animation.Texture.Height, Animation.Texture.Height);
94
95 // Draw the current frame.
96 spriteBatch.Draw(Animation.Texture, position, source, Color.White, 0.0f, Origin, 1.0f, spriteEffects, 0.0f);
97 }
98 }
99 }
100
AnimationPlayer结构体和Animation构成了Platformer游戏的动画基础,所有的和僵尸怪、英雄相关的所有动画实现都是构建于它们之上。完成这两个类后,我们实现僵尸怪的跑动效果就可以说是非常简单了。轩辕将在下一节介绍僵尸怪的跑动动画和它本身的特性的所有实现。
喜欢这篇文章的兄弟们点击文章下面的“推荐”支持下。