Windows Phone 7范例游戏Platformer实战9——僵尸怪

 

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——简单动画的绘制实现原理 

Windows Phone 7范例游戏Platformer实战8——精灵动画的绘制实现  

 

在知道精灵类的实现后,现在我们开始僵尸怪对象的编程。僵尸怪在游戏场景的特征为在游戏的平台上来回的跑动,如果跑到平台的边缘,它会稍微停止下后朝边缘相反的方向跑动。

因为我们要判断僵尸怪跑动的方向,因此可以用一个枚举进行标注。

 

enum FaceDirection
{
     Left 
= -1,
     Right 
= 1,
}

 

僵尸怪跑动的精灵图片我们在前面几节已经展示多次了,其实僵尸怪在停止时也是使用精灵图片,为了让你更好了解僵尸怪的特性,轩辕将这两个图片再次展示下。

 

 

在有了精灵图片后,我们要做的事情就是将精灵图片转化成动画。前面我们已经实现了动画和动画播放结构体,因此在游戏中首先我们可以将僵尸怪的跑动和静止动画的实现,以及动画播放结构体实例化,以备接下来的使用。

 

下面是它们的声明代码:

 

private Animation runAnimation;
private Animation idleAnimation;
private AnimationPlayer sprite;

 

因为僵尸怪需要和英雄、踏脚石进行冲突检测,因此僵尸怪的边界范围也是非常重要的。僵尸怪一开始的处于停止状态的,因此我们首先需要对静止状态下的边界进行判定。


下图是僵尸怪在静止状态下精灵图片的一帧,我们来看看该帧画面中僵尸怪的实际边界应该是多少。虽然说一帧动作图片的大小为64*64像素,但是为了每张图片能完整保护僵尸怪的各种动作,我们往往将每帧动作图片设计的稍微大些。因为在实际的游戏编程中,我们可以对图片的背景颜色进行透明处理,因此我们可以消去每帧图片中空白的部分区域,只显示僵尸怪的轮廓。

有两种方法可以实现图像部分透明:要么图像文件本身有透明的背景,要么使用特定的代码对背景颜色进行透明处理。在Platformer游戏中,因为所有的僵尸怪图片素材背景都是透明的,这样在场景中就僵尸怪的空白部位没有任何显示,这样下层的背景图片可以透过僵尸怪的空白处呈现出来了。

 

精灵图片的透明度可以存储到某些文件格式中(如.png文件)用作alpha通道。这些格式不仅仅包含RGB值,每个像素有一个额外的alpha通道(RGBA中的A)来确定像素的透明度。

 

下面是进行了透明处理和未透明两种状况下的僵尸怪呈现画面。

 

 

正是因为透明这个特性,这样僵尸怪的可呈现部分实际上就大大缩小了,那么Platformer游戏中僵尸怪处于静止状态时的边界区域为多少呢?从下面这帧僵尸怪的静止图片上,僵尸怪位于图片的中间部位.

 

 

可以大概看到僵尸怪实际的宽度不过是整个图片是1/3左右。

int width = (int)(idleAnimation.FrameWidth * 0.35);

 

这样我们就可以得到僵尸怪在帧图片中的X起始坐标为

 

int left = (idleAnimation.FrameWidth - width) / 2;

 

此外僵尸怪的高度目测为整个帧图片的70%左右,上面的30%为空白背景。

 

int height = (int)(idleAnimation.FrameWidth * 0.7);

 

因此我们可以得到僵尸怪的顶端是从图片Y轴方向的30%处开始。下面是获得僵尸怪顶端Y坐标的代码

 

int top = idleAnimation.FrameHeight - height;

 

 最终我们得到了僵尸怪静止状态下的单帧图片的边界区域。

localBounds = new Rectangle(left, top, width, height);

 

 因此对于僵尸怪的LoadContent方法来说,其实现代码如下:

 

 1 public void LoadContent(string spriteSet)
 2 {
 3     // Load animations.
 4     spriteSet = "Sprites/" + spriteSet + "/";
 5     runAnimation = new Animation(Level.Content.Load<Texture2D>(spriteSet + "Run"), 0.1ftrue);
 6     idleAnimation = new Animation(Level.Content.Load<Texture2D>(spriteSet + "Idle"), 0.15ftrue);
 7     sprite.PlayAnimation(idleAnimation);
 8 
 9     // Calculate bounds within texture size.
10     int width = (int)(idleAnimation.FrameWidth * 0.35);
11     int left = (idleAnimation.FrameWidth - width) / 2;
12     int height = (int)(idleAnimation.FrameWidth * 0.7);
13     int top = idleAnimation.FrameHeight - height;
14     localBounds = new Rectangle(left, top, width, height);
15 }     

 

