START:最近在公交車上無聊,於是用平板看了看下載的坦克大戰的開發教程,於是在晚上回家后花了兩天模仿了一個,現在來總結一下。
一、關於坦克大戰
《坦克大戰》(Battle City)是1985年日本南夢宮Namco游戲公司開發並且在任天堂FC平台上,推出的一款多方位平面射擊游戲。游戲以坦克戰斗及保衛基地為主題,屬於策略型聯機類。同時也是FC平台上少有的內建關卡編輯器的幾個游戲之一,玩家可自己創建獨特的關卡,並通過獲取一些道具使坦克和基地得到強化。

1985年推出的坦克大戰(Battle City)由13×13大小的地圖組成了35個關卡,地形包括磚牆、海水、鋼板、森林、地板5種,玩家作為坦克軍團僅存的一支精銳部隊的指揮官,為了保衛基地不被摧毀而展開戰斗。游戲中可以獲取有多種功能的寶物,敵人種類則包括裝甲車、輕型坦克、反坦克炮、重型坦克4種,且存在炮彈互相抵消和友軍火力誤傷的設定。
1990年推出的坦克大戰較原版而言,可以選擇14種規則進行游戲(Tank A-Tank N),且敵方坦克增加了護甲,也能通過寶物讓我方陷入不利局面。寶物當中增加了能通過海水或樹林的特性。全部關卡為50關。
二、關於游戲設計
2.1 總結游戲印象
我相信坦克大戰一定是大部分80后童鞋兒時的經典,現在我們拉看看這款游戲的經典之處:
(1)一個玩家坦克,多個電腦坦克
①
②
③
④
(2)玩家可以發子彈,電腦坦克也可以發子彈
①
②
(3)電腦坦克被擊中后有爆炸效果,並且有一定幾率出現游戲道具
①
②
③
2.2 總結設計思路
(1)萬物皆對象
在整個游戲中,我們看到的所有內容,我們都可以理解為游戲對象(GameObject),每一個游戲對象,都由一個單獨的類來創建;在游戲中主要有三類游戲對象:一是坦克,二是子彈,三是道具;其中,坦克又分為玩家坦克和電腦坦克,子彈又分為玩家子彈和電腦子彈。於是,我們可以對坦克進行抽象形成一個抽象父類:TankFather,然后分別創建兩個子類:PlayerTank和EnemyTank;然后對子彈進行抽象形成一個抽象類:BulletFather,然后分別創建兩個子類:PlayerBullet和EnemyBullet。但是,我們發現這些游戲對象都有一些共同的屬性和方法,例如X,Y軸坐標,長度和寬度,以及繪制(Draw())和移動(Move())的方法,這時我們可以設計一個抽象類,形成了GameObject類:將共有的東西封裝起來,減少開發時的冗余代碼,提高程序的可擴展性,符合面向對象設計的思路:

(2)計划生育好
在整個游戲中,我們的玩家坦克對象只有一個,也就是說在內存中只需要存一份即可。這時,我們想到了偉大的計划生育政策,於是我們想到了使用單例模式。借助單例模式,可以保證只生成一個玩家坦克的實例,即為程序提供一個全局訪問點,避免重復創建浪費不必要的內存。當然,除了玩家坦克外,我們的電腦坦克集合、子彈集合等集合對象實例也保證只有一份存儲,降低游戲開銷;
(3)對象的運動
在整個游戲過程中,玩家可以通過鍵盤上下左右鍵控制玩家坦克的上下左右運動,而坦克的運動本質上還是改變游戲對象的X軸和Y軸的坐標,然后一直不間斷地在窗體上重繪游戲對象。相比玩家坦克的移動,電腦坦克的移動則完全是通過程序中設置的隨機函數控制上下左右實現的,而坦克們發出的子彈執行的運動則是從上到下或從下到上,從左到右或從右到左。

(4)設計流程圖

