Windows Phone 7范例游戏Platformer实战7——简单动画的绘制实现原理

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——加速度传感器解读

 

本文参考了木木二进制翻译的Learning XNA 3.0文章,非常感谢木木同学做出的杰出贡献。

 

在开始介绍游戏动画前,轩辕先引入一个具备动画效果的对象,就是Platformer游戏中的宝石。在游戏中,数个宝石作为一个整体,按照正弦波函数的方式缓慢地上升或下降,以展现类似波浪上下起伏的悬浮效果。如图所示:

 

 

 

宝石类在Platformer项目中的Gem.cs文件中,该类包含了如下的重要方法:

 

LoadContent()
加载一个灰色宝石状的纹理图片和声音

 

 

Update()
Update方法是在固定的间隔时间内更新宝石的高度,使得数个宝石可以按照正弦波的方式产生类似波浪的上下起伏效果。

 

Draw()
该方法重要是给宝石绘制指定的颜色,因为我们前面说过宝石的纹理图片默认情况下是灰色的。而在游戏中,我们可以按照宝石的分值不同绘制不同的颜色,Platforme游戏中的宝石默认为黄色。

  

OnCollected()方法
当英雄和宝石发生碰撞时调用该方法,并播放声音提示用户宝石已经被收集。

 

前面章节我们已经提及过游戏的冲突检测方法,其中介绍了宝石的边界是一个包含其在内的圆形。如果英雄的边界和宝石的边界发生了碰撞,那么这个宝石将从关卡的宝石集合中删除,将宝石的对应分值加到英雄的得分上。

 

 

因此尤为重要的就是Circle类的Intersects()方法,正是这个方法用来检测英雄是否和宝石进行了碰撞。该方法被关卡类的Level.UpdateGems()方法调用,如果宝石和英雄发生了碰撞,将返回true,并使用OnGemCollected()方法将场景中的宝石移除显示。

 

关于关卡类Level轩辕将会在后续的课程中深入探讨,但是这里我们重点来说下XNA中几个非常重要的方法,那就是LoadContent()、Update()、Draw()。


说明这几个方法的重要性,我们先打开Visual Studio 2010新建一个XNA项目,在Program.cs中的Main()函数只有三行代码,主要目的就是创建一个新的game对象,并启动游戏。代码如下:

 

1 using (Game1 game = new Game1())
2 {
3    game.Run();
4 }

 

 

真正重要的是这个Game1类,我们看看默认情况下该类包含的方法吧,这些是你开始XNA游戏编程必须掌握的基础,以后所有的游戏代码扩展都是搭建于这个类的基础上。下面是Game1.cs的代码:

 

 1     public class Game1 : Microsoft.Xna.Framework.Game
 2     {
 3         GraphicsDeviceManager graphics;
 4         SpriteBatch spriteBatch;
 5 
 6         public Game1()
 7         {
 8             graphics = new GraphicsDeviceManager(this);
 9             Content.RootDirectory = "Content";
10 
11             // Frame rate is 30 fps by default for Windows Phone.
12             TargetElapsedTime = TimeSpan.FromTicks(333333);
13         }
14 
15         /// <summary>
16         /// Allows the game to perform any initialization it needs to before starting to run.
17         /// This is where it can query for any required services and load any non-graphic
18         /// related content.  Calling base.Initialize will enumerate through any components
19         /// and initialize them as well.
20         /// </summary>
21         protected override void Initialize()
22         {
23             // TODO: Add your initialization logic here
24 
25             base.Initialize();
26         }
27 
28         /// <summary>
29         /// LoadContent will be called once per game and is the place to load
30         /// all of your content.
31         /// </summary>
32         protected override void LoadContent()
33         {
34             // Create a new SpriteBatch, which can be used to draw textures.
35             spriteBatch = new SpriteBatch(GraphicsDevice);
36 
37             // TODO: use this.Content to load your game content here
38         }
39 
40         /// <summary>
41         /// UnloadContent will be called once per game and is the place to unload
42         /// all content.
43         /// </summary>
44         protected override void UnloadContent()
45         {
46             // TODO: Unload any non ContentManager content here
47         }
48 
49         /// <summary>
50         /// Allows the game to run logic such as updating the world,
51         /// checking for collisions, gathering input, and playing audio.
52         /// </summary>
53         /// <param name="gameTime">Provides a snapshot of timing values.</param>
54         protected override void Update(GameTime gameTime)
55         {
56             // Allows the game to exit
57             if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
58                 this.Exit();
59 
60             // TODO: Add your update logic here
61 
62             base.Update(gameTime);
63         }
64 
65         /// <summary>
66         /// This is called when the game should draw itself.
67         /// </summary>
68         /// <param name="gameTime">Provides a snapshot of timing values.</param>
69         protected override void Draw(GameTime gameTime)
70         {
71             GraphicsDevice.Clear(Color.CornflowerBlue);
72 
73             // TODO: Add your drawing code here
74 
75             base.Draw(gameTime);
76         }
77     }

 