前面的LoadContent即加载了僵尸怪跑动和静止状态下的精灵图片,也对僵尸怪的边界区域进行了初始化。 

 

上面的localBounds只是僵尸怪本身的边界区域,我们需要还需要将该边界区域转换为整个游戏场景中的坐标区域。在僵尸怪的动画绘制中,我们将每帧图片的底端中间位置看着是该帧的原点。大家可以在AnimationPlayer结构体中看到如下的代码:

 

public Vector2 Origin
{
   
get { return new Vector2(Animation.FrameWidth / 2.0f, Animation.FrameHeight); }
}

 

通过那个Origin属性我们就可以访问到当前动画绘制帧的底端中部坐标。

 

在XNA游戏的世界中,左上角是整个游戏坐标的系统的原点(0,0),X坐标轴越往右值越大,Y坐标轴则是越往下值越大,这和我们平常使用的坐标系统完全是两个不同的概念。

因此要将边界区域转化为实际场景中的坐标区域,我们需要事先获得僵尸怪在场景中的位置Position,Position的定义也是僵尸怪纹理图片在实际场景中的底端中间位置。

 

下面是Position属性的定义:

 

public Vector2 Position
{
   
get { return position; }
}
Vector2 position;

 

Position坐标是由Level对象传入僵尸怪类的构造函数的,至于具体的僵尸怪在场景中的Position值为多少,轩辕将会在讲到Level类时引入。目前你只需知道僵尸怪在场景中的位置坐标Position已经获取到。下面是僵尸怪类Enemy的构造函数

 

public Enemy(Level level, Vector2 position, string spriteSet)
{
    
this.level = level;
    
this.position = position;
    LoadContent(spriteSet);
}

 

将僵尸怪的边界区域转化为实际场景中坐标其实非常简单,主要是根据相对位置这个概念。实现代码如下

 

public Rectangle BoundingRectangle
{
    
get
    {
         
int left = (int)Math.Round(Position.X - sprite.Origin.X) + localBounds.X;
         
int top = (int)Math.Round(Position.Y - sprite.Origin.Y) + localBounds.Y;
         
return new Rectangle(left, top, localBounds.Width, localBounds.Height);
    }
}

 

下面是计算边界区域最左端在整个游戏场景中的坐标说明图,请参照上面的代码进行分析!

 


 

 判定了僵尸怪的边界区域在游戏中的位置后,接下来我们要实现僵尸怪跑动或者静止。前面我们提及过,僵尸怪会在平台上来回跑动,当跑到平台边缘或者遇到不可通过的墙壁时,僵尸怪会停留一段时间后按反方向跑动。在Update()方法中,会不停地计算僵尸怪的位置。

 

下面轩辕来分析下如何实现僵尸怪的跑动,以及走到平台边缘或者不可通过的墙壁时的回跑。我们可以用代码设定转向时僵尸怪等待的最长时间,以及僵尸怪跑动的速度。

 

/// <summary>
/// How long to wait before turning around.
/// </summary>
private const float MaxWaitTime = 0.5f;

/// <summary>
/// The speed at which this enemy moves along the X axis.
/// </summary>
private const float MoveSpeed = 64.0f;

  

接下来我们就可以在Update()中使用gameTime.ElapsedGameTime.TotalSeconds来获得自上一帧之后经过的时间,这样我们就可以根据时间*MoveSpeed来判定僵尸怪跑动的像素了。


在Platformer游戏中,包括踏脚石、宝石、出口在内的对象其实都是Tile对象,正是由一个个Tile对象才构成了整个游戏的关卡场景。下面的图就展示了部分tile对象的布局:

 

 

在游戏中每个Tile对象的大小为40*32像素,我们在前面讲过,游戏场景是读取地图文本文件中的字符进行解析的,现在让我们再回顾下这个地图文件。

 

  

 

可以看到该文件为20列,15行。虽然僵尸怪和英雄不是Tile对象,但是他们字符串占用的位置依然可以看着是个Tile,只不过这个Tile为透明罢了。这样我们就可以知道场景是由20*15个这样的Tile对象组成,每个Tile大小为40*32像素。在Platformer游戏中我们使用Tile[20][15]这样的二维数组来保存地图信息。


目前我们知道僵尸怪在真实场景中的位置Position,所以我们需要知道僵尸怪跑向的踏脚石对应的Tile对象在场景二维数组的索引。因为Position是僵尸怪的底端中部位置,所我们可以根据僵尸怪跑动的方向,来查看僵尸怪的最左侧(向左跑动)或者最右侧(向右跑动)的位置坐标。根据这个坐标我们就可以求得接下来要站立的踏脚石在二维数组中的索引了。

 

