繪制游戲主角 繪制游戲的角色,并且讓角色能動起來是我們本節的重點。泡泡堂游戲的角色主要分為兩大類:玩家與敵人。沒有陣營劃分的游戲永遠是沒有趣味性的,我們目前開發的游戲是單機版本,沒有涉及多人對戰的形式。所以玩家就只有你自己,你的敵人就是電腦(NPC)了。當然我們都知道,要熟悉一款游戲,首先要和電腦對戰熟悉游戲的操作模式,這樣到時候我們再為你的朋友開發一個對戰模式,游戲就更加充滿樂趣了。 首先我們來看看游戲的對戰雙方的素材組成: 
圖玩家的素材 
圖敵人的素材 我們發現,游戲的角色素材都是由4*4的圖片效果來描述角色的移動方位。每個方位還有角色的移動動作。在開始制作游戲前,我們先來看看游戲的組成元素。如圖所示: 
圖 游戲的對象關系 從上面的圖中,我們可以看出游戲中角色對象的繼承關系。首先我們看看BaseEntity類的代碼: - /// <summary>
- /// 游戲中所有元素的基類
- /// </summary>
- public abstract class BaseEntity
- {
- /// <summary>
- /// 表示元素的X坐標(以像素為單位)
- /// </summary>
- public int X { get; set; }
- /// <summary>
- /// 表示元素的Y坐標(以像素為單位)
- /// </summary>
- public int Y { get; set; }
- /// <summary>
- /// 該元素的繪制方法
- /// </summary>
- /// <param name="g">封裝一個 GDI+ 繪圖圖面不能被繼承的類</param>
- public abstract void Draw(Graphics g);
- }</font></font></font></i>
復制代碼
我們的游戲不僅僅有可以移動的角色,還包括一些靜態的物體。這些物體將組成游戲不可缺少的場景,我們將在第二節學習到繪制游戲場景動畫。無論是角色還是物體它們在游戲中都是有坐標點的(X橫向坐標,Y縱向坐標)。它們都有不同的繪圖方式,所以我們把這些共同的特征定義為所有游戲對象的基類。并且提供一個Draw的抽象方法來實現對不同對象的繪制。這就是我們平時學習面向對象中用到的多態。接下來,我們看看GameRole類的實現代碼:
代碼比較多,我們從上往下一步一步的分析。首先我們需要明確GameRole類是所有游戲角色的基類,它繼承了BaseEntity類。在GameRole中我們定義了游戲角色移動的方向枚舉Direction,代碼如下:- /// <summary>
- /// 定義方向的枚舉
- /// </summary>
- public enum Direction
- {
- Static, //原地不動
- Left, //向左
- Up, //向上
- Right, //向右
- Down //向下
- }</font></font></font></i>
復制代碼
我們還定義了角色的公共屬性:角色類型,移動方向,生命力,移動速度等,代碼如下:- /// <summary>
- /// 方向的枚舉
- /// </summary>
- public Direction direction;
- /// <summary>
- /// 角色名稱(玩家和敵人)
- /// </summary>
- public string Role;
- /// <summary>
- /// 生命值
- /// </summary>
- public int Life;
- /// <summary>
- /// 速度
- /// </summary>
- public int Speed=1;
- /// <summary>
- /// 記錄楨(根據角色移動方位改變)
- /// </summary>
- private int Frame = 0;</font></font></font></i>
復制代碼
其中需要注意記錄楨Frame屬性,它是根據角色移動方位改變為改變的。現在大家可能還不能理解它的作用,我們先看看初始化游戲角色對象的方法實現后,大家就明白了。在GameRole類的構造函數中,我們調用了CreateGameRole這個方法。我們是如何實現游戲角色的創建的呢,我們一起來分析一下實現代碼:- /// <summary>
- /// 初始化游戲角色對象
- /// </summary>
- /// <param name="role"></param>
- public void CreateGameRole(string role)
- {
- //初始化游戲角色二維數組
- bitmaps = InitResource.CreateInstance().InitGameRole(role);
- }</font></font></font></i>
復制代碼
上面代碼中并沒有很直觀的告訴我們如何創建游戲角色。我們還需要繼續尋根問底。我們發現其中用到了一個名叫InitResource的類,這個類是用來為某個游戲對象(如:一個人物,障礙物等元素)加載其所用到的資源文件的。我們看看代碼的具體實現:- /// <summary>
- /// 初始化資源文件類
- /// </summary>
- public class InitResource
- {
- private static InitResource instance;
- /// <summary>
- /// 實例化資源文件類
- /// </summary>
- /// <returns></returns>
- public static InitResource CreateInstance()
- {
- if (instance == null)
- {
- instance = new InitResource();
- }
- return instance;
- }
-
- /// <summary>
- /// 初始化游戲角色人物(4*4素材)
- /// </summary>
- /// <param name="value">資源文件值</param>
- /// <returns>角色4個方向走動圖</returns>
- public Bitmap[][] InitGameRole(string value)
- {
- //創建游戲角色位圖二維數組
- Bitmap[][] bitmaps= new Bitmap[UtilityResource.RoleDirection][]
- {
- //創建角色移動每個方位都有四個動作
- new Bitmap[UtilityResource.RoleStep],
- new Bitmap[UtilityResource.RoleStep],
- new Bitmap[UtilityResource.RoleStep],
- new Bitmap[UtilityResource.RoleStep]
- };
- //創建單個對象的位圖
- Bitmap bitmap = new Bitmap(UtilityResource.BitmapWidth, UtilityResource.BitmapHeight);
- //訪問資源文件初始化游戲素材(游戲素材是一張包含角色不同方位不同動作的圖片)
- bitmap = (Bitmap)Properties.Resources.ResourceManager.GetObject(value);
- //通過循環,初始化游戲角色單幀素材(i:行數,j:列數,x:圖片人物X坐標 y:圖片人物Y坐標)
- for (int y = 0, i = 0; y < UtilityResource.ResourceHeight; y += UtilityResource.BitmapHeight, i++)
- {
- for (int x = 0, j = 0; x < UtilityResource.ResourceWidth; x += UtilityResource.BitmapWidth, j++)
- {
- //通過指定坐標切割游戲素材,獲得游戲角色單楨素材
- bitmaps[i][j] = bitmap.Clone(new Rectangle(x, y, UtilityResource.BitmapWidth, UtilityResource.BitmapHeight), System.Drawing.Imaging.PixelFormat.DontCare);
- }
- }
- return bitmaps;
- }
- }</font></font></font></i>
復制代碼
代碼中定義了兩個方法,CreateInstance方法的主要作用就是幫助我們創建資源文件類的實例。此處采用了單例模式,目的是為了防止一個類有多個實例。我們還定義了InitGameRole()方法,該方法返回一個Bitmap類型的二維數組。為什么需要返回二維數組了?這點我們需要詳細說明一下。我們的游戲角色素材都是采用的4*4的圖形。圖形中包含了游戲角色4個方位移動的不同的4組動作。如圖描述: 
圖 4*4角色素材
我們只需要在加載素材的時候,對素材進行行列拆分就可以獲得角色的每個方位的每個動作。這樣做的好處就是我們無需制作16張圖片來顯示角色移動的動作改變。二維數組的代碼如下: - Bitmap[][] bitmaps= new Bitmap[UtilityResource.RoleDirection][]
- {
- //創建角色移動每個方位都有四個動作
- new Bitmap[UtilityResource.RoleStep],
- new Bitmap[UtilityResource.RoleStep],
- new Bitmap[UtilityResource.RoleStep],
- new Bitmap[UtilityResource.RoleStep]
- };</font></font></font></i>
復制代碼
其中我們發現又出現了一個新的類: UtilityResource,這個類是用來初始化游戲中所用到的所有素材的尺寸以及資源訪問值的。參考代碼如下:- /// <summary>
- ///資源文件定義類
- /// </summary>
- public class UtilityResource
- {
- #region 資源文件尺寸
- //資源大小(玩家和敵人的原始圖片大小)
- public static int ResourceWidth = 256;
- public static int ResourceHeight = 256;
- //圖像大小(玩家和敵人的實際游戲圖片大小)
- public static int BitmapWidth = 64;
- public static int BitmapHeight = 64;
- //爆竹大小
- public static int BombWidth = 144;
- public static int BombHeight = 48;
- //火焰大小
- public static int FireWidth = 240
- public static int FireHeight = 48
- //地板大小
- public static int FloorWidth = 192;
- public static int FloorHeight = 48;
- //土墻大小(可以被爆竹炸掉)
- public static int SoilWallWidth = 48;
- public static int SoilWallHeight = 48;
- //石墻大小(堅固不可摧毀)
- public static int StoneWallWidth = 240;
- public static int StoneWallHeight = 48;
- #endregion
-
- #region 資源文件動畫幀數
- //火焰幀數(圖片組成元素個數)
- public static int FireFrames = 5;
- //爆竹幀數
- public static int BombFrames = 3;
- //地板幀數
- public static int FloorFrames = 4;
- //土墻幀數
- public static int SoilWallFrames = 1;
- //石墻幀數
- public static int StoneWallFrames = 5;
- #endregion
-
- #region 資源文件訪問值
- /// <summary>
- /// 爆竹資源值
- /// </summary>
- public static string BombValue = "Bomb";
- /// <summary>
- /// 火焰資源值
- /// </summary>
- public static string FireValue = "Fire";
- /// <summary>
- /// 玩家資源值
- /// </summary>
- public static string HeroValue = "Hero";
- /// <summary>
- /// 敵人資源值
- /// </summary>
- public static string EnemyValue = "Enemy";
- /// <summary>
- /// 土墻資源值
- /// </summary>
- public static string SoilWallValue = "SoilWall";
- /// <summary>
- /// 石墻資源值
- /// </summary>
- public static string StoneWallValue = "StoneWall";
- /// <summary>
- /// 地板資源值
- /// </summary>
- public static string FloorValue = "Floor";
- #endregion
-
- #region 游戲地圖尺寸
- //網格大小(寬度和高度均為48像素)
- public static int GridSize = 48;
- //游戲地圖尺寸
- public static int MapRows = 15;
- public static int MapCols = 15;
- #endregion
-
- #region 角色移動動作
- //角色移動方位數(上,下,左,右)
- public const int RoleDirection = 4;
- //角色完成單方向一套動作需要的步數
- public const int RoleStep = 4;
- #endregion
- }</font></font></font></i>
復制代碼
在前面的GameRole類中,我們還定義了一個Move方法使游戲角色能夠移動,移動的原理跟電影的原理是一樣的,都是通過一張張圖像幀快速切換形成的效果。只是這里的移動是四組不同方向的4個動作組成的。于是我們要在GameRole類中添加以下代碼:- //創建游戲角色位圖二維數組,保存角色行走方向圖
- protected Bitmap[][] bitmaps = new Bitmap[UtilityResource.RoleDirection][]
- {
- new Bitmap[UtilityResource.RoleStep],//下
- new Bitmap[UtilityResource.RoleStep],//左
- new Bitmap[UtilityResource.RoleStep],//右
- new Bitmap[UtilityResource.RoleStep] //上
- };
-
- /// <summary>
- /// 角色移動
- /// </summary>
- public virtual void Move()
- {
- switch (direction)
- {
- case Direction.Left:
- {
- this.X -= this.Speed;
- break;
- }
- case Direction.Right:
- {
- this.X += this.Speed;
- break;
- }
- case Direction.Down:
- {
- this.Y += this.Speed;
- break;
- }
- case Direction.Up:
- {
- this.Y -= this.Speed;
- break;
- }
- case Direction.Static:
- break;
- }
- }
-
- /// <summary>
- /// 記錄楨
- /// </summary>
- public void RecordFrame()
- {
- switch (direction)
- {
- case Direction.Down:
- {
- this.Frame = 0;
- break;
- }
- case Direction.Left:
- {
- this.Frame = 1;
- break;
- }
- case Direction.Right:
- {
- this.Frame = 2;
- break;
- }
- case Direction.Up:
- {
- this.Frame = 3;
- break;
- }
- case Direction.Static:
- break;
- }
- }
-
- /// <summary>
- /// 繪制游戲中人物對象
- /// </summary>
- /// <param name="g"></param>
- private int i = 0;//列索引
- public override void Draw(System.Drawing.Graphics g)
- {
- switch (direction)
- {
- case Direction.Down:
- case Direction.Up:
- case Direction.Left:
- case Direction.Right:
- i = i+1 < UtilityResource.RoleStep ? i + 1 : 0;
- break;
- case Direction.Static:
- i = 0;
- break;
- }
- //繪制圖形
- g.DrawImage(bitmaps[Frame][i], this.X, this.Y, UtilityResource.GridSize, UtilityResource.GridSize);
- }</font></font></font></i>
復制代碼
這里我們為GameRole類建立了一個數組來保存四個方向的每個動作。然后通過繪制不同的圖片幀來形成動作的效果。真正的移動還是通過Move方法來改變GameRole的坐標來實現的。動作的切換和坐標的改變是必不可少的。然后游戲中每個元素都需要繪制到窗體上,所以我們要重寫Draw方法。- /// <summary>
- /// 繪制游戲中人物對象
- /// </summary>
- /// <param name="g"></param>
- private int i = 0;//列索引
- public override void Draw(System.Drawing.Graphics g)
- {
- switch (direction)
- {
- case Direction.Down:
- case Direction.Up:
- case Direction.Left:
- case Direction.Right:
- i = i+1 < UtilityResource.RoleStep ? i + 1 : 0;
- break;
- case Direction.Static:
- i = 0;
- break;
- }
- //繪制圖形
- g.DrawImage(bitmaps[Frame][i], this.X, this.Y, UtilityResource.GridSize, UtilityResource.GridSize);
- }</font></font></font></i>
復制代碼
在這個方法中,我們根據方向
游戲所需要的資源已經準備好,現在我們來添加一個Hero類,即玩家控制的角色類,代碼如下:/// <summary>
/// 游戲主角
/// </summary>
public class Hero : GameRole
{
//靜態屬性和方法不能被繼承
//構造函數不能被繼承
//所以使用base關鍵字調用父類的構造函數或方法。
public Hero():base(UtilityResource.HeroValue)
{
}
public Hero(string role)
: base(role)
{
}
/// <summary>
/// 主角移動
/// </summary>
public void Move(bool isMove)
{
if (isMove)
{
base.Move();
base.RecordFrame();
}
else
{
base.RecordFrame();
}
}
}
這個類繼承了GameRole類,這樣Hero類就繼承了GameRole類中的屬性和方法。
到此為止,一個角色的屬性及需要的資源都有了,我們還需要添加一個游戲邏輯類來管理這些資源。游戲邏輯類代碼如下:
/// <summary>
/// 游戲邏輯處理類
/// </summary>
public class GameManager
{
//游戲主角
private Hero hero;
public GameManager()
{
//初始化游戲
InitGame();
}
/// <summary>
/// 初始化游戲
/// </summary>
public void InitGame()
{
//實例化游戲主角
hero = new Hero();
//主角移動速度
hero.Speed = 4;
//主角的生命值
hero.Life = 5;
//初始化游戲地圖網格
int cols = UtilityResource.MapCols;
int rows = UtilityResource.MapRows;
//創建了地圖(石墻和地面)
map = new GameMap(cols, rows);
//cols * rows:獲得地圖網格的面積
}
/// <summary>
/// 繪制游戲
/// </summary>
/// <param name="g"></param>
public void Draw(Graphics g)
{
//繪制玩家
hero.Draw(g);
}
}
有了這個類,我們就可以將角色繪制到游戲窗體中去。
新建一個窗體,修改窗體的標題、名字以及窗體大小等基本屬性。
public partial class GameMain : Form
{
public GameMain()
{
InitializeComponent();
//繪制游戲角色移動的網格寬度和高度
this.Width = UtilityResource.MapCols * 48;
this.Height = UtilityResource.MapRows * 48+24;
}
}
在這里我們通過后臺代碼的方式修改了窗體的大小。游戲窗體的屬性設置好后怎么才能把剛才我們做的角色放置到游戲窗體中去呢?這時我們需要使用窗體的Paint事件。

