在前文分享我的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方法。注意判斷矩形是否相交的"或"判斷也改為了"且"判斷. 再次運行,瑪麗終於很穩的站在地面上了。
我給出的碰撞代碼已經處理了所有位置的碰撞檢測,大家可以自己試試在游戲中加一些障礙,如牆,水管,停在空中的磚塊。
結束
終於寫完了,剛才寫到一半的時候,想先保存個草稿,然后繼續寫,結果提交失敗,按瀏覽器上后退按鈕,退過來啥都沒了,太受打擊了。
最后希望能給大家帶來幫助,有用的就點個頂,寫的我腰酸背疼的。。。
下一篇介紹地圖編輯器的使用。