XNA模板自动为我们提供了数个成员变量,此外还有一个构造函数和5个方法。轩辕有必要对这些变量和方法逐一进行介绍。

 

GraphicsDeviceManager,顾名思义,就是图像设备管理器,这个类为开发人员提供了一种可以访问Windows Phone上图像设备的途径。GraphicsDeviceManager有一个属性GraphicDevice代表了WP7手机的图形设备。因为图形设备对象在XNA游戏和您的显卡之间一个中介作用,因此您的XNA游戏在屏幕上做的任何事情都要通过这个对象。

 

第2个成员变量是SpriteBatch类的实例,这是一个您将用来绘制精灵(Sprite)的核心对象,注意,这里所谓的精灵不是国外魔法世界中的长耳朵的帅哥和美女,而是游戏中的一种专用名词。在计算机图形学术语中,一个精灵被定义为场景中的一个2D或3D图像。2D游戏由场景中各种各样的精灵组成,Platformer游戏中用到的精灵图片较为有限,无外乎就是英雄和僵尸怪了。下面轩辕再次给出游戏中的部分精灵图片,以加深你对这个概念的印象。如下图所示:

 

 

Initialize方法用来初始化变量和Game1对象相关的其他对象。

 

LoadContent方法主要是用来加载内容管理器的各种游戏资源,包括图片、视频、音频等各种对象。像前面提及的精灵图片可以在这进行加载。

 

当LoadContent方法调用完成后,Game1对象将进入游戏循环。几乎所有的游戏都使用某种形式的游戏循环,不管它们是否用XNA写成。这是游戏开发和我们常见的事件驱动开发有点不同,这对一些开发者来说可能需要一定的时间进行适应。

 

本质上来说,一个游戏循环由一系统方法组成,这些方法反复被调用直到游戏结束。在XNA中,游戏循环只包含两个函数:Update和Draw。眼下您可以这样理解游戏循环:所有影响游戏的逻辑都将在Update或Draw方法中完成。您应该设法在Draw方法避免做除绘制外的其它事情。游戏运行涉及的状态变化,比如说移动物体,碰撞检测,更新分数,游戏结束检测逻辑应该放到Update方法中进行处理。

 

这里轩辕要说明下XNA游戏开发和Silverlight开发有何不同。在Silverlight中,我们还是采用传统的事件驱动模式。比如说我们使用Silverlight构建了一个只包含文本框和按钮的登录窗体,如果你对窗体不做任何操作的话,那么系统对窗体不会有任何的操作,也就是说窗体不会出现循环刷新的过程。只有你点击窗体上的按钮、或者输入文本时等操作时,系统才会被激活以便进行页面的重绘制和信息的处理。而XNA不尽然,假设你同样是构建了怎么一个登录窗体(相对于Silverlight可能实现起来要复杂些),即使你对游戏不做任何的操作,登录窗体画面看起来也没有任何的变化,但是系统却是在对窗体进行重复的绘制。 

 

在Silverlight编程中,事件的产生是被动的,比如说我们点击一个按钮控件,按钮的生成了一个可以被系统捕获的Click事件。换句话说,应用程序只有在用户给它发送一个某个按钮被按下的事件标识后才会被唤醒并做一些处理。因此大多数情况下,Silverlight的窗体是保持一种静默是状态。

 

