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( 0 0 0 0 50 );

 

这个告诉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的完整代码:

 

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

 

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

 

转载于:https://www.cnblogs.com/imobiler/archive/2010/12/14/1905054.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值