圖 窗體屬性及事件
為該事件添加以下代碼:
//游戲主要邏輯實現類
private GameManager manager = new GameManager();
private void GameMain_Paint(object sender, PaintEventArgs e)
{
//繪制游戲
manager.Draw(e.Graphics);
}
這里我們調用了主要邏輯類中的Draw方法,這個時候我們就可以把角色繪制到窗體中了。
運行窗體,會發現人物已經在窗體中,但是還不能移動。角色移動當然需要鍵盤上的方向鍵。這時我們還需要給窗體添加兩個事件,一個是鍵盤按鍵按下時觸發的事件,這個時候人物會移動;另一個是鍵盤按鍵彈起的事件,這個時候人物會停止移動。 
圖 窗體屬性及事件
添加這兩個事件后,角色移動該怎么移動,停止該怎么停止呢?這時我們還需要寫一個KeyBoard類來管理鍵盤按鍵事件。
/// <summary>
/// 鍵盤事件類
/// </summary>
public class Keyboard
{
private static Keyboard instance;
//鍵盤按下事件的集合
private List<Keys> keys = new List<Keys>();
public Keyboard()
{
keys.Clear();
}
/// <summary>
/// 創建鍵盤類實例
/// </summary>
/// <returns></returns>
public static Keyboard CreateInstance()
{
if (instance == null)
{
instance = new Keyboard();
}
return instance;
}
/// <summary>
/// 判斷鍵是否被按下
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool IsKeyDown(Keys key)
{
return keys.Contains(key);
}
/// <summary>
/// 鍵盤按下的鍵的集合
/// </summary>
/// <param name="e"></param>
public void KeyDown(Keys key)
{
if (!keys.Contains(key))
{
keys.Add(key);
}
}
/// <summary>
/// 移除鍵盤按起的鍵(防止按鍵后角色一直移動)
/// </summary>
/// <param name="key"></param>
public void KeyUp(Keys key)
{
if (keys.Contains(key))
{
keys.Remove(key);
}
}
}
這里我們通過一個集合來判斷玩家按下了哪些鍵。然后通過KeyDown和KeyUp兩個方法來控制集合中的鍵。另外還有一個IsKeyDown方法來判斷某個鍵是否被按下,這對后面的角色移動很有用。然后再在窗體后臺代碼中添加以下代碼:
private void GameMain_KeyDown(object sender, KeyEventArgs e)
{
//鍵盤按下
Keyboard.CreateInstance().KeyDown(e.KeyCode);
}
private void GameMain_KeyUp(object sender, KeyEventArgs e)
{
//鍵盤按上
Keyboard.CreateInstance().KeyUp(e.KeyCode);
}
這里我們把鍵盤的按鍵狀態存儲到程序中,程序可以通過獲得這些按鍵的狀態來根據GameManager游戲邏輯類中的邏輯去修改游戲中各種元素的狀態。這時我們需要在GameManager類中添加以下代碼:
/// <summary>
/// 鍵盤事件
/// </summary>
public void KeyBoardEvent()
{
if (Keyboard.CreateInstance().IsKeyDown(Keys.Up))
{
hero.direction = Direction.Up;
hero.Move(IsMove(hero));
}
else if (Keyboard.CreateInstance().IsKeyDown(Keys.Left))
{
hero.direction = Direction.Left;
hero.Move(IsMove(hero));
}
else if (Keyboard.CreateInstance().IsKeyDown(Keys.Right))
{
hero.direction = Direction.Right;
hero.Move(IsMove(hero));
}
else if (Keyboard.CreateInstance().IsKeyDown(Keys.Down))
{
hero.direction = Direction.Down;
hero.Move(IsMove(hero));
}
else
{
hero.direction = Direction.Static;
}
}
/// <summary>
/// 更新游戲楨,實現動畫
/// </summary>
public void UpdateGameFrames()
{
#region 更新鍵盤事件
KeyBoardEvent();
#endregion
}
/// <summary>
/// 判斷前方是否通過
/// </summary>
/// <param name="role">游戲角色對象</param>
/// <returns></returns>
public bool IsMove(GameRole role)
{
int newX = role.X;
int newY = role.Y;
//前進一步,記錄改變的坐標點
switch (role.direction)
{
case Direction.Down:
newY += role.Speed;
break;
case Direction.Left:
newX -= role.Speed;
break;
case Direction.Right:
newX += role.Speed;
break;
case Direction.Up:
newY -= role.Speed;
break;
}
//窗體臨界點檢測
if (newX < 0 || newX > (map.Cols - 1) * UtilityResource.GridSize || newY < 0 || newY > (map.Rows - 1) * UtilityResource.GridSize)
{
return false;
}
return true;
}
但是到目前位置,角色還是不能移動,因為窗體中的元素是通過繪制來表現出來的。所以我們需要給窗體添加一個Timer控件來不斷重繪窗體。例如,開始一個角色在窗體的第一個格子,我們通過代碼修改這個角色所在位置的屬性過后,必須通過這個角色的位置屬性來重繪窗體中角色所在位置。給窗體添加Timer控件并添加以下代碼:
private void GameTimer_Tick(object sender, EventArgs e)
{
//重繪窗體
this.Invalidate(new Rectangle(0, 0, this.Width, Height), true);
//更新游戲楨,實現動畫
manager.UpdateGameFrames();
}
但是這個時候,由于電腦的不同,窗體中的角色可能會閃動,我們需要在窗體構造方法中加入以下代碼:
public GameMain()
{
InitializeComponent();
//采用雙緩沖,解決角色移動屏幕閃爍
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | ControlStyles.OptimizedDoubleBuffer, true);
//繪制游戲角色移動的網格寬度和高度
this.Width = UtilityResource.MapCols * 48;
this.Height = UtilityResource.MapRows * 48 + 24;
}
到目前為止,角色已經可以在窗體中移動了,繪制敵人的方法跟角色的方法差不多,只是Hero是通過鍵盤按鍵來控制移動方向,而敵人則是自動移動和改變方向的。由于敵人產生的位置是隨機的,這需要我們把地圖繪制出來之后再去繪制敵人。
第二天目標:繪制游戲場景
繪制游戲的場景主要包含游戲中涉及到的各種物品。游戲場景主要由地面背景,石墻背景,箱子背景組成。地圖上的障礙物設置也為游戲增加了不少可玩性。地圖由一系列坐標構成,不同的坐標設置不同的標識并加以繪制,便可以構造出一張張不同的地圖出來。首先我們看看游戲場景素材的組成: 
圖石墻圖片 
圖地板圖片 
圖土墻圖片 首先我們需要考慮如何對組合的圖片素材進行分割,我們可以對圖片楨進行分解。實現圖片分離的效果。我們在InitResource類中創建InitGameGood方法,實現對1*N素材的圖片進行分離,單獨構造每張圖片素材。實現代碼如下: /// <summary>
/// 初始化游戲物品(1*N素材)
/// </summary>
/// <param name="value">資源文件值</param>
/// <param name="frames">圖片幀數</param>
/// <param name="size">圖片尺寸</param>
/// <returns>一次動畫需要的楨</returns>
public Bitmap[] InitGameGood(string value,int frames,Size size)
{
Bitmap[] bitmaps = new Bitmap[frames];
Bitmap bitmap = new Bitmap(size.Width, size.Height);
//訪問資源文件初始化游戲素材
bitmap = (Bitmap)Properties.Resources.ResourceManager.GetObject(value);
//size.Width/frames:獲得每張圖片的像素寬度
for (int i = 0, j = 0; i < size.Width; i += size.Width / frames, j++)
{
bitmaps[j] = bitmap.Clone(new Rectangle(i, 0, size.Width / frames, size.Height), System.Drawing.Imaging.PixelFormat.DontCare);
}
return bitmaps;
}
我們的游戲地圖是由多個48*48像素的網格組成的,在每個網格內我們放置不同的素材類型,因此我們需要對素材類型進行管理,我們通過GridType枚舉來管理游戲素材類型,代碼實現如下:
/// <summary>
/// 地圖格式
/// </summary>
public enum GridType
{
Empty, //空地
Stone, //石墻
Soil, //土墻
Floor, //地板
Bomb //爆竹
}
說明:空地類型沒有對應的圖片,在地圖初始化的時候,所有的網格默認均為空地。
接著創建地圖類(GameMap)初始化地圖,先繪制空地,再隨機繪制石墻(不可通過不可炸毀),再在為空地標記的坐標繪制箱子(可以被炸毀),便可以構造出一幅原始的地圖。
GameMap類的實現代碼如下:
/// <summary>
/// 游戲地圖類
/// </summary>
public class GameMap : BaseEntity
{
//定義地圖行
public int Rows = UtilityResource.MapRows;
//定義地圖列
public int Cols = UtilityResource.MapCols;
//定義地圖格式枚舉數組
public GridType[,] Grids;
//石墻圖片數組
private Bitmap[] bmpsStone = new Bitmap[UtilityResource.StoneWallFrames];
//地板圖片數組
private Bitmap[] bmpsFloor = new Bitmap[UtilityResource.FloorFrames];
//定義隨機數對象
private Random random = new Random();
//定義石墻和地板產生的隨機變量
private int stones = 0;
private int floors = 0;
/// <summary>
/// 初始化地圖
/// </summary>
/// <param name="rows">格子行數</param>
/// <param name="cols">格子列數</param>
public GameMap(int rows, int cols)
{
this.Rows = rows;
this.Cols = cols;
//初始化格式(Cols:X坐標,Rows:Y坐標)
Grids = new GridType[Cols, Rows];
//創建石墻圖片
bmpsStone = InitResource.CreateInstance().InitGameGood(UtilityResource.StoneWallValue, UtilityResource.StoneWallFrames, new Size(UtilityResource.StoneWallWidth, UtilityResource.StoneWallHeight));
//創建地板圖片
bmpsFloor = InitResource.CreateInstance().InitGameGood(UtilityResource.FloorValue, UtilityResource.FloorFrames, new Size(UtilityResource.FloorWidth, UtilityResource.FloorHeight));
//隨機產生地面
floors = random.Next(0, UtilityResource.FloorFrames);
//建立原始地圖
for (int x = 0; x < Cols; x++)
{
for (int y = 0; y < Rows; y++)
{
//默認為空地
Grids[x, y] = GridType.Empty;
//隨機產生地圖格式(隨機數從1開始的目的就是防止第一個單元格被填充)
int i = random.Next(1, Cols - 1);
int j = random.Next(1, Rows - 1);
//隨機產生石墻(先產生石墻占位,然后再產生其他的物體)
if (Grids[i, j] != GridType.Stone)
{
Grids[i, j] = GridType.Stone;
}
}
}
}
//定義記錄隨機產生石墻下標的數組
private List<int> stoneType = new List<int>();
public void RandStoneType()
{
for (int x = 0; x < Cols; x++)
{
for (int y = 0; y < Rows; y++)
{
stones = random.Next(0, UtilityResource.StoneWallFrames);
stoneType.Add(stones);
}
}
}
/// <summary>
/// 繪制游戲地圖
/// </summary>
/// <param name="g"></param>
private bool isCreate = true;
public override void Draw(System.Drawing.Graphics g)
{
if (isCreate)
{
RandStoneType();
isCreate = false;
}
for (int x = 0; x < Cols; x++)
{
for (int y = 0; y < Rows; y++)
{
//首先繪制地圖
g.DrawImage(bmpsFloor[floors], x * (UtilityResource.FloorWidth / UtilityResource.FloorFrames), y * (UtilityResource.FloorHeight), UtilityResource.FloorWidth / UtilityResource.FloorFrames, UtilityResource.FloorHeight);
//如果是石墻,就在地圖上添加石墻
if (Grids[x, y] == GridType.Stone)
{
g.DrawImage(bmpsStone[stoneType[x * Rows + y]], x * (UtilityResource.StoneWallWidth / UtilityResource.StoneWallFrames), y * (UtilityResource.StoneWallHeight), UtilityResource.StoneWallWidth / UtilityResource.StoneWallFrames, UtilityResource.StoneWallHeight);
}
}
}
}
}
上述代碼實現了一張包含石墻和地板背景的游戲場景,游戲的場景采用了隨機產生的組合效果,如圖所示: 
圖游戲初始化地圖場景(1) 
圖 游戲初始化地圖場景(2)
在上述代碼中,需要注意的是地圖的重繪問題。我們在窗體的定期器中設置了創建的重繪方法來保證角色的移動。這樣就可能出現每隔指定毫秒游戲的地圖出現重繪的問題。解決辦法是在GameMap類中添加記錄隨機產生石墻下標的數組的方法,代碼如下所示:
private List<int> stoneType = new List<int>();
public void RandStoneType()
{
for (int x = 0; x < Cols; x++)
{
for (int y = 0; y < Rows; y++)
{
stones = random.Next(0, UtilityResource.StoneWallFrames);
stoneType.Add(stones);
}
}
}
定義List<int>集合保存隨機產生的石墻下標。在繪制的方法中判斷是否只需要產生一次。這樣就可以防止每次重繪地圖的時候改變已經產生的石墻。我們還需要注意創建初始化地圖的順序,我們一定要先創建地板,再創建石墻。其實原理很簡單,因為石墻圖片是不規則的圖形,背景色是透明的,地板創建完成后,石墻就覆蓋在地板上面。這樣不規則圖片的底色就是地板的顏色了。
我們可能發現游戲初始化界面中還差了一個土墻圖片素材元素。接下來我們還需要繼續在地圖上創建土墻的地圖。定義土墻類SoilWall,代碼實現如下:
/// <summary>
/// 土墻類
/// </summary>
public class SoilWall : BaseEntity
{
//定義土墻圖片集合
private Bitmap[] bmpSoil = new Bitmap[UtilityResource.SoilWallFrames];
public SoilWall()
{
//創建土墻
bmpSoil = InitResource.CreateInstance().InitGameGood(UtilityResource.SoilWallValue,UtilityResource.SoilWallFrames,new Size(UtilityResource.SoilWallWidth,UtilityResource.SoilWallHeight));
}
public override void Draw(System.Drawing.Graphics g)
{
g.DrawImage(bmpSoil[0], X, Y, UtilityResource.SoilWallWidth, UtilityResource.SoilWallHeight);
}
}
土墻圖片素材是單楨的(沒有連續圖片動畫),我們初始化土墻是通過GameManager完成的,實現代碼如下:
在游戲初始化方法(InitGame)中新增代碼:
//土墻集合(全局變量中定義)
private List<SoilWall> soilwall_list = new List<SoilWall>();
//初始化游戲地圖網格(初始化方法中定義)
int cols = UtilityResource.MapCols;
int rows = UtilityResource.MapRows;
map = new GameMap(cols, rows);
//初始化土墻
soilwall_list.Clear();
//cols * rows:獲得地圖網格的面積
for (int i = 0; i < cols * rows; i++)
{
//定義隨機數
int x = random.Next(0, cols);
int y = random.Next(0, rows );
//防止角色被堵住或者角色在土墻上被創建出來
if ((x == 0 && y == 0) || (x == 1 && y == 0) || (x == 0 && y == 1))
{
continue;
}
//將空地部分設置為土墻
if (map.Grids[x, y] == GridType.Empty)
{
//創建土墻
SoilWall soilWall = new SoilWall();
soilWall.X = x*UtilityResource.SoilWallWidth;
soilWall.Y = y * UtilityResource.SoilWallHeight;
map.Grids[x, y] = GridType.Soil;
soilwall_list.Add(soilWall);
}
}
在繪制游戲方法(Draw)中,添加繪制土墻實現代碼:
/// <summary>
/// 繪制游戲
/// </summary>
/// <param name="g"></param>
public void Draw(Graphics g)
{
//1.繪制地圖
map.Draw(g);
//2.繪制土墻
for (int i = 0; i < soilwall_list.Count; i++)
{
soilwall_list.Draw(g);
}
//3.繪制玩家
hero.Draw(g);
}
注意上述代碼中此句代碼的作用:防止角色被堵住或者角色在土墻上被創建出來
if ((x == 0 && y == 0) || (x == 1 && y == 0) || (x == 0 && y == 1))
{
continue;
}
如果我們沒有判斷土墻創建坐標可能就會出現以下情況:  圖人物進入了死胡同BUG
圖土墻與人物重疊BUG 經過判斷后的正常運行效果: 
但是我們發現那還不是最終效果,如下圖所示: 
圖石墻四周是封閉的 大家注意到上面圖片上標記的紅色區域,這種情況下是不允許這樣創建地圖的,在以后加載怪物的時候可能會進入到封閉的石墻區域內,這樣的話無論如何我們的主角也沒能力消滅它了,游戲也將不會勝利。 解決方式: 剔除存在封閉石墻的石塊,以留一個路口供怪物或者主角出入 在Map類中定義一個DeleteAroundStone方法,判斷能否在當前坐標周圍建立石墻,代碼如下: /// <summary>
/// 判斷是否是四周環繞的石墻
/// </summary>
/// <param name="curCol">當前X坐標</param>
/// <param name="curRow">當前Y坐標</param>
/// <returns></returns>
public bool DeleteAroundStone(int curCol,int curRow)
{
int count = 0;
for (int x = curCol - 1; x <= curCol + 1; x++)
{
for (int y = curRow - 1; y <= curRow + 1; y++)
{
if (Grids[x, y] == GridType.Stone)
{
count++;
}
}
}
if (count >= 4)
{
return false;
}
return true;
}
首先遍歷當前坐標四周,如果存在石墻則count++,如果至少存在上下左右都是石墻的話就不允許創建了。在Map類構造函數中,初始化地圖時,進行判斷,代碼如下:
//剔除四周封閉的石墻
for (int x = 0; x < Cols; x++)
{
for (int y = 0; y < Rows; y++)
{
if (Grids[x, y] == GridType.Stone)
{
//判斷是否被封閉
if (DeleteAroundStone(x, y))
{
continue;
}
else
{
Grids[x, y] = GridType.Empty;
}
}
}
}
最終效果如下:

