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的完整代码:

 

 1     class 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,这一切都完成之后,我们还需要对动画进行不同类型的播放控制。比如说游戏程序检测到僵尸怪动画从未播放过,那么好,动画就第一帧开始。如果动画已经在播放,那么动画从上个动作继续进行绘制和解析。下面就是判定动画是否已经播放的代码

 

public void PlayAnimation(Animation animation)
{
   
//如果动画已经在运行,无需重新开始
   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类构造函数的末尾:

 

TargetElapsedTime = new TimeSpan(000050);

 

这个告诉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配置允许的最大帧率。

 

我们可以用类似下面的代码来控制每个动画以自己的刷新率运行:

 

 1 /// <summary>
 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行代码的含义:

 

Rectangle source = new Rectangle(FrameIndex * Animation.Texture.Height, 0, Animation.Texture.Height, Animation.Texture.Height);
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的完整代码:

 

代码
  1 #region File Description
  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游戏的动画基础,所有的和僵尸怪、英雄相关的所有动画实现都是构建于它们之上。完成这两个类后,我们实现僵尸怪的跑动效果就可以说是非常简单了。轩辕将在下一节介绍僵尸怪的跑动动画和它本身的特性的所有实现。

 

喜欢这篇文章的兄弟们点击文章下面的“推荐”支持下。

 

posted @ 2010-12-14 10:54  軒轅  阅读(1812)  评论(0编辑  收藏  举报