三、關鍵代碼實現
3.1 設計抽象父類封裝共有屬性
1 /// <summary> 2 /// 所有游戲對象的基類 3 /// </summary> 4 public abstract class GameObject 5 { 6 #region 游戲對象的屬性 7 public int X 8 { 9 get; 10 set; 11 } 12 13 public int Y 14 { 15 get; 16 set; 17 } 18 19 public int Width 20 { 21 get; 22 set; 23 } 24 25 public int Height 26 { 27 get; 28 set; 29 } 30 31 public int Speed 32 { 33 get; 34 set; 35 } 36 37 public int Life 38 { 39 get; 40 set; 41 } 42 43 44 public Direction Dir 45 { 46 get; 47 set; 48 } 49 #endregion 50 51 #region 初始化游戲對象 52 public GameObject(int x, int y, int width, int height, 53 int speed, int life, Direction dir) 54 { 55 this.X = x; 56 this.Y = y; 57 this.Width = width; 58 this.Height = height; 59 this.Speed = speed; 60 this.Life = life; 61 this.Dir = dir; 62 } 63 64 public GameObject(int x, int y) 65 : this(x, y, 0, 0, 0, 0, 0) 66 { 67 68 } 69 70 public GameObject(int x, int y, int width, int height) 71 : this(x, y, width, height, 0, 0, 0) 72 { 73 74 } 75 #endregion 76 77 #region 游戲對象公有方法 78 /// <summary> 79 /// 抽象方法:繪制自身 80 /// </summary> 81 /// <param name="g"></param> 82 public abstract void Draw(Graphics g); 83 84 /// <summary> 85 /// 虛方法:移動自身 86 /// </summary> 87 public virtual void Move() 88 { 89 switch (this.Dir) 90 { 91 case Direction.Up: 92 this.Y -= this.Speed; 93 break; 94 case Direction.Down: 95 this.Y += this.Speed; 96 break; 97 case Direction.Left: 98 this.X -= this.Speed; 99 break; 100 case Direction.Right: 101 this.X += this.Speed; 102 break; 103 } 104 // 在游戲對象移動完成后判斷一下:當前游戲對象是否超出當前的窗體 105 if (this.X <= 0) 106 { 107 this.X = 0; 108 } 109 if (this.Y <= 0) 110 { 111 this.Y = 0; 112 } 113 if (this.X >= 720) 114 { 115 this.X = 720; 116 } 117 if (this.Y >= 580) 118 { 119 this.Y = 580; 120 } 121 } 122 123 /// <summary> 124 /// 獲取所在區域,用於碰撞檢測 125 /// </summary> 126 /// <returns>矩形區域</returns> 127 public Rectangle GetRectangle() 128 { 129 return new Rectangle(this.X, this.Y, this.Width, this.Height); 130 } 131 #endregion 132 }
一切皆對象,這里封裝了游戲對象坦克和子彈以及其他游戲對象共有的屬性,以及兩個抽象方法,讓對象們(坦克?子彈?爆炸效果?出現效果?)自己去實現。
3.2 設計單例模式減少對象創建
1 /// <summary> 2 /// 單例游戲對象類 3 /// </summary> 4 public class SingleObject 5 { 6 private SingleObject() 7 { } 8 9 private static SingleObject _singleObject = null; 10 11 public static SingleObject GetInstance() 12 { 13 if (_singleObject == null) 14 { 15 _singleObject = new SingleObject(); 16 } 17 return _singleObject; 18 } 19 20 /// <summary> 21 /// 玩家坦克單一實例 22 /// </summary> 23 public PlayerTank Player 24 { 25 get; 26 set; 27 } 28 /// <summary> 29 /// 電腦坦克集合單一實例 30 /// </summary> 31 public List<EnemyTank> EnemyList 32 { 33 get; 34 set; 35 } 36 /// <summary> 37 /// 玩家坦克子彈對象集合單一實例 38 /// </summary> 39 public List<PlayerBullet> PlayerBulletList 40 { 41 get; 42 set; 43 } 44 /// <summary> 45 /// 電腦坦克子彈對象集合單一實例 46 /// </summary> 47 public List<EnemyBullet> EnemyBulletList 48 { 49 get; 50 set; 51 } 52 /// <summary> 53 /// 爆炸效果對象集合單一實例 54 /// </summary> 55 public List<Boom> BoomImageList 56 { 57 get; 58 set; 59 } 60 /// <summary> 61 /// 閃爍圖片效果集合單一實例 62 /// </summary> 63 public List<TankBorn> TankBornList 64 { 65 get; 66 set; 67 } 68 /// <summary> 69 /// 游戲道具集合單一實例 70 /// </summary> 71 public List<Prop> PropList 72 { 73 get; 74 set; 75 } 76 77 /// <summary> 78 /// 新增游戲對象 79 /// </summary> 80 /// <param name="go">游戲對象</param> 81 public void AddGameObject(GameObject go) 82 { 83 if (go is PlayerTank) 84 { 85 this.Player = go as PlayerTank; 86 } 87 else if (go is EnemyTank) 88 { 89 if (this.EnemyList == null) 90 { 91 this.EnemyList = new List<EnemyTank>(); 92 } 93 this.EnemyList.Add(go as EnemyTank); 94 } 95 else if (go is PlayerBullet) 96 { 97 if (this.PlayerBulletList == null) 98 { 99 this.PlayerBulletList = new List<PlayerBullet>(); 100 } 101 this.PlayerBulletList.Add(go as PlayerBullet); 102 } 103 else if (go is EnemyBullet) 104 { 105 if (this.EnemyBulletList == null) 106 { 107 this.EnemyBulletList = new List<EnemyBullet>(); 108 } 109 this.EnemyBulletList.Add(go as EnemyBullet); 110 } 111 else if (go is Boom) 112 { 113 if (this.BoomImageList == null) 114 { 115 this.BoomImageList = new List<Boom>(); 116 } 117 this.BoomImageList.Add(go as Boom); 118 } 119 else if (go is TankBorn) 120 { 121 if (this.TankBornList == null) 122 { 123 this.TankBornList = new List<TankBorn>(); 124 } 125 this.TankBornList.Add(go as TankBorn); 126 } 127 else if (go is Prop) 128 { 129 if (this.PropList == null) 130 { 131 this.PropList = new List<Prop>(); 132 } 133 this.PropList.Add(go as Prop); 134 } 135 else 136 { 137 return; 138 } 139 } 140 141 /// <summary> 142 /// 移除游戲對象 143 /// </summary> 144 /// <param name="go"></param> 145 public void RemoveGameObject(GameObject go) 146 { 147 if (go is PlayerTank) 148 { 149 // 玩家被擊中后 150 } 151 else if (go is PlayerBullet) 152 { 153 PlayerBulletList.Remove(go as PlayerBullet); 154 } 155 else if (go is EnemyBullet) 156 { 157 EnemyBulletList.Remove(go as EnemyBullet); 158 } 159 else if (go is EnemyTank) 160 { 161 EnemyList.Remove(go as EnemyTank); 162 } 163 else if (go is Boom) 164 { 165 BoomImageList.Remove(go as Boom); 166 } 167 else if (go is TankBorn) 168 { 169 TankBornList.Remove(go as TankBorn); 170 } 171 else if (go is Prop) 172 { 173 PropList.Remove(go as Prop); 174 } 175 else 176 { 177 return; 178 } 179 } 180 181 /// <summary> 182 /// 繪制游戲對象 183 /// </summary> 184 /// <param name="g">繪圖圖面</param> 185 public void Draw(Graphics g) 186 { 187 // Step1:繪制玩家坦克 188 if(Player != null) 189 { 190 Player.Draw(g); 191 } 192 // Step2:繪制電腦坦克 193 if(EnemyList != null) 194 { 195 for (int i = 0; i < EnemyList.Count; i++) 196 { 197 EnemyList[i].Draw(g); 198 } 199 } 200 // Step3:繪制子彈效果 201 if (PlayerBulletList != null) 202 { 203 for (int i = 0; i < PlayerBulletList.Count; i++) 204 { 205 PlayerBulletList[i].Draw(g); 206 } 207 } 208 if (EnemyBulletList != null) 209 { 210 for (int i = 0; i < EnemyBulletList.Count; i++) 211 { 212 EnemyBulletList[i].Draw(g); 213 } 214 } 215 // Step4:繪制爆炸效果 216 if (BoomImageList != null) 217 { 218 for (int i = 0; i < BoomImageList.Count; i++) 219 { 220 BoomImageList[i].Draw(g); 221 } 222 } 223 // Step5:繪制閃爍效果 224 if (TankBornList != null) 225 { 226 for (int i = 0; i < TankBornList.Count; i++) 227 { 228 TankBornList[i].Draw(g); 229 } 230 } 231 // Step6:繪制游戲道具 232 if (PropList != null) 233 { 234 for (int i = 0; i < PropList.Count; i++) 235 { 236 PropList[i].Draw(g); 237 } 238 } 239 } 240 }
這里借助單例模式,保證玩家坦克只有一個存儲,電腦坦克集合也只有一個,而具體的電腦坦克對象則分別在集合中Add和Remove。
3.3 設計道具檢測方法使玩家能夠碉堡
(1)設計游戲道具類,為三種類型的道具設置一個標志屬性:
1 /// <summary> 2 /// 游戲道具類 3 /// </summary> 4 public class Prop : GameObject 5 { 6 private static Image imgStar = Resources.star; 7 private static Image imgBomb = Resources.bomb; 8 private static Image imgTimer = Resources.timer; 9 10 /// <summary> 11 /// 游戲道具類型:0-五角星,1-炸彈,2-定時器 12 /// </summary> 13 public int PropType 14 { 15 get; 16 set; 17 } 18 19 public Prop(int x, int y, int propType) 20 : base(x, y, imgStar.Width, imgStar.Height) 21 { 22 this.PropType = propType; 23 } 24 25 public override void Draw(System.Drawing.Graphics g) 26 { 27 switch(PropType) 28 { 29 case 0: 30 g.DrawImage(imgStar,this.X,this.Y); 31 break; 32 case 1: 33 g.DrawImage(imgBomb, this.X, this.Y); 34 break; 35 case 2: 36 g.DrawImage(imgTimer, this.X, this.Y); 37 break; 38 } 39 } 40 }
(2)在單例類中創建一個判斷道具類型的方法,根據標志屬性區分不同道具,並進行對應的道具效果:
1 /// <summary> 2 /// 判斷游戲道具類型 3 /// </summary> 4 /// <param name="propType"></param> 5 public void JudgePropType(int propType) 6 { 7 switch (propType) 8 { 9 case 0:// 吃到五角星讓玩家子彈速度變快 10 if (Player.BulletLevel < 2) 11 { 12 Player.BulletLevel++; 13 } 14 break; 15 case 1:// 吃到炸彈讓一定區域內的電腦坦克爆炸 16 for (int i = 0; i < EnemyList.Count; i++) 17 { 18 // 把電腦坦克生命值設置為0 19 EnemyList[i].Life = 0; 20 EnemyList[i].IsOver(); 21 } 22 break; 23 case 2:// 吃到定時器讓所有坦克定住一段時間 24 for (int i = 0; i < EnemyList.Count; i++) 25 { 26 EnemyList[i].isPause = true; 27 } 28 break; 29 } 30 }
3.4 設計碰撞檢測方法使電腦坦克可以減少
(1)Rectangle的IntersectsWith方法