float posX = Position.X + localBounds.Width / 2 * (int)direction;
int tileX = (int)Math.Floor(posX / Tile.Width) - (int)direction;
int tileY = (int)Math.Floor(Position.Y / Tile.Height);

 

接下来我们要判断僵尸怪需要等待,默认情况下,waitTime是等于0。这样僵尸怪会继续跑动下去,新位置为:

 

Vector2 velocity = new Vector2((int)direction * MoveSpeed * elapsed, 0.0f);
position 
= position + velocity;

 

如果僵尸怪在跑动的过程中使用冲突检测算法发现接下来跑动的地方没有踏脚石,或者遇到不可穿越的Tile对象时,那么将waitTime设置为MaxWaitTime,也就是让僵尸怪开始等待。

 

if (Level.GetCollision(tileX + (int)direction, tileY - 1== TileCollision.Impassable ||
        Level.GetCollision(tileX 
+ (int)direction, tileY) == TileCollision.Passable)
{
    waitTime 
= MaxWaitTime;
}

 

前面说过僵尸怪在静止一段时间后会按照反方向跑动,因为我们可以估计gameTime.ElapsedGameTime.TotalSeconds来获取每次Update()发生需要的时间,因此我们可以使用waitTime 减去gameTime.ElapsedGameTime.TotalSeconds开计算新的等待时间,这样一直到waitTime小于或者等于0为止,一旦到达小于或者等于0这个条件,僵尸怪静止状态介绍,按照反方向开始跑动。

 

if (waitTime > 0)
{
    
// Wait for some amount of time.
    waitTime = Math.Max(0.0f, waitTime - (float)gameTime.ElapsedGameTime.TotalSeconds);
    
if (waitTime <= 0.0f)
    {
          
// Then turn around.
          direction = (FaceDirection)(-(int)direction);
    }
}

 

 Update()方法实现后,接下来就是Draw()方法的绘制了。我们知道在Platformer游戏中,一旦英雄死亡,或者英雄到达出口位置,又或关卡时间耗尽。僵尸怪都将静止,而其它时刻都是在跑动。

 

下面的代码就是实现僵尸怪动画绘制的逻辑:

 


 
if (!Level.Player.IsAlive ||
                Level.ReachedExit 
||
                Level.TimeRemaining 
== TimeSpan.Zero ||
                waitTime 
> 0)
{
     sprite.PlayAnimation(idleAnimation);
}
else
{
     sprite.PlayAnimation(runAnimation);
}
 

 

因为我们已经知道僵尸怪纹理图片在真实场景中的位置Position,也就是帧图片的底端中间的坐标。接下来是重头戏,我们知道僵尸怪的跑动精灵图片都是面向左侧的,那么怎么实现僵尸怪右侧跑动的动画呢,其实在XNA中很简单,我们只需要使用下面的代码:

 

SpriteEffects flip = direction > 0 ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
sprite.Draw(gameTime, spriteBatch, Position, flip);

 

重点就在于SpriteEffects.FlipHorizontally这个参数,也就是说在绘制时,将绘制的纹理图片按照水平方向翻转。另外这个参数还可以为SpriteEffects.FlipVertically(垂直翻转)、SpriteEffects.None(不翻转)。

 

下面是整个僵尸怪Enemy.cs的完整代码:

 

代码
  1 #region File Description
  2 //-----------------------------------------------------------------------------
  3 // Enemy.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     /// Facing direction along the X axis.
 18     /// </summary>
 19     enum FaceDirection
 20     {
 21         Left = -1,
 22         Right = 1,
 23     }
 24 
 25     /// <summary>
 26     /// A monster who is impeding the progress of our fearless adventurer.
 27     /// </summary>
 28     class Enemy
 29     {
 30         public Level Level
 31         {
 32             get { return level; }
 33         }
 34         Level level;
 35 
 36         /// <summary>
 37         /// Position in world space of the bottom center of this enemy.
 38         /// </summary>
 39         public Vector2 Position
 40         {
 41             get { return position; }
 42         }
 43         Vector2 position;
 44 
 45         private Rectangle localBounds;
 46         /// <summary>
 47         /// Gets a rectangle which bounds this enemy in world space.
 48         /// </summary>
 49         public Rectangle BoundingRectangle
 50         {
 51             get
 52             {
 53                 int left = (int)Math.Round(Position.X - sprite.Origin.X) + localBounds.X;
 54                 int top = (int)Math.Round(Position.Y - sprite.Origin.Y) + localBounds.Y;
 55 
 56                 return new Rectangle(left, top, localBounds.Width, localBounds.Height);
 57             }
 58         }
 59 
 60         // Animations
 61         private Animation runAnimation;
 62         private Animation idleAnimation;
 63         private AnimationPlayer sprite;
 64 
 65         /// <summary>
 66         /// The direction this enemy is facing and moving along the X axis.
 67         /// </summary>
 68         private FaceDirection direction = FaceDirection.Left;
 69 
 70         /// <summary>
 71         /// How long this enemy has been waiting before turning around.
 72         /// </summary>
 73         private float waitTime;
 74 
 75         /// <summary>
 76         /// How long to wait before turning around.
 77         /// </summary>
 78         private const float MaxWaitTime = 0.5f;
 79 
 80         /// <summary>
 81         /// The speed at which this enemy moves along the X axis.
 82         /// </summary>
 83         private const float MoveSpeed = 64.0f;
 84 
 85         /// <summary>
 86         /// Constructs a new Enemy.
 87         /// </summary>
 88         public Enemy(Level level, Vector2 position, string spriteSet)
 89         {
 90             this.level = level;
 91             this.position = position;
 92 
 93             LoadContent(spriteSet);
 94         }
 95 
 96         /// <summary>
 97         /// Loads a particular enemy sprite sheet and sounds.
 98         /// </summary>
 99         public void LoadContent(string spriteSet)
100         {
101             // Load animations.
102             spriteSet = "Sprites/" + spriteSet + "/";
103             runAnimation = new Animation(Level.Content.Load<Texture2D>(spriteSet + "Run"), 0.1ftrue);
104             idleAnimation = new Animation(Level.Content.Load<Texture2D>(spriteSet + "Idle"), 0.15ftrue);
105             sprite.PlayAnimation(idleAnimation);
106 
107             // Calculate bounds within texture size.
108             int width = (int)(idleAnimation.FrameWidth * 0.35);
109             int left = (idleAnimation.FrameWidth - width) / 2;
110             int height = (int)(idleAnimation.FrameWidth * 0.7);
111             int top = idleAnimation.FrameHeight - height;
112             localBounds = new Rectangle(left, top, width, height);
113         }
114 
115 
116         /// <summary>
117         /// Paces back and forth along a platform, waiting at either end.
118         /// </summary>
119         public void Update(GameTime gameTime)
120         {
121             float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
122 
123             // Calculate tile position based on the side we are walking towards.
124             float posX = Position.X + localBounds.Width / 2 * (int)direction;
125             int tileX = (int)Math.Floor(posX / Tile.Width) - (int)direction;
126             int tileY = (int)Math.Floor(Position.Y / Tile.Height);
127 
128             if (waitTime > 0)
129             {
130                 // Wait for some amount of time.
131                 waitTime = Math.Max(0.0f, waitTime - (float)gameTime.ElapsedGameTime.TotalSeconds);
132                 if (waitTime <= 0.0f)
133                 {
134                     // Then turn around.
135                     direction = (FaceDirection)(-(int)direction);
136                 }
137             }
138             else
139             {
140                 // If we are about to run into a wall or off a cliff, start waiting.
141                 if (Level.GetCollision(tileX + (int)direction, tileY - 1== TileCollision.Impassable ||
142                     Level.GetCollision(tileX + (int)direction, tileY) == TileCollision.Passable)
143                 {
144                     waitTime = MaxWaitTime;
145                 }
146                 else
147                 {
148                     // Move in the current direction.
149                     Vector2 velocity = new Vector2((int)direction * MoveSpeed * elapsed, 0.0f);
150                     position = position + velocity;
151                 }
152             }
153         }
154 
155         /// <summary>
156         /// Draws the animated enemy.
157         /// </summary>
158         public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
159         {
160             // Stop running when the game is paused or before turning around.
161             if (!Level.Player.IsAlive ||
162                 Level.ReachedExit ||
163                 Level.TimeRemaining == TimeSpan.Zero ||
164                 waitTime > 0)
165             {
166                 sprite.PlayAnimation(idleAnimation);
167             }
168             else
169             {
170                 sprite.PlayAnimation(runAnimation);
171             }
172 
173 
174             // Draw facing the way the enemy is moving.
175             SpriteEffects flip = direction > 0 ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
176             sprite.Draw(gameTime, spriteBatch, Position, flip);
177         }
178     }
179 }
180 

 

 

 

热情期盼喜欢本文的园友点击文章下方的"推荐",非常感谢!

 

posted @ 2010-12-21 07:52  軒轅  阅读(1975)  评论(2编辑  收藏  举报