圖 地圖引擎最終效果
到目前為止,我們的游戲基本場景就設計完成了,我們可以讓上一節課中繪制好的主角在游戲地圖中”暢游”了。為什么說是”暢游”了,我們發現主角可以穿越任何的障礙物。也許這就是所謂游戲的外掛吧。為了保障游戲的可玩性,我們需要對游戲中角色移動進行臨界點檢測以及障礙物的碰撞檢測。精彩內容,下回分解,盡請期待。。。
第三天目標:角色移動檢測
上一節我們介紹了游戲的地圖繪制,本節將針對角色移動檢測進行展開。首先我們發現游戲的角色在移動過程中會跑出游戲地圖的邊界,我們成為移動的臨界點吧。我們先來把這個問題解決了。角色的移動范圍控制我們通過獲得地圖的寬度和高度就可以解決,在GameManager類中新增IsMove方法,代碼如下:
/// <summary>
/// 判斷前方是否通過
/// </summary>
/// <param name="role">游戲角色對象</param>
/// <returns></returns>
public bool IsMove(GameRole role)
{
int newX = role.X;
int newY = role.Y;
//前進一步,記錄改變的坐標點
switch (role.direction)
{
case Direction.Down:
newY += role.Speed;
break;
case Direction.Left:
newX -= role.Speed;
break;
case Direction.Right:
newX += role.Speed;
break;
case Direction.Up:
newY -= role.Speed;
break;
}
//窗體臨界點檢測
if (newX < 0 || newX > (map.Cols - 1) * UtilityResource.GridSize || newY < 0 || newY > (map.Rows - 1) * UtilityResource.GridSize)
{
return false;
}
return true;
}
記錄角色前進后坐標點的改變,然后與窗體的最大值進行比較,沒有得到窗體的最大值就可以繼續移動,否則停止移動。當然上述代碼只是判斷角色是否可以繼續移動,我們還要修改控制角色移動的方法,找到Hero類,修改Move方法:
/// <summary>
/// 主角移動
/// </summary>
public void Move(bool isMove)
{
if (isMove)
{
base.Move();
base.RecordFrame();
}
else
{
base.RecordFrame();
}
}
解決了角色移動超出臨界點的問題,接下來就是重點了。如何防止角色移動過程中可以穿越障礙物。也就是游戲中必須考慮的障礙物碰撞檢測。首先我們創建碰撞檢測類HitCheck,實現代碼如下:
/// <summary>
/// 碰撞檢測類
/// </summary>
public class HitCheck
{
/// <summary>
/// 判斷物體是否相交
/// </summary>
/// <param name="x1">對象1 X坐標</param>
/// <param name="y1">對象1 Y坐標</param>
/// <param name="x2">對象2 X坐標</param>
/// <param name="y2">對象2 Y坐標</param>
/// <returns></returns>
public static bool IsIntersect(int x1, int y1, int x2, int y2)
{
Rectangle r1 = new Rectangle(x1, y1, UtilityResource.GridSize, UtilityResource.GridSize);
Rectangle r2 = new Rectangle(x2,y2,UtilityResource.GridSize-4,UtilityResource.GridSize-4);
//判斷對象是否相交
if (r1.IntersectsWith(r2))
{
return true;
}
return false;
}
}
上面的代碼主要是通過判斷兩個矩形是否相交,如果矩形相交就說明有碰撞產生。接下來我們需要修改GameManager類中的IsMove方法,增加角色的碰撞檢測代碼:
/// <summary>
/// 判斷前方是否通過
/// </summary>
/// <param name="role">游戲角色對象</param>
/// <returns></returns>
public bool IsMove(GameRole role)
{
int newX = role.X;
int newY = role.Y;
//前進一步,記錄改變的坐標點
switch (role.direction)
{
case Direction.Down:
newY += role.Speed;
break;
case Direction.Left:
newX -= role.Speed;
break;
case Direction.Right:
newX += role.Speed;
break;
case Direction.Up:
newY -= role.Speed;
break;
}
//窗體臨界點檢測
if (newX < 0 || newX > (map.Cols - 1) * UtilityResource.GridSize || newY < 0 || newY > (map.Rows - 1) * UtilityResource.GridSize)
{
return false;
}
//碰撞檢查
for (int x = 0; x < map.Cols; x++)
{
for (int y = 0; y < map.Rows; y++)
{
//如果前方網格是爆竹,土墻,石墻
if (map.Grids[x, y] == GridType.Bomb || map.Grids[x, y] == GridType.Soil || map.Grids[x, y] == GridType.Stone)
{
//記錄當前障礙物坐標點
int posX = x * UtilityResource.GridSize;
int posY = y * UtilityResource.GridSize;
//判斷角色與障礙物的焦點
if(HitCheck.IsIntersect(posX,posY,newX,newY))
{
return false;
}
}
}
}
return true;
}
到此為止角色就不能實現穿越的效果了。接下來我們開始創建敵人類了,敵人類(Enemy)同樣繼承GameRole類,實現代碼與Hero類的代碼類似,具體如下所示:
/// <summary>
/// 敵人類
/// </summary>
public class Enemy : GameRole
{
public Enemy()
: base(UtilityResource.EnemyValue)
{
}
public Enemy(string role)
: base(role)
{
}
/// <summary>
/// 敵人移動
/// </summary>
public override void Move()
{
base.Move();
base.RecordFrame();
}
}
我們在GameManager游戲主要邏輯類中創建敵人,代碼實現如下:
//敵人集合
private List<Enemy> enemy_list = new List<Enemy>();
//初始化敵人
enemy_list.Clear();
//默認設置5個敵人
for (int i = 0; i < 5; i++)
{
//怪物出現的位置的隨機坐標
int rCol = random.Next(2, cols);
int rRow = random.Next(2, rows);
//在空地位置創建敵人
if (map.Grids[rCol, rRow] == GridType.Empty)
{
//創建敵人
Enemy enemy = new Enemy();
//設置敵人的初始化移動方向,默認是Static
enemy.direction = Direction.Left;
//設置敵人的移動速度
enemy.Speed = 4;
//設置敵人初始化的坐標
enemy.X = rCol * UtilityResource.GridSize;
enemy.Y = rRow * UtilityResource.GridSize;
//添加敵人到集合中
enemy_list.Add(enemy);
}
else
{
//如果當前網格不是空地,就繼續找空地,重新生成敵人
i = i > 0 ? i -= 1 : i;
}
}
我們默認創建5個敵人,隨著后期對游戲設置了關卡,我們可以采用動態改變敵人數量的方式來添加敵人數量。需要注意的敵人的方位屬性一定要設置為非Static的,否則角色永遠也無法移動起來。敵人的移動速度默認設置為4,不能讓敵人比玩家跑得慢吧。還需要注意敵人是在空地上創建出來的,如果當前網格不是空地,就繼續找空地,直到找到空地創建敵人。
接下來,我們在GameManager類的Draw()中繪制敵人,代碼如下:
/// <summary>
/// 繪制游戲
/// </summary>
/// <param name="g"></param>
public void Draw(Graphics g)
{
//1.繪制地圖
map.Draw(g);
//2.繪制土墻
for (int i = 0; i < soilwall_list.Count; i++)
{
soilwall_list.Draw(g);
}
//3.繪制玩家
hero.Draw(g);
//4.繪制敵人
for (int i = 0; i < enemy_list.Count; i++)
{
enemy_list.Draw(g);
}
}
現在玩家和敵人都繪制在游戲地圖上了,如圖所示: 
圖 玩家與敵人效果
不過現在的敵人還不能移動,我們需要讓它們充滿活力。我們就需要為它們添加移動的方法。我們修改UpdateGameFrames()更新游戲楨的方法,添加敵人移動的行為,代碼如下:
#region 更新敵人移動
for (int i = 0; i < enemy_list.Count; i++)
{
//判斷敵人是否允許移動
if (IsMove(enemy_list))
{
enemy_list.Move();
}
else
{
//不能移動,就重新調整方位
AutoChangeDirection(enemy_list);
}
//如果敵人與主角相交,就進行碰撞檢測
if (HitCheck.IsIntersect(hero.X, hero.Y, enemy_list.X, enemy_list.Y))
{
//主角生命值降低1次
hero.Life -= 1;
}
}
#endregion
敵人移動同樣需要進行臨界檢查與碰撞檢測。我們修改IsMove方法,添加敵人的判斷代碼:
//如果是敵人
if (role is Enemy)
{
if (HitCheck.IsIntersect(posX, posY, newX, newY))
{
return false;
}
}
else
{
if (HitCheck.IsIntersect(posX, posY, newX, newY))
{
return false;
}
}
如果敵人不能移動,就重新調整方位,新增AutoChangeDirection方法代碼如下:
/// <summary>
/// 自動調整方位
/// </summary>
/// <param name="role">游戲角色</param>
public void AutoChangeDirection(GameRole role)
{
//隨機調用方位
int rDirection = random.Next(0, 4);
switch (rDirection)
{
case 0:
{
role.direction = Direction.Down;
break;
}
case 1:
{
role.direction = Direction.Left;
break;
}
case 2:
{
role.direction = Direction.Right;
break;
}
case 3:
{
role.direction = Direction.Up;
break;
}
}
}
主要敵人也可以自由地調整方位移動了,如圖所示: 
圖敵人移動 我們現在看到了地圖上自由移動的敵人,作為玩家的我們,現在是不是迫不及待地想去解決這些敵人了。可是很無奈,我們沒有武器,又無法穿越障礙物。所以我們現在需要制作游戲的武器爆竹。爆竹的素材如圖所示: 
圖爆竹素材 
圖火焰素材 接下來,我們開始創建爆竹類Bomb,實現代碼如下: /// <summary>
/// 爆竹類
/// </summary>
public class Bomb : BaseEntity
{
//設置爆竹延遲破時間為50毫秒
public int DelayTime = 50;
Bitmap[] bmps = new Bitmap[UtilityResource.BombFrames];
public Bomb()
{
//初始化爆竹
bmps = InitResource.CreateInstance().InitGameGood(UtilityResource.BombValue,UtilityResource.BombFrames,new Size(UtilityResource.BombWidth,UtilityResource.BombHeight));
}
private int i = 0;
public override void Draw(System.Drawing.Graphics g)
{
//實現爆竹動畫效果
i = i + 1 < UtilityResource.BombFrames ? i + 1 : 0;
g.DrawImage(bmps, X, Y, UtilityResource.GridSize, UtilityResource.GridSize);
}
}
我們需要在游戲邏輯類GameManager中繪制爆竹類,代碼如下:
//爆竹集合(全局變量中定義)
private List<Bomb> bomb_list = new List<Bomb>();
/// <summary>
/// 繪制游戲
/// </summary>
/// <param name="g"></param>
public void Draw(Graphics g)
{
//1.繪制地圖
map.Draw(g);
//2.繪制土墻
for (int i = 0; i < soilwall_list.Count; i++)
{
soilwall_list.Draw(g);
}
//3.繪制玩家
hero.Draw(g);
//4.繪制敵人
for (int i = 0; i < enemy_list.Count; i++)
{
enemy_list.Draw(g);
}
//5.繪制爆竹
for (int i = 0; i < bomb_list.Count; i++)
{
bomb_list.Draw(g);
}
}
爆竹可能有多個,默認只有一個,后面我們將介紹通過吃道具的方式,增加爆竹的數量。
我們如何讓玩家放出爆竹呢?我們需要修改鍵盤事件,設置玩家按下空格鍵后,就在地圖的空白位置放置一個爆竹,代碼如下:
else if (Keyboard.CreateInstance().IsKeyDown(Keys.Space))
{
//放置爆竹
int col = 0;
int row = 0;
//放置爆竹時需要調整方位
AdjustDirection(hero, ref col, ref row);
if (BombNumber > 0 && map.Grids[col, row] != GridType.Bomb)
{
//創建爆竹對象
Bomb bomb = new Bomb();
//設置爆竹的坐標點
bomb.X = col * UtilityResource.GridSize;
bomb.Y = row * UtilityResource.GridSize;
bomb_list.Add(bomb);
//爆竹已經添加
map.Grids[col, row] = GridType.Bomb;
BombNumber--;
}
}
我們需要注意放置爆竹是需要調整爆竹的位置,AdjustDirection()方式實現了對爆竹位置的調整,代碼如下:
/// <summary>
/// 調整爆竹的位置
/// </summary>
/// <param name="role"></param>
/// <param name="col"></param>
/// <param name="row"></param>
public void AdjustDirection(GameRole role,ref int col,ref int row)
{
//獲得當前角色所在的網格位置
int cur_col = role.X / UtilityResource.GridSize;
int cur_row = role.Y / UtilityResource.GridSize;
//獲得當前角色的偏移量坐標
int posX = cur_col * UtilityResource.GridSize;
int posY = cur_row * UtilityResource.GridSize;
//角色X坐標位移量如果超過網格的一半,就在下一個網格放置爆竹
if (Math.Abs(posX - role.X) > UtilityResource.GridSize / 2)
{
col = cur_col + 1;
}
else
{
col = cur_col;
}
if (Math.Abs(posY - role.Y) > UtilityResource.GridSize / 2)
{
row = cur_row + 1;
}
else
{
row = cur_row;
}
}
當我們按下空格鍵后的效果:

