自己动手写游戏:坦克撕逼大战
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