相比之下,游戏程序由事件轮询(polling for events)驱动,而不是等待并监听是否有事件被激发。取而代之,游戏程序会主动询问系统鼠标是否被移动,同时程序会一直运行,不管有没有用户输入。 在同一时间,不管用户是否与系统进行交互,XNA游戏中的一切仍在进行。我们不可能在游戏中为每个对象注册一个事件,以便被动去响应。游戏的很多对象都应该按照自己的设定模式进行运动或者存在,无需任何外界输入的驱动。比如说Platformer中的僵尸怪,它会按照自己的方法在平台上来回跑动,无需用户的输入和操作。 这就是XNA使用游戏循环的一个主要原因:它提供了一种途径让游戏始终运行着而不管玩家在做什么。

 

此外,XNA还使用循环不断地进行着动画的更新,物体的移动,碰撞检测,分数更新,检测游戏是否结束、获得用户输入等等操作。

 

在Silverlight程序中,要做到持续检测非用户事件这一点有些困难,但在XNA开发中,这一点用游戏循环的形式整合进了应用程序框架。所有这些任务都在游戏循环的Update方法中进行处理,接着场景绘制在游戏循环的Draw方法中完成。

 

事实上,所有的应用程序都有和游戏循环功能相似的循环。Windows本身使用一个消息和事件系统,不断循环告知应用程序何时需要重绘和完成其他功能。对这些循环的访问通常是隐藏起来的,不过,大多数应用程序不需要访问这些非用户驱动事件。

 

如早先提到的,Update方法是您更新和游戏有关的一切东西的地方。您能更新物体在屏幕中的位置,分数,动画序列等等。您也能检查用户输入,进行碰撞检测,并且调整AI算法。

 

在Update方法中对游戏中变化的检测和依照这些变化的行动通常都和游戏状态有关。游戏状态是一个非常重要的概念:这是一种让游戏知道当前游戏状况的方法。游戏一般有多种完全不同的状态,比如启动画面,实际游戏画面,游戏结束画面。还会有一些细微的状态改变,比如玩家得到某种宝物使角色无敌一段时间或其他一些游戏行为的改变(比如说获得宝石或者和僵尸怪发生了碰撞)。通常您需要在Update方法中改变游戏状态,然后在Draw方法中使用这些状态来绘制不同的图像、场景以及其他的一些和特定状态相关的信息。

 

Draw方法是把游戏中所有的物体绘制到屏幕上的地方。在Gem这个宝石类中,Draw方法做的事情就是给宝石添色,并根据时间和正弦波所计算出的位置绘制宝石。这一点轩辕会在后面进行讲解。

 

下图展示了一个XNA游戏的生命周期,包括Update和Draw方法形成一个游戏循环。

 

 

请注意Update方法的执行有两个可能的结果:要么持续执行然后Draw方法被调用,要么游戏结束,退出游戏循环并且调用UnloadContent方法。当您调用Game类的Exit方法时,游戏循环将结束。

 

在您的游戏退出循环和结束游戏之间应该存在一些过程。比如说,如果英雄碰到僵尸怪死亡的话,然后游戏直接退出并且游戏窗口就这么消失,将使玩家觉得很郁闷。实际上大多数玩家将会把这种行为看作某种Bug。相应的,您通常使用某种游戏状态逻辑使Draw方法调用去渲染游戏结束画面代替游戏进行画面。然后过了固定时间后或您检测到玩家按下某些键,游戏才会真正的退出。这些工作现在好像有些复杂不容易理解,但是请不要过分担心,轩辕会在后续的章节告诉大家如何实现合乎情理的退出方式。 下面是Platformer游戏关卡结束时提示界面:

 

 

一旦游戏退出循环,UnloadContent会被调用。这个方法用来卸载所有在LoadContent方法里加载的内容。就像.NET一样,XNA会进行自动垃圾回收,但如果你在某些对象中进行需要特别处理的内存操作,在UnloadContent方法做相应的操作就是了。

 

好了,讲了这么多,现在我们开始实现游戏中的宝石吧。前面就提及过,游戏资源中宝石素材是一个灰色的图片,我们在游戏中需要将这个图片加载到内容管道中,并为图片绘制颜色和上下浮动的效果。

 

为此我们可以事先进行如下的声明,texture变量和宝石纹理相关联。

