自己动手写游戏:坦克撕逼大战

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

作者:周旭龙

出处:http://www.cnblogs.com/edisonchou/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏编程一生

一条项目中常用的linux命令引发的经典算法题

1213
来自专栏码神联盟

语音识别 | Java 实现 AI 人工智能技术 - 语音识别功能

说到语音识别、语音翻译、图像识别、人脸识别等等,现在已经非常非常非常普及了,看过‘最强大脑’的朋友,也应该对‘小度’这个机器人有所了解,战胜国际顶尖的‘大脑’-...

1.9K5
来自专栏平凡文摘

十年之后再看“面向对象”

1153
来自专栏海天一树

NOIP普及组初赛题型分析

初赛的考察内容的一部分是计算机的基础知识,比如进制转换,工作原理,算法原理、历史事件名人等。这些对于大部分第一次参加noip的同学来说应该比较陌生,这样的知识只...

762
来自专栏WOLFRAM

一行代码论英雄—2017 Wolfram 语言“一行代码竞赛”结果

2046
来自专栏精讲JAVA

十年之后再看“面向对象”

一起帮里有人问“面向对象”的问题。但我创建“一起帮”的目的是帮人解决“具体的”“实务性的”问题,“面向对象”太过于抽象,所以没批准发布。后来在QQ群里讨论,看他...

1836
来自专栏HBStream流媒体与音视频技术

C++基础代码--20余种数据结构和算法的实现

3698
来自专栏小小挖掘机

数据城堡参赛代码实战篇(一)---手把手教你使用pandas

小编们最近参加了数据城堡(http://www.pkbigdata.com/)举办的“大学生助学金精准资助预测”比赛,分组第19名的成绩进入了复赛,很激动有木有...

3554
来自专栏牛客网

个人的前端面经,回馈社会

酷家乐(10-20k) 电话一面 三十五分钟 如何学习前端,看了什么书 谈实习经历 谈项目,问为什么用那么多插件,有没有想过自己写 position有几个属性 ...

3725
来自专栏C语言小白到大神

最美的C语言程序流体,你会做吗?

想好好学C语言,尽量不要单一的只是看书(尤指谭浩强老师的,这是来自众多社群小伙伴的建议),多动手才是王道。

1160

扫码关注云+社区

领取腾讯云代金券