XNA之RGP游戏开发教程之七
这节中我们将会为游戏中添加一个动态精灵;实际上精灵移动的实现跟我们最初的动画实现相似,都是通过重复绘制精灵使得产生一种精灵运动的错觉,当绘制速度达到一定值后就会实现动态精灵。先从http://xnagpa.net/xna4/downloads/playersprites.zip上下载精灵图片
在EyesOfTheDragonContent项目下添加一个新的文件夹PlayerSprites,将精灵图片添加进这个文件夹。有了图片接下来就是构建一个动画类Animation,来作为动态精灵的基类
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; namespace XRpgLibrary.SpriteClasses { public enum AnimationKey { Down, Left, Right, Up }//动态精灵所对应的方向 public class Animation : ICloneable { #region Field Region Rectangle[] frames;//精灵帧的矩形框 int framesPerSecond;//帧频率变量 TimeSpan frameLength;//相邻帧之间的时间间隔 TimeSpan frameTimer; int currentFrame;//当前帧的索引 int frameWidth;//帧的宽度和高度 int frameHeight; #endregion #region Property Region public int FramesPerSecond { get { return framesPerSecond; } set {
//对帧频的限定,要保持在1到60之间 if (value < 1) framesPerSecond = 1; else if (value > 60) framesPerSecond = 60; else framesPerSecond = value; frameLength = TimeSpan.FromSeconds(1 / (double)framesPerSecond);//初始化帧之间的时间间隔 } } public Rectangle CurrentFrameRect { get { return frames[currentFrame]; } } public int CurrentFrame { get { return currentFrame; } set { currentFrame = (int)MathHelper.Clamp(value, 0, frames.Length - 1);//限定当前帧索引的范围 } } public int FrameWidth { get { return frameWidth; } } public int FrameHeight { get { return frameHeight; } } #endregion #region Constructor Region其中xOffset和yOffset的作用是实现在不同行不同列上截取图片
public Animation(int frameCount, int frameWidth, int frameHeight, int xOffset, int yOffset) { frames = new Rectangle[frameCount];//初始化该动画的帧集 this.frameWidth = frameWidth; this.frameHeight = frameHeight; for (int i = 0; i < frameCount; i++)//根据选定的行来实例化动画中的帧图 { frames[i] = new Rectangle( xOffset + (frameWidth * i), yOffset, frameWidth, frameHeight); } FramesPerSecond = 5;//设置帧频 Reset(); }
//直接用动画来初始化 private Animation(Animation animation) { this.frames = animation.frames; FramesPerSecond = 5; } #endregion #region Method Region动画中帧的更新 public void Update(GameTime gameTime) { frameTimer += gameTime.ElapsedGameTime; if (frameTimer >= frameLength) { frameTimer = TimeSpan.Zero; currentFrame = (currentFrame + 1) % frames.Length;//当时间间隔超过设定值时间,翻到下一帧 } } public void Reset() { currentFrame = 0;//重置当前的帧索引和时间间隔的值 frameTimer = TimeSpan.Zero; } #endregion #region Interface Method Region public object Clone() { Animation animationClone = new Animation(this); animationClone.frameWidth = this.frameWidth; animationClone.frameHeight = this.frameHeight; animationClone.Reset(); return animationClone; } #endregion } }
在以上代码中要说明两个地方,一个就是第一个构造函数,其中通过xOffset和yOffset来选择不同行的帧图片,从精灵图中可以看到,各个方向的图片在同一行中,所以对于图片的加载是以行为单位,我们可以通过设定构造函数的参数为3,32,32,0,0来获取第一行的三个精灵图片,也可以通过参数3,32,32,0,32来获取第二行的三个精灵图片...第二个要说明的地方就是Update方法中通过模运算,使得图片索引在0,1,2之间交替,对应的是精灵图片中一行的三幅图片
有了动画的基类,接下来就是构建活动的精灵类
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using XRpgLibrary.TileEngine; namespace XRpgLibrary.SpriteClasses { public class AnimatedSprite { #region Field Region Dictionary<AnimationKey, Animation> animations;//字典类,键值对存储 AnimationKey currentAnimation;//精灵当前的状态 bool isAnimating;//精灵当前是否活动 Texture2D texture;//精灵图片
Vector2 position;//精灵位置 Vector2 velocity;//精灵移动方向向量 float speed = 2.0f;//精灵移动速度 #endregion #region Property Region public AnimationKey CurrentAnimation { get { return currentAnimation; } set { currentAnimation = value; } } public bool IsAnimating { get { return isAnimating; } set { isAnimating = value; } } public int Width { get { return animations[currentAnimation].FrameWidth; } } public int Height { get { return animations[currentAnimation].FrameHeight; } } public float Speed { get { return speed; } set { speed = MathHelper.Clamp(speed, 1.0f, 16.0f); } } public Vector2 Position { get { return position; } set { position = value; } } public Vector2 Velocity { get { return velocity; } set { velocity = value; if (velocity != Vector2.Zero) velocity.Normalize();//方向向量要单位化后才能乘以速度,表示精灵的移动 } } #endregion #region Constructor Region public AnimatedSprite(Texture2D sprite, Dictionary<AnimationKey, Animation> animation) { texture = sprite; animations = new Dictionary<AnimationKey, Animation>(); foreach (AnimationKey key in animation.Keys) animations.Add(key, (Animation)animation[key].Clone()); } #endregion #region Method Region public void Update(GameTime gameTime) { if (isAnimating) animations[currentAnimation].Update(gameTime); }
//关于精灵的绘制要注意一点,在地图的绘制中要减去Camera的位置坐标,所以在绘制地图中的任何对象时都要减去Camera的位置坐标 public void Draw(GameTime gameTime, SpriteBatch spriteBatch, Camera camera) { spriteBatch.Draw( texture, position - camera.Position, animations[currentAnimation].CurrentFrameRect, Color.White); }
//锁定精灵的位置,防止其移出游戏窗体 public void LockToMap() { position.X = MathHelper.Clamp(position.X, 0, TileMap.WidthInPixels - Width); position.Y = MathHelper.Clamp(position.Y, 0, TileMap.HeightInPixels - Height); } #endregion } }
创建好精灵类后,在GamePlayScreen中添加活动精灵,在LoadContent方法中加载所要的资源
using XRpgLibrary.SpriteClasses; AnimatedSprite sprite; protected override void LoadContent() { Texture2D spriteSheet = Game.Content.Load<Texture2D>(@"PlayerSprites\malefighter");//加载活动精灵图片 Dictionary<AnimationKey, Animation> animations = new Dictionary<AnimationKey, Animation>();//定义活动精灵动画集合 Animation animation = new Animation(3, 32, 32, 0, 0);//选取图片中的第一行来构造Animation对象,其对应的是Down状态的精灵 animations.Add(AnimationKey.Down, animation);//将活动精灵动画对象加入集合,用于创建精灵 animation = new Animation(3, 32, 32, 0, 32);//图片第二行 animations.Add(AnimationKey.Left, animation); animation = new Animation(3, 32, 32, 0, 64);//图片第三行 animations.Add(AnimationKey.Right, animation); animation = new Animation(3, 32, 32, 0, 96);//图片第四行 animations.Add(AnimationKey.Up, animation); sprite = new AnimatedSprite(spriteSheet, animations);//创建精灵 base.LoadContent();
//以上是代码增加部分 Texture2D tilesetTexture = Game.Content.Load<Texture2D>(@"Tilesets\tileset1"); Tileset tileset1 = new Tileset(tilesetTexture, 8, 8, 32, 32); tilesetTexture = Game.Content.Load<Texture2D>(@"Tilesets\tileset2"); Tileset tileset2 = new Tileset(tilesetTexture, 8, 8, 32, 32); List<Tileset> tilesets = new List<Tileset>(); tilesets.Add(tileset1); tilesets.Add(tileset2); MapLayer layer = new MapLayer(40, 40); for (int y = 0; y < layer.Height; y++) { for (int x = 0; x < layer.Width; x++) { Tile tile = new Tile(0, 0); layer.SetTile(x, y, tile); } } MapLayer splatter = new MapLayer(40, 40); Random random = new Random(); for (int i = 0; i < 80; i++) { int x = random.Next(0, 40); int y = random.Next(0, 40); int index = random.Next(2, 14); Tile tile = new Tile(index, 0); splatter.SetTile(x, y, tile); } splatter.SetTile(1, 0, new Tile(0, 1)); splatter.SetTile(2, 0, new Tile(2, 1)); splatter.SetTile(3, 0, new Tile(0, 1)); List<MapLayer> mapLayers = new List<MapLayer>(); mapLayers.Add(layer); mapLayers.Add(splatter); map = new TileMap(tilesets, mapLayers); }
更新Update和Draw方法如下
public override void Update(GameTime gameTime) { player.Update(gameTime); sprite.Update(gameTime);//更新精灵 base.Update(gameTime); } public override void Draw(GameTime gameTime) { GameRef.SpriteBatch.Begin( SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp,null, null, null, Matrix.Identity); map.Draw(GameRef.SpriteBatch, player.Camera); sprite.Draw(gameTime, GameRef.SpriteBatch, player.Camera);//绘制精灵 base.Draw(gameTime); GameRef.SpriteBatch.End(); }
这个时候精灵还是不会动,它还是位于地图的左上角,我们即想让精灵可以自由的移动,也想通过将精灵和Camera关联,实现通过精灵扩充地图,这样Camera就会有两种状态:Free或者Fellow,在Free状态下Camera还是通过玩家按上下左右键来拓展地图,游戏精灵通过自己的方向按键(通常定义为w,a,s,d)来移动,这样的话会看到精灵移出窗体;在Fellow状态下,Camera和游戏精灵关联,通过控制精灵来拓展地图;修改Camera类代码如下
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using XRpgLibrary.SpriteClasses; namespace XRpgLibrary.TileEngine { public enum CameraMode { Free, Follow }//camera的两种状态 public class Camera { #region Field Region Vector2 position; float speed; float zoom; Rectangle viewportRectangle; CameraMode mode;//camera状态的类级变量 #endregion #region Property Region public Vector2 Position { get { return position; } private set { position = value; } } public float Speed { get { return speed; } set { speed = (float)MathHelper.Clamp(speed, 1f, 16f); } } public float Zoom { get { return zoom; } } public CameraMode CameraMode { get { return mode; } } #endregion #region Constructor Region public Camera(Rectangle viewportRect) { speed = 4f; zoom = 1f; viewportRectangle = viewportRect; mode = CameraMode.Follow;//初始化摄像头为Follow状态 } public Camera(Rectangle viewportRect, Vector2 position) { speed = 4f; zoom = 1f; viewportRectangle = viewportRect; Position = position; mode = CameraMode.Follow;// } #endregion #region Method Region public void Update(GameTime gameTime) { if (mode == CameraMode.Follow)//如果为Follow状态,不再执行Camera自身的Update方法 return; Vector2 motion = Vector2.Zero; if (InputHandler.KeyDown(Keys.Left) || InputHandler.ButtonDown(Buttons.RightThumbstickLeft, PlayerIndex.One)) motion.X = -speed; else if (InputHandler.KeyDown(Keys.Right) || InputHandler.ButtonDown(Buttons.RightThumbstickRight, PlayerIndex.One)) motion.X = speed; if (InputHandler.KeyDown(Keys.Up) || InputHandler.ButtonDown(Buttons.RightThumbstickUp, PlayerIndex.One)) motion.Y = -speed; else if (InputHandler.KeyDown(Keys.Down) || InputHandler.ButtonDown(Buttons.RightThumbstickDown, PlayerIndex.One)) motion.Y = speed; if (motion != Vector2.Zero) { motion.Normalize(); position += motion * speed; LockCamera(); } } private void LockCamera() { position.X = MathHelper.Clamp(position.X, 0, TileMap.WidthInPixels - viewportRectangle.Width); position.Y = MathHelper.Clamp(position.Y, 0,TileMap.HeightInPixels - viewportRectangle.Height); }
//将Camera的坐标与精灵坐标关联起来 public void LockToSprite(AnimatedSprite sprite) { position.X = sprite.Position.X + sprite.Width / 2 - (viewportRectangle.Width / 2);//当精灵移动超过当前Camera视场宽度的一半,Camera的位置才改变,才会水平方向重绘地图 position.Y = sprite.Position.Y + sprite.Height / 2 - (viewportRectangle.Height / 2); LockCamera(); }
//Camera状态的变换方法 public void ToggleCameraMode() { if (mode == CameraMode.Follow) mode = CameraMode.Free; else if (mode == CameraMode.Free) mode = CameraMode.Follow; } #endregion } }
做了这么多,现在精灵还是不能动,最后一步就是要在GameplayScreen类中修改update方法,加入精灵移动控制代码
public override void Update(GameTime gameTime) { player.Update(gameTime); sprite.Update(gameTime); Vector2 motion = new Vector2();//方向向量
//按W键,精灵向上移动 if (InputHandler.KeyDown(Keys.W) || InputHandler.ButtonDown(Buttons.LeftThumbstickUp, PlayerIndex.One)) { sprite.CurrentAnimation = AnimationKey.Up; motion.Y = -1; }
//按S键,精灵向下移动 else if (InputHandler.KeyDown(Keys.S) || InputHandler.ButtonDown(Buttons.LeftThumbstickDown, PlayerIndex.One)) { sprite.CurrentAnimation = AnimationKey.Down; motion.Y = 1; } if (InputHandler.KeyDown(Keys.A) || InputHandler.ButtonDown(Buttons.LeftThumbstickLeft, PlayerIndex.One)) { sprite.CurrentAnimation = AnimationKey.Left; motion.X = -1; } else if (InputHandler.KeyDown(Keys.D) || InputHandler.ButtonDown(Buttons.LeftThumbstickRight, PlayerIndex.One)) { sprite.CurrentAnimation = AnimationKey.Right; motion.X = 1; } if (motion != Vector2.Zero) { sprite.IsAnimating = true;//精灵发生移动,在Update方法中需要更新 motion.Normalize(); sprite.Position += motion * sprite.Speed;//单位化方向向量,并乘以速度,重置精灵位置 sprite.LockToMap();//控制精灵的位置,防止移出地图 if (player.Camera.CameraMode == CameraMode.Follow)//如果当前Camera的状态是Follow,则将精灵和Camera关联起来,Camera的位置会跟着sprite的位置移动 player.Camera.LockToSprite(sprite); } else { sprite.IsAnimating = false; }
//按F键,实现Camera状态的切换 if (InputHandler.KeyReleased(Keys.F) || InputHandler.ButtonReleased(Buttons.RightStick, PlayerIndex.One)) { player.Camera.ToggleCameraMode();
//如果切换后的Camera状态变为Follow,将其与sprite关联 if (player.Camera.CameraMode == CameraMode.Follow) player.Camera.LockToSprite(sprite); } if (player.Camera.CameraMode != CameraMode.Follow) { if (InputHandler.KeyReleased(Keys.C) || InputHandler.ButtonReleased(Buttons.LeftStick, PlayerIndex.One)) { player.Camera.LockToSprite(sprite); } } base.Update(gameTime); }
OK,通过这一节,我们实现了游戏中精灵动画,并添加到游戏页面,实现图如下