//宝石纹理
private Texture2D texture;

//宝石开始矢量坐标
private Vector2 origin;

//英雄收集宝石时的声音效果
private SoundEffect collectedSound;

 

 这个方法就是实现宝石图片资源、声音的加载。 传入Content.Load方法的参数是图像和音频文件的路径,根目录是解决方案中的Content节点。请注意参数中使用的资源名而不是文件名

 

1 public void LoadContent()
2 {
3     texture = Level.Content.Load<Texture2D>("Sprites/Gem");
4     origin = new Vector2(texture.Width / 2.0f, texture.Height / 2.0f);
5     collectedSound = Level.Content.Load<SoundEffect>("Sounds/GemCollected");
6 }

 

 在加载完资源文件后,接下来就是宝石在场景的绘制了。此外,宝石是按照正弦波的方式做上下的起伏运动,因此如何判定宝石当前的位置就是至关重要的。这也就是前面提及的游戏状态的改变,而且这个改变和游戏的时间密切相关。因为宝石位置的确定和绘制无关,我们可以将这个逻辑的实现放到Update()方法中。下面是具体的代码:

 

 

 1 /// <summary>
 2 /// 宝石在空中上下浮动,以吸引用户对宝石进行收集
 3 /// </summary>
 4 public void Update(GameTime gameTime)
 5 {
 6     //宝石浮动控制常量 
 7     const float BounceHeight = 0.18f;
 8     const float BounceRate = 3.0f;
 9     const float BounceSync = -0.75f;
10 
11     // 随着时间的推移宝石按正弦波的方式上下浮动
12 
13     // 这里使用当前宝石的X坐标,以便确认附近宝石的X坐标, 这样多个宝石就可以形成类似波浪的效果。  
14     double t = gameTime.TotalGameTime.TotalSeconds * BounceRate + Position.X * BounceSync;
15     bounce = (float)Math.Sin(t) * BounceHeight * texture.Height;
16 }
17 
18 /// <summary>
19 /// 获得场景中宝石的当前位置
20 /// </summary>
21 public Vector2 Position
22 {
23    get
24    {
25       return basePosition + new Vector2(0.0f, bounce);
26    }
27 }
28 
29 /// <summary>
30 /// 场景中宝石的圆形边界,以实现冲突检测
31 /// </summary>
32 public Circle BoundingCircle
33 {
34    get
35    {
36       return new Circle(Position, Tile.Width / 3.0f);
37    }
38 }
39 
40 

 

我们注意到,在Update()方法中使用了一个GameTime类型的参数,GameTime用来表示游戏中经过的时间。在Windows Phone 7中默认情况下XNA的游戏画面绘制为30pfs,就是每秒钟绘制30次。我们需要使用GameTime这个类似计时器的对象来确定何时进行游戏状态更新和画面绘制,以免延时造成游戏画面的不流畅。

 

这里要引入帧这个概念,我们知道所有的动画和视频都是一幅幅的图片构成,而人眼的视觉暂留时间接近是0.1秒。如果一个物体快速移动,达到一定的速度时,人就看到拖尾现象. 当照片以16张每秒的速度播放时就是连贯的动画了。在XNA中,每调用Draw()绘制游戏画面一次我们就称为一帧。而一秒内调用Draw()的次数我们称为帧速率,Windows Phone 7上默认情况下XNA的游戏画面绘制为30pfs(帧/秒)。

 

1 /// <summary>
2 /// 为宝石添加合适的特定的颜色
3 /// </summary>
4 public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
5 {
6    spriteBatch.Draw(texture, Position, null, Color, 0.0f, origin, 1.0f, SpriteEffects.None, 0.0f);
7 }
8 

 

该方法除了使用了前面提及的GameTime外,还使用SpriteBatch对象,SpriteBatch顾名思义,就是对精灵的批处理。我们知道在游戏中包含大量的绘制过程,包括英雄的跳跃跑动、僵尸怪的来回巡逻、背景图片等,我们可以将所有这些绘制都放到一个批处理中进行。XNA中不建议使用多个SpriteBatch来绘制纹理,否则会导致性能的下降。

 