在游戲界面中,任何一個游戲對象我們都可以視為一個矩形區域(Rectangle類實例),它的坐標是X軸和Y軸,它還有長度和寬度,可以輕松地確定一個它所在的矩形區域。那么,我們可以通過Rectangle的IntersectsWith方法確定兩個Rectangle是否存在重疊,如果有重疊,此方法將返回 true;否則將返回 false。那么,在坦克大戰中主要是判斷兩種情況:一是玩家或電腦坦克發射的子彈是否擊中了對方?二是玩家是否吃到了游戲道具?
(2)在定時器事件中定期執行碰撞檢測方法
1 /// <summary> 2 /// 碰撞檢測 3 /// </summary> 4 public void CollisionDetection() 5 { 6 #region Step1:判斷玩家發射的子彈是否擊中了電腦坦克 7 // Step1:判斷玩家發射的子彈是否擊中了電腦坦克 8 if (PlayerBulletList != null) 9 { 10 for (int i = 0; i < PlayerBulletList.Count; i++) 11 { 12 for (int j = 0; j < EnemyList.Count; j++) 13 { 14 if (PlayerBulletList[i].GetRectangle().IntersectsWith(EnemyList[j].GetRectangle())) 15 { 16 // 電腦坦克減少生命值 17 EnemyList[j].Life -= PlayerBulletList[i].Power; 18 EnemyList[j].IsOver(); 19 // 移除子彈對象實例 20 PlayerBulletList.Remove(PlayerBulletList[i]); 21 break; 22 } 23 } 24 } 25 } 26 #endregion 27 28 #region Step2:判斷電腦發射的子彈是否擊中了玩家坦克 29 // Step2:判斷電腦發射的子彈是否擊中了玩家坦克 30 if (EnemyBulletList != null) 31 { 32 for (int i = 0; i < EnemyBulletList.Count; i++) 33 { 34 if (EnemyBulletList[i].GetRectangle().IntersectsWith(Player.GetRectangle())) 35 { 36 // 玩家坦克減少生命值 37 Player.Life -= EnemyBulletList[i].Power; 38 Player.IsOver(); 39 // 移除子彈對象實例 40 EnemyBulletList.Remove(EnemyBulletList[i]); 41 } 42 } 43 } 44 #endregion 45 46 #region Step3:判斷玩家是否吃到了游戲道具 47 // Step3:判斷玩家是否吃到了游戲道具 48 if (PropList != null) 49 { 50 for (int i = 0; i < PropList.Count; i++) 51 { 52 if (PropList[i].GetRectangle().IntersectsWith(Player.GetRectangle())) 53 { 54 // 播放吃到道具音效 55 SoundPlayer sp = new SoundPlayer(Resources.add); 56 sp.Play(); 57 // 增加子彈等級 58 JudgePropType(PropList[i].PropType); 59 // 移除游戲道具實例 60 PropList.Remove(PropList[i]); 61 } 62 } 63 } 64 #endregion 65 66 #region Step4:判斷電腦坦克是否和玩家坦克相撞 67 if (EnemyList != null) 68 { 69 for (int i = 0; i < EnemyList.Count; i++) 70 { 71 if (EnemyList[i].GetRectangle().IntersectsWith(Player.GetRectangle())) 72 { 73 switch (Player.Dir) 74 { 75 case Direction.Up: 76 EnemyList[i].Dir = Direction.Right; 77 break; 78 case Direction.Down: 79 EnemyList[i].Dir = Direction.Left; 80 break; 81 case Direction.Left: 82 EnemyList[i].Dir = Direction.Up; 83 break; 84 case Direction.Right: 85 EnemyList[i].Dir = Direction.Down; 86 break; 87 } 88 } 89 } 90 } 91 #endregion 92 93 #region Step5:判斷電腦坦克A是否和電腦坦克B發生了碰撞 94 // Step5:判斷電腦坦克A是否和電腦坦克B發生了碰撞 95 if (EnemyList != null) 96 { 97 for (int i = 0; i < EnemyList.Count - 1; i++) 98 { 99 for (int j = i + 1; j < EnemyList.Count; j++) 100 { 101 if (EnemyList[i].GetRectangle().IntersectsWith(EnemyList[j].GetRectangle())) 102 { 103 switch (EnemyList[i].Dir) 104 { 105 case Direction.Up: 106 EnemyList[j].Dir = Direction.Right; 107 break; 108 case Direction.Down: 109 EnemyList[j].Dir = Direction.Left; 110 break; 111 case Direction.Left: 112 EnemyList[j].Dir = Direction.Up; 113 break; 114 case Direction.Right: 115 EnemyList[j].Dir = Direction.Down; 116 break; 117 } 118 } 119 } 120 } 121 } 122 #endregion 123 }
四、個人開發小結
從下面的運行效果可以看出,此次DEMO主要完成了幾個比較核心的內容:一是玩家坦克和電腦坦克的移動,二是玩家和電腦發射子彈,三是坦克和子彈的碰撞檢測。
當然,還有很多核心的內容沒有實現,比如:計算被擊中的電腦坦克數量、游戲歡迎界面和結束界面等。希望有興趣的童鞋可以去繼續完善實現,這里提供一個我的坦克大戰實現僅供參考,謝謝!
參考資料
趙建宇,《六小時C#開發搞定坦克大戰游戲》:http://bbs.itcast.cn/thread-28540-1-1.html
附件下載
MyTankGame:http://pan.baidu.com/s/1o6wUGae
