[翻译]XNA外文博客文章精选之one
PS:自己翻译的,转载请著明出处
动画精灵表单和移动的精灵
Nick Gravelyn
XNA Game Studio使它难以置信的很容易去加载并且绘制一个精灵到屏幕上。但是有一个共同的问题,如何通过使一个精灵活动而被发现精灵运动了。毕竟想象一下没有动画的超级马力奥兄弟。这将是相当枯燥,看起来怪怪的。在本文中,我提出一个方法使游戏里的精灵动起来。还有很多其他的方法来解决这个问题。某些可能工作的很好,某些可能工作的更糟。这个方法结合了良好的性能和实用性。
第一步,为了活动精灵来了解精灵是如何活动的。精灵的活动是简单的使用一个新的图象,在图象中让它显示一点小变化,用它来代替当前的图象。通过这些图象,我们可以得到一个活动的感觉。你可以改变速度和改变图片的数量去获得不同的结果。
这里是精灵表单,我们将在本教程中使用它:
你可以看见我们有四组不同的动画,每一组由两祯构成。在我们环境中,我们想从我们正绘制区域中简单改变纹理的区域。所以执行角色背对着我们的运动,我们只需要头两片图象。
首先我们可以绘制第一个连续的图象:
然后我们可以转向在规定时间内去绘制第二个图象:
然后我们重复这一过程作为需要去得到角色行走的动画。
现在,我们看见一个动画精灵是如何实现的,我们可以开始编写程序了。首先我们需要的是一个跟踪绘制的精灵到哪里的方法。幸运的是XNA Framework 有一个矩形类,我们稍后会用到它,在绘制这个精灵到指定这一点时。所以我们将首先创建一个类命名为Animation,在一个给定的动画中它将代表一系列的矩形。
因此,首先我们需要去确定这个类需要什么样的领域。我们将需要一个开始的矩形数组。我们同样需要跟踪当前的祯的索引值,这样我们可以选择哪个矩形被使用。我们的类同样需要知道一祯持续多久以及确保能准时转换祯。因此,考虑到这一点我们可以开始我们的类了:
1 public class Animation
2 {
3 Rectangle[] frames;
4 float frameLength = 1f / 5f;
5 float timer = 0f;
6 int currentFrame = 0;
7 }
在默认的情况下我们准备设置我们的祯的长度为1f/5f,它的意思是我们将每秒动画5祯。但是如果我们想要改变这个呢?接下来让我们执行一个公开的属性,我们可以使用去改变动画的速度。有一个祯的长度在我们的类中作为一个float,就象非常好的更新,但是改变设置它是有点沉闷。所以代替我们的属性将会被使用去得到并且设置每秒祯的速度,我们的属性做了所有的数学计算都在后台:
2 {
3 Rectangle[] frames;
4 float frameLength = 1f / 5f;
5 float timer = 0f;
6 int currentFrame = 0;
7 }
1 public int FramesPerSecond
2 {
3 get { return (int)(1f / frameLength); }
4 set { frameLength = 1f / (float)value; }
5 }
现在如果我们想要去改变动画,我们可以简单说FramesSecond=15,它就会比FrameLength=1f/5f要非常清楚。本质上是一回事,但是它是一个不错的用户界面。就象一个例子,你如何使用属性去改变从外部到内部。2 {
3 get { return (int)(1f / frameLength); }
4 set { frameLength = 1f / (float)value; }
5 }
那么到目前为止,我们有一个好的开始,但是我们的类还不是很有用。我们不可以得到当前的矩形,为我们的精灵去使用,所以接下来我们将添加一个只读属性去返回它:
1 public Rectangle CurrentFrame
2 {
3 get { return frames[currentFrame]; }
4 }
现在,我们有一个有用的类。接下来我们需要一个方法去创建一个矩形数组。我们可以使用户通过并且一次添加它们一个,但是这是相当麻烦的。相反,我们准备制造一个结构为我们去创建矩形。这个结构将要求动画的总宽度,动画的高度,在动画中祯的数量,然后一个矩形应该的X和Y偏移量。2 {
3 get { return frames[currentFrame]; }
4 }
让我们以我们的精灵为列子。每个单独的祯是32*32象素。这样为这个角色创建动画背朝我们的行走,我们可以使用64作为我们的总宽度(2张每张32象素)。我们可以使用32为高度和2为祯数。直到祯的表单开始,我们可以使用0作为X和Y的偏移量。
然后,如果我们想要为这个角色创建朝我们走来的动画,我们可能只有改变X偏移量,我们仍然使用2祯,我们使用同样的总宽度,高度和祯的数量。我们的动画表单只有一行,所以我们使用同样的Y偏移量。但是直到我们不是在表的最左边的位置上,我们不得不修改X的偏移量。为了得到适当的X偏移量,所有我们需要去做的是使用任何动画的总宽度到它的左边。所以X偏移量这里是64。
所以,这就是在我们例子里动画如何工作。让我们用代码创建结构:
1 public Animation(int width, int height, int numFrames, int xOffset, int yOffset)
2 {
3 frames = new Rectangle[numFrames];
4 int frameWidth = width / numFrames;
5 for (int i = 0; i < numFrames; i++)
6 {
7 frames[i] = new Rectangle(xOffset + (frameWidth * i), yOffset, frameWidth, height);
8 }
9 }
你可以看见它是相当简单明了的。当创建我们的矩形,我们使用xOffset作为矩形基础的X参数,然后添加它到每一祯。我们刚好使用这个yOffset为矩形的这Y参数并且使用frameWidth和高度分别为宽度和告诉参数。2 {
3 frames = new Rectangle[numFrames];
4 int frameWidth = width / numFrames;
5 for (int i = 0; i < numFrames; i++)
6 {
7 frames[i] = new Rectangle(xOffset + (frameWidth * i), yOffset, frameWidth, height);
8 }
9 }
现在我们可以创建我们的动画和得到从它们中返回的信息。但是我们仍然没有产生任何实际的动画。为了解决这个我们将需要另外的方法来更新。更新是一个简单的过程。我们添加这些游戏的elapsed时间到我们的计时器里。如果计时器大于或等于祯的长度,我们增加当前的祯并且重置计时器,。这个过程正好重复。所以,我们的Update方法象下面现在的样子:
1 public void Update(GameTime gameTime)
2 {
3 timer += (float)gameTime.ElapsedGameTime.TotalSeconds;
4 if (timer >= frameLength)
5 {
6 timer = 0f;
7 currentFrame = (currentFrame + 1) % frames.Length;
8 }
9 }
这里所有的都相当容易读。这一个trick我们使用的是模运算符(%).通过添加一个到当前祯然后使用模运算符在祯数组的长度中,我们确保currentFrame总是在数组的边界上。这确保我们不会得到一个无效的帧数组的索引。2 {
3 timer += (float)gameTime.ElapsedGameTime.TotalSeconds;
4 if (timer >= frameLength)
5 {
6 timer = 0f;
7 currentFrame = (currentFrame + 1) % frames.Length;
8 }
9 }
最后我们需要一个容易的方法去重置动画退回到起点。所以我们简单设置计时器为0,并且设置当前的祯为0:
1 public void Reset()
2 {
3 currentFrame = 0;
4 timer = 0f;
5 }
这对动画类。你可以使用这个通过它自己的去跟踪你的动画,但是我们将稍后扩展它,去有一个独立的精灵有一系列的动画。我们将调用这个类AnimatedSprite.2 {
3 currentFrame = 0;
4 timer = 0f;
5 }
为了开始我们的AnimatedSprite类,让我们决定我们需要什么领域。所有的精灵需要一个位置,这样我们将包括他们中的一个,我们同样需要一个容器去保存动画。我们需要一个纹理,这样我们可以绘制它。我们同样需要去保存当前的动画以及我们是否真正想成为的动画(这个方法我们可以用来关闭/打开动画)。所以让我们开始我们的动画类:
1 public class AnimatingSprite
2 {
3 public Vector2 Position = Vector2.Zero;
4 public Dictionary<string, Animation> Animations = new Dictionary<string, Animation>();
5 Texture2D texture;
6 string currentAnimation;
7 bool updateAnimation = true;
8 }
你可以看见我选择用一个字典类(Dictionary)去保存Animations在里面。这是因为它将使它很容易让程序去选择一个动画,通过名字而不是使用整数索引。缺点是可能会引起我们去创建字符串去改变动画,它将导致垃圾的产生。有方法绕开它,但是配合我们可用性的目标,我们将使用字符串。2 {
3 public Vector2 Position = Vector2.Zero;
4 public Dictionary<string, Animation> Animations = new Dictionary<string, Animation>();
5 Texture2D texture;
6 string currentAnimation;
7 bool updateAnimation = true;
8 }
接下来,我们需要设计我们如何得到或者设置数据在这个类中。我们使位置和动画字典公有,这样用户可以根据他们的需要容易的修改。这样我们将需要其它的属性或者方法。接下来,我们需要为这个纹理创建一个简单的存取访问属性,这样我们可以设置精灵的纹理:
1 public Texture2D Texture
2 {
3 get { return texture; }
4 set { texture = value; }
5 }
下一步,我们想要可以容易的改变当前的动画。所以我们需要一个属性。虽然我们想要做一些错误检查。我们想要确保用户可以能够设置当前的活动成为一个动画在我们的字典中。所以这里是我们的属性应该的样子:
2 {
3 get { return texture; }
4 set { texture = value; }
5 }
1 public string CurrentAnimation
2 {
3 get { return currentAnimation; }
4 set
5 {
6 if (!Animations.ContainsKey(value))
7 throw new Exception("Invalid animation specified.");
8 if (currentAnimation == null || !currentAnimation.Equals(value))
9 {
10 currentAnimation = value;
11 Animations[currentAnimation].Reset();
12 }
13 }
14 }
你们将会看见get部分非常简单,而set部分是我们的逻辑部分。首先我们确保,给定的字符串是一个有效键在这个字典中。如果它不是,我们抛出一个异常,这样用户可以处理它。之后我们要看看我们是否想改变动画。我们通过检查currentAnimation是否已经设置了(因此它仍然为空)或者新的值不同于旧的值。如果过期,我们更新currentAnimation,然后重新设置动画到起始状态。2 {
3 get { return currentAnimation; }
4 set
5 {
6 if (!Animations.ContainsKey(value))
7 throw new Exception("Invalid animation specified.");
8 if (currentAnimation == null || !currentAnimation.Equals(value))
9 {
10 currentAnimation = value;
11 Animations[currentAnimation].Reset();
12 }
13 }
14 }
现在我们需要一个方法去停止和开始动画,我们将做这些使用两个简单的方法:
1 public void StartAnimation()
2 {
3 updateAnimation = true;
4 }
5 public void StopAnimation()
6 {
7 updateAnimation = false;
8 }
现在,所有的左边正在更新并且绘制一个精灵。我们开始更新。直到我们已经有一个方法去更新Animation类,这个方法可能非常简单:
2 {
3 updateAnimation = true;
4 }
5 public void StopAnimation()
6 {
7 updateAnimation = false;
8 }
1 public void Update(GameTime gameTime)
2 {
3 if (updateAnimation)
4 Animations[currentAnimation].Update(gameTime);
5 }
这是个潜在的工作,但是我们没有问题。当这个类被创建,没有值被保证在currentAnimation字符串中。这将导致一个异常被抛出。我们不想这样,所以我们需要确保在更新前我们有一个动画设置。所以我们将从上述新方法中改变我们的update方法:
2 {
3 if (updateAnimation)
4 Animations[currentAnimation].Update(gameTime);
5 }
1 public void Update(GameTime gameTime)
2 {
3 if (currentAnimation == null)
4 {
5 if (Animations.Keys.Count == 0)
6 return;
7 string[] keys = new string[Animations.Keys.Count];
8 Animations.Keys.CopyTo(keys, 0);
9 currentAnimation = keys[0];
10 }
11 if (updateAnimation)
12 Animations[currentAnimation].Update(gameTime);
13 }
我们首先要做的是检查currentAnimation是否已是空。如果它是(意思没有动画被设置)我们不得不得到一个动画去使用。首先我们看看,我们是否有动画在我们的字典中。如果一个也没有,我们只能退出Update方法因为这里什么都不用我们去做。如果这里没有动画,我们从字典中得到键的数组,并且设置我们的currentAnimation到第一个中。然后我们更新就象我们的旧方法那样。2 {
3 if (currentAnimation == null)
4 {
5 if (Animations.Keys.Count == 0)
6 return;
7 string[] keys = new string[Animations.Keys.Count];
8 Animations.Keys.CopyTo(keys, 0);
9 currentAnimation = keys[0];
10 }
11 if (updateAnimation)
12 Animations[currentAnimation].Update(gameTime);
13 }
现在我们有一个很好,很安全的Update方法,它将避免很多错误。现在我们只需要去绘制精灵。我们将添加一个小的绘制方法,它接收一个SpriteBatch作为一个参数和绘制精灵在它的上面。
1 public void Draw(SpriteBatch batch)
2 {
3 batch.Draw(texture,Position,Animations[currentAnimation].CurrentFrame,Color.White);
4 }
你会看见,我们使用一个基本的绘制方法,它他的纹理,位置,并且使用白色。我们同样指定一个源矩形,我们从我们当前的动画中得到它。2 {
3 batch.Draw(texture,Position,Animations[currentAnimation].CurrentFrame,Color.White);
4 }
现在我们有一个完整的AnimateSprite类!我们可以接受这个,并且使用它去绘制所有的你想要的动画精灵。让我们从一开始就使用。
回到你游戏的类的开头添加下面两行:
1 SpriteBatch batch;
2 AnimatingSprite sprite;
接下来移动到初始方法中,我们将创建我们的精灵Animations和设置精灵到一个好位置:
2 AnimatingSprite sprite;
1 protected override void Initialize()
2 {
3 sprite = new AnimatingSprite();
4 Animation up = new Animation(64, 32, 2, 0, 0);
5 Animation down = new Animation(64, 32, 2, 64, 0);
6 Animation left = new Animation(64, 32, 2, 128, 0);
7 Animation right = new Animation(64, 32, 2, 192, 0);
8 sprite.Animations.Add("Up", up);
9 sprite.Animations.Add("Down", down);
10 sprite.Animations.Add("Left", left);
11 sprite.Animations.Add("Right", right);
12 sprite.Position = new Vector2(100f);
13 base.Initialize();
14 }
你会看见正如我们描述的使用的Animation结构。所有这些值都是相同的在动画除了xOffset参数(第四个参数)之间。然后我们添加我们的动画中的每一个到精灵到字典中,用很容易回忆起它们的名字。然后我们设置我们的精灵在这个点(100,100)上。 2 {
3 sprite = new AnimatingSprite();
4 Animation up = new Animation(64, 32, 2, 0, 0);
5 Animation down = new Animation(64, 32, 2, 64, 0);
6 Animation left = new Animation(64, 32, 2, 128, 0);
7 Animation right = new Animation(64, 32, 2, 192, 0);
8 sprite.Animations.Add("Up", up);
9 sprite.Animations.Add("Down", down);
10 sprite.Animations.Add("Left", left);
11 sprite.Animations.Add("Right", right);
12 sprite.Position = new Vector2(100f);
13 base.Initialize();
14 }
接下来,我们创建SpriteBatch和加载我们的精灵的威力,这样移动到LoadContent方法中:
1 protected override void LoadContent()
2 {
3 batch = new SpriteBatch(graphics.GraphicsDevice);
4 sprite.Texture = content.Load<Texture2D>(@"Content/knt1");
5 }
没有什么新东西,我们创建SpriteBatch就象平时并且加载纹理也象平时一样。依赖于你的项目,你可以在Content.Load方法中调整资源的名字。2 {
3 batch = new SpriteBatch(graphics.GraphicsDevice);
4 sprite.Texture = content.Load<Texture2D>(@"Content/knt1");
5 }
下面,我们不得不确保我们的精灵得到更新。只需要添加下面的行到你的Update方法里:
1 sprite.Update(gameTime);
最后,我们不得不绘制这个精灵。移动到Draw方法并且添加下面的三行代码:
1 batch.Begin();
2 sprite.Draw(batch);
3 batch.End();
做的不错!运行游戏并且你会看见我们小角色在散步。现在在你自己的项目中你有一个工作的精灵动画设置去使用:2 sprite.Draw(batch);
3 batch.End();
为了完成这个教程我们将介绍如何移动我们的精灵围绕屏幕并且确保这个动画属性被处理。因为我们的角色只有4个初始方向的动画,我们将限制我们的运动到这些4个方向。为了这样我们将使用GamePad.也欢迎你使用键盘达到这个目的,代码包含在页面的底下,但由于这类型的运动映射到手柄上,我们将覆盖它代替这部分。
因此,这个运动图是如何精确反映到手柄上的?想象一下,我们分操纵杆如图4个部分:
所以,为了移动我们的角色,我们不得不检测手柄的那部分按下,角色就朝向哪部分,它是检查角色的移动。因此添加下面的方法到你的游戏中:
1 private void GetThumbstickInput()
2 {
3 }
添加一个调用到这个方法之前的sprite.Update(gameTime);在你Update方法中。2 {
3 }
现在我们填充我们的GetThumbstickMethod.首先我们需要得到左手柄的位置:
1 Vector2 leftStick = GamePad.GetState(PlayerIndex.One).ThumbSticks.Left;
接下来我们将做一个小的检查。我们只想要我们的角色移动和活动,如果用户压下手柄的键一点。所以下面我们添加下面的几行代码:
1 if (leftStick.Length() > 0.2f)
2 {
3 sprite.StartAnimation();
4 }
5 else
6 {
7 sprite.StopAnimation();
8 }
你会看见,如果手柄被压下超过1/5的方式远离中心,我们将活动我们的精灵。如果不是,我们告诉我们的精灵停止移动。2 {
3 sprite.StartAnimation();
4 }
5 else
6 {
7 sprite.StopAnimation();
8 }
现在我们不得不处理移动的精灵。这部分帮助如果你明白一点三角法则,但是如果不明白,我试图去解释它用一个简单的方式。让我们回到操纵杆的显示中来:
让我们先想象下从两个角中延长出来的四条对角线。在三角法则里角度一般被用来用弧度表示。整个圆周的长度,例如,2*pi的弧度。为什么是pi?Pi是一个圆的圆周弧度。对我们来说幸运的是,这里已经为Pi的许多变量创建了值,我们将使用它在MathHelper类中。
因此返回到我们的图象中。现在我们可以找到每个单独直线的值。例如,在两个区域的直线一个和两个可能是在Pi/4或者MathHelper.PiOver4弧度。两个区域的直线两个和四个可能在3*pi/4或者3*MathHelper.PiOver4弧度。我们可以完全一致的正方向或者负方向围绕这个圆。
在我们的代码中,我们将不得不找到角度,它是我们的操纵杆的朝向。我们通过使用Math.Atan2方法来实现。下面的代码将进入到if语句在sprite.StartAnimation();之前:
1 float stickAngle = (float)Math.Atan2(leftStick.Y, leftStick.X);
有一件是需要注意的,关于Math.Atan2方法,它返回一个值在-Pi和Pi之间。所以我们的逻辑将不得不基于这个稍微的调整。这解释了编号对应的操纵杆的区域。我们首先将检查用户想要向右,向上,向下,向左移动。首先我们将检查用户想要向右。正如我们所说的,直线在区域一和二之间的是Pi/4.直线在区域一和三之间的是-Pi/4.所以我们检查就象这样:
1 if (stickAngle > -MathHelper.PiOver4 &&stickAngle < MathHelper.PiOver4)
2 {
3 sprite.CurrentAnimation = "Right";
4 sprite.Position.X++;
5 }
它正是一个简单的if语句为这个检查。如果通过,我们设置精灵的活动向右并且移动精灵向左。2 {
3 sprite.CurrentAnimation = "Right";
4 sprite.Position.X++;
5 }
接下来,我们将检查用户移动向上和向下用一个简单的方式:
1 else if (stickAngle > MathHelper.PiOver4 &&stickAngle < 3f * MathHelper.PiOver4)
2 {
3 sprite.CurrentAnimation = "Up";
4 sprite.Position.Y--;
5 }
6 else if (stickAngle > -(3.0f * MathHelper.PiOver4) &&stickAngle < -MathHelper.PiOver4)
7 {
8 sprite.CurrentAnimation = "Down";
9 sprite.Position.Y++;
10 }
现在我们添加向上,向下,向右移动。所有这些仍然向左边。用这个方法移动想左边,是很难进行数学检查。这个值将会warp around因为我们的Math.Atan2方法返回值在-Pi和Pi之间。所以我们简单离开这个最终的检查并且使它跳出这种情况。这使我们的工作比试图为它去写数学检查要容易的多。
2 {
3 sprite.CurrentAnimation = "Up";
4 sprite.Position.Y--;
5 }
6 else if (stickAngle > -(3.0f * MathHelper.PiOver4) &&stickAngle < -MathHelper.PiOver4)
7 {
8 sprite.CurrentAnimation = "Down";
9 sprite.Position.Y++;
10 }
1 else
2 {
3 sprite.CurrentAnimation = "Left";
4 sprite.Position.X--;
5 }
这是它所需要的。现在如果你运行这个游戏,你应该可以移动你的角色在这四个方向,使用GamePad的操纵杆。这种感觉很自然的映射到操纵杆。2 {
3 sprite.CurrentAnimation = "Left";
4 sprite.Position.X--;
5 }
代码:http://www.ziggyware.com/readarticle.php?article_id=138
(完)