好了,宝石类是有的特性我们就基本上都实现了,宝石的动画效果也会随着时间的推进而上下浮动。下面是Gem.cs的全部代码,大家参考下:

 

 

代码
  1 using System;
  2 using Microsoft.Xna.Framework;
  3 using Microsoft.Xna.Framework.Graphics;
  4 using Microsoft.Xna.Framework.Audio;
  5 
  6 namespace Platformer
  7 {
  8     /// <summary>
  9     /// A valuable item the player can collect.
 10     /// </summary>
 11     class Gem
 12     {
 13         private Texture2D texture;
 14         private Vector2 origin;
 15         private SoundEffect collectedSound;
 16 
 17         public const int PointValue = 30;
 18         public readonly Color Color = Color.Yellow;
 19 
 20         // The gem is animated from a base position along the Y axis.
 21         private Vector2 basePosition;
 22         private float bounce;
 23 
 24         public Level Level
 25         {
 26             get { return level; }
 27         }
 28         Level level;
 29 
 30         /// <summary>
 31         /// Gets the current position of this gem in world space.
 32         /// </summary>
 33         public Vector2 Position
 34         {
 35             get
 36             {
 37                 return basePosition + new Vector2(0.0f, bounce);
 38             }
 39         }
 40 
 41         /// <summary>
 42         /// Gets a circle which bounds this gem in world space.
 43         /// </summary>
 44         public Circle BoundingCircle
 45         {
 46             get
 47             {
 48                 return new Circle(Position, Tile.Width / 3.0f);
 49             }
 50         }
 51 
 52         /// <summary>
 53         /// Constructs a new gem.
 54         /// </summary>
 55         public Gem(Level level, Vector2 position)
 56         {
 57             this.level = level;
 58             this.basePosition = position;
 59 
 60             LoadContent();
 61         }
 62 
 63         /// <summary>
 64         /// Loads the gem texture and collected sound.
 65         /// </summary>
 66         public void LoadContent()
 67         {
 68             texture = Level.Content.Load<Texture2D>("Sprites/Gem");
 69             origin = new Vector2(texture.Width / 2.0f, texture.Height / 2.0f);
 70             collectedSound = Level.Content.Load<SoundEffect>("Sounds/GemCollected");
 71         }
 72 
 73         /// <summary>
 74         /// Bounces up and down in the air to entice players to collect them.
 75         /// </summary>
 76         public void Update(GameTime gameTime)
 77         {
 78             // Bounce control constants
 79             const float BounceHeight = 0.18f;
 80             const float BounceRate = 3.0f;
 81             const float BounceSync = -0.75f;
 82 
 83             // Bounce along a sine curve over time.
 84             // Include the X coordinate so that neighboring gems bounce in a nice wave pattern.            
 85             double t = gameTime.TotalGameTime.TotalSeconds * BounceRate + Position.X * BounceSync;
 86             bounce = (float)Math.Sin(t) * BounceHeight * texture.Height;
 87         }
 88 
 89         /// <summary>
 90         /// Called when this gem has been collected by a player and removed from the level.
 91         /// </summary>
 92         /// <param name="collectedBy">
 93         /// The player who collected this gem. Although currently not used, this parameter would be
 94         /// useful for creating special powerup gems. For example, a gem could make the player invincible.
 95         /// </param>
 96         public void OnCollected(Player collectedBy)
 97         {
 98             collectedSound.Play();
 99         }
100 
101         /// <summary>
102         /// Draws a gem in the appropriate color.
103         /// </summary>
104         public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
105         {
106             spriteBatch.Draw(texture, Position, null, Color, 0.0f, origin, 1.0f, SpriteEffects.None, 0.0f);
107         }
108     }
109 }
110 

 

宝石实现完成了,下一节轩辕开始介绍僵尸怪的动画实现。僵尸怪的行为包括只有跑动一种,和宝石的动画不一样的是,僵尸怪是由一个连贯的精灵图片实现的,这里需要对精灵图片进行解析,将整个图片解析为细分的动作。如下图所示:

 

 

 

好了,今天就到这里,喜欢这篇文章的兄弟们点击文章下面的“推荐”支持下。

 

posted @ 2010-12-12 23:38  軒轅  阅读(1700)  评论(1编辑  收藏  举报