圖 玩家與爆竹重合
我們發現玩家與爆竹重合了,玩家置于爆竹的下方。出現這樣的原因是因為我們先繪制出玩家,再同一個網格位置又繪制爆竹,所以爆竹覆蓋了玩家。解決辦法就是最后繪制玩家。另外我們還注意到玩家這個時候無法移動了。因為我們在前面做了障礙物碰撞檢測。我們需要對障礙物碰撞檢測的代碼進行修改,代碼如下:
//碰撞檢查
for (int x = 0; x < map.Cols; x++)
{
for (int y = 0; y < map.Rows; y++)
{
//如果前方網格是爆竹,土墻,石墻
if (map.Grids[x, y] == GridType.Bomb || map.Grids[x, y] == GridType.Soil || map.Grids[x, y] == GridType.Stone)
{
//記錄當前障礙物坐標點
int posX = x * UtilityResource.GridSize;
int posY = y * UtilityResource.GridSize;
//如果是敵人
if (role is Enemy)
{
if (HitCheck.IsIntersect(posX, posY, newX, newY))
{
return false;
}
}
else
{
//判斷玩家放置爆竹時可以通過,直到玩家離開爆竹就成了障礙物
if (HitCheck.IsIntersect(posX, posY, role.X, role.Y))
{
continue;
}
//判斷角色與障礙物的焦點
if (HitCheck.IsIntersect(posX, posY, newX, newY))
{
return false;
}
}
}
}
}
我們首先判斷玩家放置爆竹時可以通過,直到玩家離開爆竹后就成了障礙物。 
圖 爆竹變成了障礙物
我們實現了爆竹的創建,接下來就可以創建爆竹破時候的火焰效果了。我們首先創建火焰類Fire,具體代碼如下:
/// <summary>
/// 火焰類
/// </summary>
public class Fire : BaseEntity
{
//設置火花延遲時間
public int DelayTime = 5;
//設置火花對象
Bitmap[] bmpFire = new Bitmap[UtilityResource.FireFrames];
public Fire()
{
bmpFire = InitResource.CreateInstance().InitGameGood(UtilityResource.FireValue, UtilityResource.FireFrames, new Size(UtilityResource.FireWidth, UtilityResource.FireHeight));
}
private int i = 0;
public override void Draw(System.Drawing.Graphics g)
{
i = i + 1 < UtilityResource.FireFrames ? i + 1 : 0;
g.DrawImage(bmpFire, X, Y, UtilityResource.GridSize, UtilityResource.GridSize);
}
}
接下來,我們在GameManager游戲業務邏輯類中,實現破的效果。在破效果實現前,我們首先需要制造火焰,創建CreateFire方法,實現代碼如下:
/// <summary>
/// 制造火焰
/// </summary>
/// <param name="col"></param>
/// <param name="row"></param>
public void CreateFire(int col,int row)
{
Fire fire = new Fire();
fire.X = col * UtilityResource.GridSize;
fire.Y = row* UtilityResource.GridSize;
fire_list.Add(fire);
}
當爆竹破時,火焰是四處延伸的。火焰根據當前位置(爆竹位置)不同方向遞增或者遞減蔓延。當遇到土墻,土墻被燒毀并結束該方向火焰的蔓延;當遇到石墻,直接結束該方向火焰的蔓延。具體實現的代碼為:
/// <summary>
/// 爆竹破
/// </summary>
/// <param name="bomb"></param>
public void CreateBomb(Bomb bomb)
{
//獲得放置爆竹的網格
int col = bomb.X / UtilityResource.GridSize;
int row = bomb.Y / UtilityResource.GridSize;
//清空火焰
fire_list.Clear();
//創建中心點火焰,制造火焰
CreateFire(col, row);
//創造上火焰效果(上火焰隨Y坐標遞減,遞減的次數根據火焰的強度來決定)
int cur_row = row-1;
for(int i=0;i<FirePower;i++,cur_row--)
{
Fire fire = new Fire();
//火焰結束
if (cur_row < 0)
{
break;
}
//如果遇到石墻
if (map.Grids[col, cur_row] == GridType.Stone)
{
break;
}
//如果遇到土墻
else if (map.Grids[col, cur_row] == GridType.Soil)
{
//產生火焰
CreateFire(col, cur_row);
break;
}
else
{
CreateFire(col, cur_row);
}
}
//創造下火焰效果(下火焰隨Y坐標遞增,遞增的次數根據火焰的強度來決定)
cur_row = row + 1;
for (int i = 0; i < FirePower; i++, cur_row++)
{
Fire fire = new Fire();
//火焰結束
if (cur_row > map.Rows - 1)
{
break;
}
//如果遇到石墻
if (map.Grids[col, cur_row] == GridType.Stone)
{
break;
}
//如果遇到土墻
else if (map.Grids[col, cur_row] == GridType.Soil)
{
//產生火焰
CreateFire(col, cur_row);
break;
}
else
{
CreateFire(col, cur_row);
}
}
//創造左火焰效果(左火焰隨X坐標遞減,遞減的次數根據火焰的強度來決定)
int cur_col = col - 1;
for (int i = 0; i < FirePower; i++, cur_col--)
{
Fire fire = new Fire();
//火焰結束
if (cur_col < 0)
{
break;
}
//如果遇到石墻
if (map.Grids[cur_col, row] == GridType.Stone)
{
break;
}
//如果遇到土墻
else if (map.Grids[cur_col, row] == GridType.Soil)
{
//產生火焰
CreateFire(cur_col, row);
break;
}
else
{
CreateFire(cur_col, row);
}
}
//創造右火焰效果(右火焰隨X坐標遞增,遞增的次數根據火焰的強度來決定)
cur_col = col + 1;
for (int i = 0; i < FirePower; i++, cur_col++)
{
Fire fire = new Fire();
//火焰結束
if (cur_col > map.Cols-1)
{
break;
}
//如果遇到石墻
if (map.Grids[cur_col, row] == GridType.Stone)
{
break;
}
//如果遇到土墻
else if (map.Grids[cur_col, row] == GridType.Soil)
{
//產生火焰
CreateFire(cur_col, row);
break;
}
else
{
CreateFire(cur_col, row);
}
}
}
在UpdateGameFrames方法中定義更新爆竹破的方法,新增代碼如下:
#region 更新爆竹破
for (int i = 0; i < bomb_list.Count; i++)
{
//更新破時間
bomb_list.DelayTime--;
if (bomb_list.DelayTime == 0)
{
//調用破的方法
CreateBomb(bomb_list);
//獲得當前爆竹的網格位置
int cur_col = bomb_list.X / UtilityResource.GridSize;
int cur_row = bomb_list.Y / UtilityResource.GridSize;
//破完成將當前坐標點設置為空地
map.Grids[cur_col, cur_row] = GridType.Empty;
//爆竹數量自加
BombNumber++;
//放置一個爆竹就移除一個爆竹
bomb_list.Remove(bomb_list);
}
}
#endregion
當主角放置爆竹后,到達一定的時間爆竹便會自爆,前面爆竹類中有一個DelayTime的變量,用來存放爆竹延遲破時間,用List<Bomb>存放已放置爆竹,遍歷每一個爆竹,并更新它們破時間。不過爆竹的破后的火焰效果還沒有實現。我們還需要接著在Draw方法中繪制火焰。在GameManager類的Draw方法中,添加如下代碼:
/// <summary>
/// 繪制游戲
/// </summary>
/// <param name="g"></param>
public void Draw(Graphics g)
{
//1.繪制地圖
map.Draw(g);
//2.繪制土墻
for (int i = 0; i < soilwall_list.Count; i++)
{
soilwall_list.Draw(g);
}
//4.繪制敵人
for (int i = 0; i < enemy_list.Count; i++)
{
enemy_list.Draw(g);
}
//5.繪制爆竹
for (int i = 0; i < bomb_list.Count; i++)
{
bomb_list.Draw(g);
}
//6.繪制火焰
for (int i = 0; i < fire_list.Count; i++)
{
fire_list.Draw(g);
}
//3.繪制玩家
hero.Draw(g);
} 
圖 繪制火焰效果
我們發現火焰一直停留在游戲界面中不能消失,并且火焰破后,對玩家以及箱子都沒有產生任何摧毀的效果。這時,我們需要更新火焰的效果,當火焰與玩家,敵人,或者物品相交時都應該產生被炸毀的效果。我們需要在GameManager類中新增更新火焰效果的方法,具體實現代碼如下:
#region 更新火焰效果
for (int i = 0; i < fire_list.Count; i++)
{
//火焰時間遞減
fire_list.DelayTime--;
if (fire_list.DelayTime == 0)
{
//獲得火焰的網格位置
int col = fire_list.X / UtilityResource.GridSize;
int row = fire_list.Y / UtilityResource.GridSize;
//燒毀土墻
for (int j = 0; j < soilwall_list.Count; j++)
{
//如果火焰的坐標與土墻的坐標相同
if (soilwall_list[j].X == fire_list.X && soilwall_list[j].Y == fire_list.Y)
{
soilwall_list.Remove(soilwall_list[j]);
}
}
//燒毀敵人
for (int j = 0; j < enemy_list.Count; j++)
{
//敵人與火焰相交
if(HitCheck.IsIntersectDeep(enemy_list[j].X,enemy_list[j].Y,fire_list.X,fire_list.Y))
{
enemy_list.Remove(enemy_list[j]);
}
}
//燒毀玩家
if (HitCheck.IsIntersectDeep(hero.X, hero.Y, fire_list.X, fire_list.Y))
{
hero.X = 0;
hero.Y = 0;
}
map.Grids[col, row] = GridType.Empty;
//移除火焰,否則火焰就會一直出現在地圖上
fire_list.Remove(fire_list);
}
}
#endregion
我們通過判斷火焰產生的坐標點與物體的坐標點是否相等,來判斷是否炸毀物體。玩家和敵人我們采用了IsIntersectDeep來判斷兩個物體是否相交來進行判斷。注意此處的IsIntersectDeep方法,我們稱為深度相交。目的是為了減少兩個矩形相互點的范圍,只有敵人或玩家近距離進入火焰范圍的時候才被炸毀。
到目前為止,游戲的玩家便可以通過爆竹清理障礙物以及消滅敵人了。但還有一個問題需要在此解決,就是如果在爆竹破范圍內還有其他爆竹,那么這個爆竹是不是也應該破?這個效果怎么實現呢? 
圖 兩個爆竹
在這個圖里,我們先放置爆竹1,隔1秒后再放置爆竹2。當爆竹1破后,爆竹2并不會馬上破,而是再隔1秒后才會破,也就是說,爆竹間沒能相互影響。實際上應該是爆竹1破的時候,只要爆竹2在爆竹1破的火焰范圍內,那么爆竹2就應該立即破。這個問題看似很復雜,實際上很簡單,我們只要在爆竹1破的時候將其火焰范圍內的所有爆竹的破時間設置為立即破即可,代碼如下:
//炸掉范圍內的其他爆竹
for (int j = 0; j < bomb_list.Count; j++)
{
//如果火焰的坐標與土墻的坐標相同
if (bomb_list[j].X == fire_list.X && bomb_list[j].Y == fire_list.Y)
{
bomb_list[j].DelayTime = 0;
}
}
這里把時間設置為0是不管這個爆竹本來應該還有多久才破都讓該爆竹立即破。
到今天為止,我們的泡泡堂能夠實現人和怪物的移動,爆竹的破以及連炸。明天我們將實現道具的功能。
第四天目標:道具的制作
道具跟障礙物有一定關系,經過分析關系如下表:
完整的Word格式文檔51黑下載地址:
泡泡堂項目詳細文檔.doc
(1.42 MB, 下載次數: 13)
2020-1-6 16:36 上傳
點擊文件名下載附件
下載積分: 黑幣 -5
泡泡堂.docx
(4.2 MB, 下載次數: 13)
2020-1-6 16:36 上傳
點擊文件名下載附件
下載積分: 黑幣 -5
|