早段时间闲着无聊上了上优酷,无意中看到了一些音乐大牛们使用各种乐器弹奏红白机《魂斗罗》游戏的背景音乐(对于不知道《魂斗罗》这款游戏的朋友我只能表示无语了),有些Remix版本的音乐听到热血沸腾,不禁又回忆起小时候去同学家里打爆机的美好时光。一时间头脑发热,凭着自己的兴趣打算使用Microsoft Xna来重新打造《魂斗罗》第一代游戏第一关Jungle Juncture。不过嘛,由于之前没有游戏开发的经验,同时也从来没有接触过Microsoft Xna,只是在10年前用Delphi帮同学写过几个基于DirectX的MIDI播放插件,对于Microsoft Xna我真的是现学现卖,所以我也没有完全的把握能够真正打造出这个小游戏。OK,言归正传,在这里就谈谈最近在这方面的研究和学习成果和经验,同时分享一些设计方面的思想。
精灵表(Sprite Sheet)查看器
我首先使用C#/.NET开发了一个基于Windows Forms的集成游戏设计环境(我称之为Apworks Xna Tools,名字随便取的,还没确定最后用啥名字,还是沿用我之前做的Apworks应用开发框架的名字算了),目前这个环境还是很简单的,只是为各种不同的插件提供了一个统一的运行环境;然后,我开发了一个精灵表(Sprite Sheet)查看器(Sprite Sheet Inspector),它的功能很直观,因为大家都知道其实在2D游戏中,精灵的一系列动作都是来自于Sprite Sheet的每一帧,然后以一个固定的FPS频率轮询替换以实现动画效果。那么精灵表查看器的目的就是让开发者能够打开一张预先定义好每个动作每个帧的图片(称之为Sprite Sheet),然后根据这个图片分别定义动作序列。例如,首先我们可以新建一个Apworks Xna Tools的项目,项目模板选择Sprite Sheet Inspector,在Project File Name中输入要保存的项目文件名(注意:以后所有的输出文件都会保存在这个项目文件所在的目录下),然后在Sprite Sheet File中选择一张精灵表的图片。如下所示:
为了描述方便,我这里就打开一个已经编辑好的项目,如下所示:
图中左边的树状列表定义了单个Sprite的所有动作类型,比如对于我们的魂斗罗主角,它可以是往左跑、往右跑或者面向左边或者右边站立,等等。每个动作都是由Sprite Sheet上每一个帧拼接而成的。例如,我们可以看到,向右跑(Right)的动作由10个来自于Sprite Sheet上的帧组成。中间的图片编辑器就是用来选取Sprite Sheet上的每一帧,然后可以将选取的帧添加到动作序列中。为了能够更加精确地选取图像,在右边提供了一个缩放窗口,可以将鼠标所在位置附近的图像放大,以便精确定位。
在左下方的Preview窗格中,可以预览所选中的动作类型的动画效果,例如在上图中选中了Right的动作类型,然后单击“开始”按钮,图片框中的魂斗罗主角就会以设定的FPS速度向右跑动起来。
自动化代码生成
当所有的动作都定义好以后,我们可以单击工具栏的“产生输出文件”按钮以产生该Sprite的源程序代码(目前支持VB.NET和C#两种语言)。比如上面的Sprite Sheet Inspector Project会产生类似如下的代码:
//------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. // Runtime Version:4.0.30319.488 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ namespace ContraGame { using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; public enum ContraSpriteActions { Left, Right, UpLeft, UpRight, DownLeft, DownRight, StandLeft, StandRight, } public partial class ContraSprite { private Microsoft.Xna.Framework.Game game; private Microsoft.Xna.Framework.Graphics.Texture2D texture; private Dictionary<ContraSpriteActions, List<Rectangle>> spriteActionMatrix; private int fps = 10; private float secondsPerFrame; private ContraSpriteActions currentAction; private int currentFrameIndex = 0; private float currentElapsed = 0F; private float x = 0; private float y = 0; #region Ctor public ContraSprite(Game game) { this.game = game; this.texture = this.game.Content.Load<Texture2D>("ContraSpriteSheet"); this.spriteActionMatrix = new Dictionary<ContraSpriteActions, List<Rectangle>>(); this.InitializeSpriteActionMatrix(); this.secondsPerFrame = (1F / this.fps); } #endregion protected Dictionary<ContraSpriteActions, List<Rectangle>> SpriteActionMatrix { get { return this.spriteActionMatrix; } } protected int CurrentFrameIndex { get { return this.currentFrameIndex; } } protected ContraSpriteActions CurrentAction { get { return this.currentAction; } set { if ((this.currentAction != value)) { this.OnUpdateCurrentAction(); } this.currentAction = value; } } public float X { get { return this.x; } set { this.x = value; } } public float Y { get { return this.y; } set { this.y = value; } } private void OnUpdateCurrentAction() { this.currentFrameIndex = 0; } protected virtual void InitializeSpriteActionMatrix() { this.spriteActionMatrix.Clear(); List<Rectangle> leftActionRectangles = new List<Rectangle>(); leftActionRectangles.Add(new Rectangle(131, 81, 39, 48)); leftActionRectangles.Add(new Rectangle(164, 83, 39, 48)); leftActionRectangles.Add(new Rectangle(90, 79, 39, 48)); leftActionRectangles.Add(new Rectangle(90, 79, 39, 48)); leftActionRectangles.Add(new Rectangle(46, 84, 39, 48)); leftActionRectangles.Add(new Rectangle(10, 83, 39, 48)); leftActionRectangles.Add(new Rectangle(46, 84, 39, 48)); leftActionRectangles.Add(new Rectangle(90, 79, 39, 48)); leftActionRectangles.Add(new Rectangle(90, 79, 39, 48)); leftActionRectangles.Add(new Rectangle(164, 83, 39, 48)); this.spriteActionMatrix.Add(ContraSpriteActions.Left, leftActionRectangles); List<Microsoft.Xna.Framework.Rectangle> rightActionRectangles = new List<Rectangle>(); rightActionRectangles.Add(new Rectangle(255, 81, 39, 48)); rightActionRectangles.Add(new Rectangle(216, 82, 39, 48)); rightActionRectangles.Add(new Rectangle(298, 81, 39, 48)); rightActionRectangles.Add(new Rectangle(298, 81, 39, 48)); rightActionRectangles.Add(new Rectangle(338, 81, 39, 48)); rightActionRectangles.Add(new Rectangle(381, 81, 39, 48)); rightActionRectangles.Add(new Rectangle(338, 81, 39, 48)); rightActionRectangles.Add(new Rectangle(298, 81, 39, 48)); rightActionRectangles.Add(new Rectangle(298, 81, 39, 48)); rightActionRectangles.Add(new Rectangle(255, 81, 39, 48)); this.spriteActionMatrix.Add(ContraSpriteActions.Right, rightActionRectangles); List<Rectangle> upLeftActionRectangles = new List<Rectangle>(); upLeftActionRectangles.Add(new Rectangle(28, 21, 39, 48)); this.spriteActionMatrix.Add(ContraSpriteActions.UpLeft, upLeftActionRectangles); List<Rectangle> upRightActionRectangles = new List<Rectangle>(); upRightActionRectangles.Add(new Rectangle(83, 21, 39, 48)); this.spriteActionMatrix.Add(ContraSpriteActions.UpRight, upRightActionRectangles); List<Rectangle> downLeftActionRectangles = new List<Microsoft.Xna.Framework.Rectangle>(); downLeftActionRectangles.Add(new Rectangle(141, 15, 39, 48)); this.spriteActionMatrix.Add(ContraSpriteActions.DownLeft, downLeftActionRectangles); List<Rectangle> downRightActionRectangles = new List<Rectangle>(); downRightActionRectangles.Add(new Rectangle(209, 16, 39, 48)); this.spriteActionMatrix.Add(ContraSpriteActions.DownRight, downRightActionRectangles); List<Rectangle> standLeftActionRectangles = new List<Rectangle>(); standLeftActionRectangles.Add(new Microsoft.Xna.Framework.Rectangle(23, 170, 39, 48)); this.spriteActionMatrix.Add(ContraSpriteActions.StandLeft, standLeftActionRectangles); List<Rectangle> standRightActionRectangles = new List<Rectangle>(); standRightActionRectangles.Add(new Rectangle(93, 169, 39, 48)); this.spriteActionMatrix.Add(ContraSpriteActions.StandRight, standRightActionRectangles); } protected void IncreaseCurrentFrameIndex() { if ((this.currentFrameIndex < (this.spriteActionMatrix[this.currentAction].Count - 1))) { this.currentFrameIndex = (this.currentFrameIndex + 1); } else { this.currentFrameIndex = 0; } } public virtual void Update(Microsoft.Xna.Framework.GameTime gameTime) { this.currentElapsed = (this.currentElapsed + ((float)(gameTime.ElapsedGameTime.TotalSeconds))); if ((this.currentElapsed > this.secondsPerFrame)) { this.currentElapsed = 0; this.IncreaseCurrentFrameIndex(); } } public virtual void DrawGameTime gameTime, SpriteBatch spriteBatch) { List<Rectangle> actionRectangles = this.spriteActionMatrix[this.currentAction]; Rectangle currentRectangle = actionRectangles[this.currentFrameIndex]; spriteBatch.Draw(this.texture, new Vector2(this.x, this.y), currentRectangle, Color.White); } } }
这使得Sprite Sheet Inspector看上去更像一种基于Sprite Sheet来开发Sprite动作序列并自动化产生代码的“领域特定语言”,不错,这就是DSL在游戏领域的应用。我想,如果能把更多的开发任务以DSL的方式进行描述,那不仅将大大减少代码编写所花费的时间和错误,而且还能够实现代码的重用。
为主角添加控制逻辑
现在我们可以在Visual Studio中新建一个Windows Game(4.0)的项目,将上面产生的代码引进来,同时,写一个继承于该类(ContraSprite)的类如下:
public class ContraSpriteImpl : ContraSprite { private int currentDirection = 1; public ContraSpriteImpl(Game game) : base(game) { this.CurrentAction = ContraSpriteActions.StandRight; } public override void Update(GameTime gameTime) { KeyboardState state = Keyboard.GetState(); if (state.IsKeyDown(Keys.D)) { currentDirection = 1; this.X += 1.5F; this.CurrentAction = ContraSpriteActions.Right; } else if (state.IsKeyDown(Keys.A)) { currentDirection = -1; this.X -= 1.5F; this.CurrentAction = ContraSpriteActions.Left; } else if (state.IsKeyDown(Keys.W)) { if (currentDirection > 0) this.CurrentAction = ContraSpriteActions.UpRight; else this.CurrentAction = ContraSpriteActions.UpLeft; } else if (state.IsKeyDown(Keys.S)) { if (currentDirection > 0) this.CurrentAction = ContraSpriteActions.DownRight; else this.CurrentAction = ContraSpriteActions.DownLeft; } else if (state.GetPressedKeys().Length == 0) { if (currentDirection > 0) this.CurrentAction = ContraSpriteActions.StandRight; else this.CurrentAction = ContraSpriteActions.StandLeft; } base.Update(gameTime); } }
上面的代码主要是重写了ContraSprite的Update方法,在Update方法中通过判断当前的键盘按键输入来决定Sprite以何种动作类型进行展现。从上面的代码可以看到我们已经定义了上(W)、下(S)、左(A)、右(D)四个方向的动作变化。
接下来还需要做什么
我想,接下来需要做的大致有如下几点:
- 场景编辑器:根据提供的背景图片对游戏场景进行设计,确定Sprite的行动范围以及各种Sprite的行动方式(比如何时以什么样的方式出现,出现后又以什么样的轨迹行动)等,于是可以定义一些基础框架,然后利用设计模式为不同的Sprite添加不同的行为
- 精灵行动设计器:设计精灵的行动路线,比如可以定义一些函数来指定精灵的行动路线
- 暂时还没有想到,有相关经验的朋友希望能够留言提点……
演示视频
操作过程演示
请单击【这里】下载上述过程的演示视频