在前文分享我的XNA版超级玛丽(1)中,我详细介绍了利用XNA如何从无到有的让我们的玛丽出现在游戏画面中,并赋予它奔跑的能力,最后还完善了一个移动时加速和减速的小细节,算是起了个头。今天阳光明媚,微风徐徐,如此好日子,我想:是时候继续完善我们的玛丽。
先来点前戏
在继续为我们的玛丽增加新的游戏内容之前,先对上一篇的所有代码做一些小小的重构。
在上一篇中,我把所有的代码都一古脑的塞在我们的Game1.cs中,这显然不合适,就像我们做网页时把数据访问,业务处理,界面展示都塞在在ASPX中一样,小学生都不知道不正确。那么怎么重构呢?先介绍一个名词--“精灵”,这里的精灵,不是指WAR3中的精灵族,而是对游戏中所有绘制的一个个会动的和不会动的游戏元素的一个总称,如怪物,玩家,背景。具体解释请自行谷哥。有了精灵这个概念,我们很容易想到,我们应该在游戏中定义一个精灵类,为所有具体精灵的基类。目前我们只有玛丽一个游戏元素,那就定义一个BaseSprite基类和一个继承自BaseSprite的Mario类。对了,也许我们可以新建一个类库,用来存放精灵类。
- 新建一个名为SuperMario.Sprite的XNA Game Library ,并添加对其的引用
- 删除默认的Class1.cs
- 添加BaseSprite.cs和Mairo.cs
- 修改BaseSprite类为抽象类,让Mario类继承自BaseSprite
解决方案现在如下图:
在Game1.cs,有两个最重要的方法Update和Draw,所有的游戏精灵都需要在这两个方法中添加代码,那么我们的BaseSptire类可以添加这两个方法,让各自的精灵类实现这两个方法的逻辑,然后在Game1中调用所有BaseSprite的Update和Draw。现在BaseSprite类的代码如下:
public abstract class BaseSprite { //用于载入图像资源 public virtual void LoadContent(ContentManager content) { } public virtual void Update(GameTime gameTime) { } public virtual void Draw(GameTime gameTime,SpriteBatch sb) { } }
现在把Game1.cs中所有我们添加的关于Mairo的代码移到Mairo类中,放入对应的方法中。目前mario类的代码如下:
public class Mairo : BaseSprite { Texture2D _marioText; Vector2 _marioPosition = new Vector2(100, 100); int _frmStartIndex = 0; int _frmEndIndex = 0; int _frmIndex = 0;//当前画的图块的索引 int _frmChangeTime = 100;//多少毫秒换一次图片 int _frmCurrentTime = 0;//距离上次换图片过了多少毫秒 float _runAcceleration = 0.01F;//跑步加速度 float _runResistance = 0.005F;//... int MAXspeedR = 3;//右移最大速度 int MAXspeedL = -3;//左移最大速度 float _speed = 0;//当前速度 public override void LoadContent(ContentManager content) { _marioText = content.Load<Texture2D>(@"Image/mario"); base.LoadContent(content); } public override void Update(GameTime gameTime) { // if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) // this.Exit(); KeyboardState keyState = Keyboard.GetState(); int frmTime = gameTime.ElapsedGameTime.Milliseconds; if (keyState.IsKeyDown(Keys.A)) { _speed = _speed < MAXspeedL ? MAXspeedL : _speed - _runAcceleration * frmTime; } if (keyState.IsKeyDown(Keys.D)) { _speed = _speed > MAXspeedR ? MAXspeedR : _speed + _runAcceleration * frmTime; } //以下省略N行代码 base.Update(gameTime); } public override void Draw(GameTime gameTime,SpriteBatch sb) { sb.Begin(); sb.Draw(_marioText, _marioPosition, new Rectangle(_frmIndex * 16, 0, 16, 16), Color.White, 0, Vector2.Zero, 1.0f, SpriteEffects.None, 0); sb.End(); base.Draw(gameTime,sb); } }
最后在Game1.cs中添加一个Mario类的成员对象,并在和BaseSprite类中三个方法同名的方法中执行Mario对象的方法。现在我们的Game1.cs中变得干净多了,按F5运行,一切如旧。
前戏就先到这里,让大家久等了,下面进入正文。
跳跃
上一篇已经解决了Mario的移动问题,但要Mario通过一个一个障碍和怪物,还需要一个跳跃的功能。同移动一样,我们根据物理知识在代码中模拟玛丽跳跃时的运动情况。当玛丽跳跃时,他应该有一个向上的初始速度,在跳跃时,受到重力作用,产生垂直向下的加速度。
修改Mairo类中的速度变量,现在Mario同时拥有X轴和Y轴上的速度,改用Vector2在存放玛丽的速度,Vector2是2维向量类型。同时,把“速度”和“位置” 提到BaseSprite这个基类中。
用“K”键来控制跳跃,并添加一个枚举表示当前Mairo的运动情况,先添加如下代码:
public enum MarioAction { Stand = 0, Run = 1, Jump = 2, Fly = 3 } int MAXjump = 4;//最大掉落速度 float INITjumpspeed = -6F;//跳跃初始速度 float G = 0.02F;//重力加速度 MarioAction _action = MarioAction.Stand; private int _jumpPressTime = 0;//跳跃键按了多久了 //Update方法中的代码 if (_action == MarioAction.Jump) { if(_jumpPressTime >=200 || _jumpPressTime==0) _speed.Y = _speed.Y > MAXjump ? MAXjump : _speed.Y + G * frmTime; } if (keyState.IsKeyDown(Keys.K)) { _jumpPressTime += frmTime; if( _action ==MarioAction .Stand || _action ==MarioAction .Run ) { _speed.Y = INITjumpspeed; _action = MarioAction.Jump; } } if(keyState .IsKeyUp (Keys .K )) _jumpPressTime = 0;
为了区分Mairo此时是在跑步还是跳跃还是等等,这里增加了一个枚举表示Mario当前的运动情况,当Mario站立或移动的时候可以跳跃,当Mario跳跃时,处理重力加速度。这里我增加了一个跳跃键的时间变量,当持续按住跳跃键小于200毫秒时,排除重力加速度因素,以此模拟游戏中的“大跳”“小跳”功能 ,当然这只是我自己想的,真正的Mario游戏怎么处理大跳小跳也许不是这么处理的。
目前游戏中已经出现了不少“常量”,他们影响着游戏的表现。如“重力加速度”“最大掉落速度”等等,我们很容易想到,这些应该统一管理,并且应该可以非常方便的调整。但这里不讨论这个。
按F5运行,现在我们的Mario已经能跑能跳了,只不过当它跳起来时,却没有地面“接住他”,直接掉到屏幕外面去了,接下来我们处理这个,让Mario能站在地面上。
碰撞检测
所谓碰撞检测,就是判断两个精灵元素是否发生了碰撞,一个简单而有效的方法就是采用“包围盒算法”,具体、专业的解释请自行谷哥。这里我简单介绍一下,所谓包围盒矩形,就是为我们的精灵定义一个大小和精灵大小差不多的矩形,并随着我们的精灵移动,当进行碰撞检测时,由两个精灵的包围盒矩形的碰撞检测代替精灵的不规则图形的碰撞检测。而矩形的碰撞检测也就是矩形的相交问题,从而大大简化碰撞检测的难度。当然不同的碰撞检测适用不同的场景,一般的2D游戏,该方法足矣。
所有的精灵都需要进行碰撞检测,我们在BaseSprite中定义包围盒矩形:
/// <summary> /// 一帧图片尺寸 /// </summary> protected Point _frmSize; public BaseSprite(Point frmsize) { _frmSize = frmsize; _collisionRect.Width = frmsize.X; _collisionRect.Height = frmsize.Y; } Rectangle _collisionRect; public Rectangle CollisionRect { get { return _collisionRect ; } } //----------------------Mario.cs public Mairo() : base(new Point(16, 16)) { }
由于我自己扣的游戏素材图像都是没有留空的,所以我定义一帧图片的大小就是包围盒矩形的大小,BaseSprite中现在增加了图片尺寸变量,并在构造函数中初始化,同时初始化包围盒矩形大小,现在所有的精灵在处理完自己Update逻辑后,在基类中通过精灵位置更新包围盒矩形的位置:
public virtual void Update(GameTime gameTime) { _collisionRect.Location = new Point((int)_position.X, (int)_position.Y); }
接下来我们增加一个“地面”精灵,先在SuperMarioContent中导入如下图片。
添加一个Ground精灵,继承自BaseSprite,代码如下:
public class Ground:BaseSprite { public Ground() :base(new Point (530,50)) { this._position = new Vector2(100, 300); } public override void LoadContent(ContentManager content) { _text = content.Load<Texture2D>(@"Image/ground"); base.LoadContent(content); } public override void Update(GameTime gameTime) { base.Update(gameTime); } public override void Draw(GameTime gameTime, SpriteBatch sb) { sb.Draw(_text, _position, new Rectangle(0, 0, _frmSize .X ,_frmSize .Y ), Color.White, 0, Vector2.Zero, 1.0f, SpriteEffects.None, 0); base.Draw(gameTime, sb); } }
代码很简单,就不解释了。最后在Game1.cs中添加一个Ground精灵变量,同我们的Mario精灵一样,在相应的Draw,Update,LoadContent方法中添加代码,现在Game1.cs已经有两个精灵代码了,比如Draw方法中的代码应该是这个样子:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); _player.Draw(gameTime ,spriteBatch ); _ground.Draw(gameTime, spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
此时,按F5运行,游戏画面应该有一块地面了吧。。如图
现在添加碰撞检测代码,在BaseSprite 中添加如下函数:
//碰撞检测 public CollisionPosition Collision(BaseSprite sprite) { //获取相交矩形 Rectangle resultRect = Rectangle.Intersect(this.CollisionRect, sprite.CollisionRect); //判断是否香蕉 if (resultRect.Height == 0 || resultRect.Width == 0) { return CollisionPosition.NULL; } //以下位置指的是相交的矩形在精灵矩形内的相对位置 //左上角 if (resultRect.Location == CollisionRect.Location) { if (resultRect.Width > resultRect.Height) { return CollisionPosition.Top; } else { return CollisionPosition.Left; } } //左下角 if (resultRect.LeftBottom() == CollisionRect.LeftBottom()) { if (resultRect.Width > resultRect.Height) { return CollisionPosition.Bottom; } else { return CollisionPosition.Left; } } //右上角 if (resultRect.RightTop() == CollisionRect.RightTop()) { if (resultRect.Width > resultRect.Height) { return CollisionPosition.Top; } else { return CollisionPosition.Right; } } //右下角 if (resultRect.RightBottom() == CollisionRect.RightBottom()) { if (resultRect.Width > resultRect.Height) { return CollisionPosition.Bottom; } else { return CollisionPosition.Right; } } return CollisionPosition.NULL; } public enum CollisionPosition { Left = 1, Top = 2, Right = 3, Bottom = 4, NULL = 5 }
CollisionPosition枚举表示碰撞的位置,因为对Mario这个游戏来说,仅仅简单的获取是否发生碰撞是远远不够的,我们还需要知道具体是“头”碰到了什么还是“脚”踩到了什么。在Collision方法中,先获取相交矩形,当矩形长宽都不为0时,表示发生相交,resultRect矩形是相交矩形,如下图所示
上图展示了一个碰撞的可能,即获得的相交矩形在被检测矩形的右下角(这里Mario是被检测矩形),当相交矩形是这种情况时,有两种形成的可能,一种是Mario从上面掉下来,另一种是Mario从左上角跳过来,那么此时如何判断Mario是从上掉下来碰到了地面,应该站在上面,还是从左上掉入,右边碰到了障碍,应该往下掉?可以这样认定,当resultRect 矩形是扁的时,即宽大于高时,认定Mario是从上面掉下来,反之就是左边撞过来。用类似的算法,Collision方法定义了所有碰撞的处理,返回对应的碰撞位置。下图列出了大部分碰撞的可能,橙色矩形表示被检测精灵的包围盒,比如Mario,黑色表示障碍物或者怪物的包围盒矩形,右边用蓝色线框框起来的碰撞情况是不需要考虑相交矩形宽高情况的。
Collision方法返回了碰撞情况,接下来添加碰撞后,该作出什么反应,为Mario类添加CollisionProces方法:
public void CollisionProcess(CollisionPosition pos, BaseSprite sprite) { Rectangle spRect = sprite.CollisionRect; switch (pos) { case CollisionPosition.Left: if (_speed.X < 0) _speed.X = 0; this._position.X = spRect.Right; break; case CollisionPosition.Bottom: if (this._speed.Y > 0) this._speed.Y = 0; this._action = MarioAction.Run; this._position.Y = spRect.Top - this._frmSize.Y; break; case CollisionPosition.Right: if (this._speed.X > 0) this._speed.X = 0; this._position.X = spRect.Left - this._frmSize.X; break; case CollisionPosition.Top: this._speed.Y = 1; this._position.Y = spRect.Bottom; break; case CollisionPosition.NULL: this._action = MarioAction.Jump; break; default: break; } }
该方法处理碰撞障碍之后作出的反应,实际上游戏中Mario和大部分怪物跟障碍物的碰撞反应是有很多类似之处的,目前先这样处理,以后逐步重构。当和障碍物发生碰撞时,主要处理精灵的速度和位置,比如Mario碰到了水管,应该无法往前移动,水平速度设为0,又比如鸭子碰到水管了,通常水平速度会反一下,即往回移动。位置处理则是将两个已经相交的矩形分开,只有边和边相交,如下图示例:
最后在Game1 .cs 的Update函数中添加如下代码:
protected override void Update(GameTime gameTime) { _player.Update(gameTime); _ground.Update(gameTime); CollisionPosition pos= _player.Collision(_ground); _player.CollisionProcess(pos, _ground); base.Update(gameTime); }
大功告成,按F5 运行,现在游戏一开始由于Mario出现在半空中马上就会往下掉,并安全的停在地面上,而不是一直往下掉。但是现在还有一个问题,就是当Mario 站在地面上移动时,我们的Mario一直在地面上抖动,这是为什么呢? 这个问题困扰了我很久,后来我才发现,XNA中的Rectange类中的获取相交矩形函数并不认为当两个矩形有一条边重合时是相交情况,而比如我们的Mario踩在地面上时,刚好应该是两个矩形有一条边重合,此时应该也属于发生碰撞,因为Mario此时已经不能往下掉了。请出Reflector,复制出Rectange中Intersect方法,稍为修改,然后在BaseSprite的Collision方法中调用我们自己的Intersect方法。注意判断矩形是否相交的"或"判断也改为了"且"判断. 再次运行,玛丽终于很稳的站在地面上了。
我给出的碰撞代码已经处理了所有位置的碰撞检测,大家可以自己试试在游戏中加一些障碍,如墙,水管,停在空中的砖块。
结束
终于写完了,刚才写到一半的时候,想先保存个草稿,然后继续写,结果提交失败,按浏览器上后退按钮,退过来啥都没了,太受打击了。
最后希望能给大家带来帮助,有用的就点个顶,写的我腰酸背疼的。。。
下一篇介绍地图编辑器的使用。