网上坦克大战的游戏并不少,包括单机版,网络版。然而,作为一名程序员,学一门语言的最好方式莫过于写程序。在写的过程中,你会遇到很多的问题,这个时候你再去找原因,查帮助文档,谷歌百度等等,你的能力才会进步的更快。
好了,废话不多说,下面开始介绍这个坦克大战。目前只是单机版,后面我会升级为网络版。
可以开始、暂停、继续、退出游戏。
可以设置游戏的一些参数:包括我的坦克的生命条数,敌人坦克的总数量,同一时间敌人的坦克的最多数量、游戏时间。
1.坦克(包括我的坦克和敌人坦克)可以上、下、左、右、左上、左下、右上、右下八个方向移动,坦克不会出界(游戏窗口)。
2.坦克可以随机发射,敌人坦克间断的发射,间断时间在1-2秒之间随机,我的坦克发射由人为按键控制发射。
3.. 敌人坦克可以随机移动,发射,改变方向,敌人坦克之间不会相撞,当敌人坦克预测到以现在的方向继续移动会撞击到自己的队友时(我的坦克除外,它可以撞击我的坦克),它会立即改变方向。
4. 当战场上的敌人的数量少于游戏设置的同一时间敌人的坦克的最多数量的值时,游戏引擎会自动复活敌人坦克;同理,我的坦克死后也被自动的复活,除非我的命数为0时。
5. 坦克有两种状态:无敌状态和非无敌状态。坦克刚复活时的5秒钟为无敌状态,此时对方的子弹对它无效。当双方都为无敌状态时,相撞都不会死;当双方坦克其中一方为无敌状态时,撞上它的对方坦克会死;当双方都不是无敌状态时,双方坦克相撞,我的坦克会死。
6.在窗口右上角会显示当前的游戏信息,包括我的坦克剩余命数,敌人坦克剩余命数,杀死敌人数量,游戏剩余时间。
确定这个游戏有哪些对象,应该抽象出来那些类型。这个比较简单(相对于我这个小游戏来说,实际软件开发中,如果考虑到可扩展性、高类聚、低耦合,封装类型并不是一个简单的事情), 坦克类(这可以作为基类型,分别派生出敌人坦克和我的坦克)、子弹类、爆炸类、还有一个控制游戏的类,游戏引擎类。
坦克类型:属性(宽度、高度、颜色、形状、方位、当前位置、移动速度等),方法(移动、发射、销毁、复活等)。
子弹类型:属性(宽度、高度、颜色、形状、方位、当前位置、移动速度、所属坦克等),方法(移动、命中检测、出界处理等)。
爆炸类型:属性(颜色、爆炸形状、爆炸位置、爆炸速度等),方法(开始爆炸、爆炸玩处理)。
游戏引擎:属性(游戏窗口宽度、游戏窗口高度、游戏状态、定时器、线程、坦克容器、子弹容器、爆炸容器等),方法(开始游戏、暂停游戏、结束游戏、退出游戏、添加菜单、添加资源等)。
当然,以上属性和方法并不完全,只是列出了初步定义,在实际编程中在做修改和扩充。在坦克类、子弹类、爆炸类封装好后,就可以在游戏引擎类中消费这些类的实例了。
这里,因为是游戏,所以是有画面的,而且必须是动态的画面,所谓的动态画面就是一副副的图片连续播放,而且之间间隔时间必须足够短,才能使人的视觉感觉是连贯的。所以这里要用到重绘,坦克要重绘,子弹也要重绘,所有要出现在游戏画面的对象就必须不停的进行重绘。
所以这里必须有一个方法执行重绘,而且要间隔很短的时间就执行一次。我们知道java里面的component是一个具有图形表示能力的对象,它是所有可视化对象的基类,它有一个方法repaint(),这个方法就是重绘组件的。在api文档中,这个方法的介绍是:如果此组件是轻量级组件,则此方法会尽快调用此组件的paint 方法。否则此方法会尽快调用此组件的update 方法。什么是轻量级组件、重量级组件?因为是初学,我也不懂,所以百度谷歌了一下,网上是这样介绍的:轻量级组件是用JAVA代码画出来的,这样具有平台移植性,重量级组件是调用操作系统的函数画出来的组件,比如主窗体。
这里,坦克类、子弹类、爆炸类我没有继承任何类型,所以它的基类默认是object类,这就表示它 不是作为可以在界面显示的对象,没有一个paint()方法给你重写。但是没有关系,我们可以给它们添加一个方法draw(),在这个方法里,你可以把它自己绘画出来。
这里,必须有一个窗口显示游戏,而游戏引擎类作为游戏控制的核心,所以我把游戏引擎类继承自Frame类,这样就有了一个可以显示的窗口。根据轻量级组件、重量级组件的定义,显然现在游戏引擎类属于重量级组件。由此,我只需要重写游戏引擎类的update()方法,在update()方法里,把所有需要绘画的对象绘画一遍,但调用的时候还是要调用repaint()方法,后面,会在我遇到的问题中说明原因。
再回到前面,现在要解决怎样才能不停的执行绘画方法,也就是游戏引擎类的update()方法。前面说过,绘画方法必须在游戏期间间隔很短时间重复的调用。但是,到目前程序中只有一条线程,也就是主线程,不可能让主线程来不停的调用update()方法,这个不用解释。所以,现在需要另外的线程来执行绘画。现在有两种方式,一种是使用线程Thread,一种是使用定时器Timer,应该来说定时器也算是一个线程。使用Thread就必须实现Runnable接口,重写run()方法,在这个run()方法做死循环,还要让线程sleep()一下,由于使用Thread不能保证每次执行的间隔时间绝对相等,所以推荐使用定时器Timer,因为你要保证重绘的方法每隔一段时间后就执行,这个间隔时间要所有时候都相同。
还有一些问题是需要注意的,以下是我在实际写代码的时候碰到的问题,虽然只是一些很小的问题,但却是很重要的问题,写下来只是作为自己在程序员路上的一个成长足迹和经验积累。我并不认为出问题是一件坏事,因为,出问题的地方往往是你欠缺的地方。通过这次问题,如果你能够找到原因,并学习到了如何解决,这不就是一个很小的进步吗?
现象:
运行游戏后,虽然坦克能够显示在窗口上,但是坦克图像很频繁的闪烁。
原因:
为什么会闪烁,自己试着思考了一下,肯定跟重绘有关,但是那里有问题还是不清楚。那就要寻求帮助了,在百度谷歌上面关键字搜索了下,这是因为:游戏引擎类从Frame继承,属于重量级组件,它的repaint()方法在我的程序中每50毫秒被调用一次,repaint()方法会调用update()方法,update()方法会调用paint()方法,update()方法执行过程是:如果该组件是轻量级组件,那么它的绘画过程是先用重量级组件(游戏窗口)的背景色覆盖整个组件,然后再调用paint()方法,重新绘制。
当然,坦克没有paint()方法重写,但是我重写了游戏引擎类的paint()方法,在paint()方法里调用坦克自己定义的绘画方法。
因为坦克属于轻量级组件,它的每次绘画过程都是:先用游戏引擎(游戏窗口)的背景色覆盖自己,然后在绘画自己,但是,正是这种先用背景色覆盖组件再重绘图像的方式导致了闪烁。
在两次看到不同位置坦克的中间时刻,总是存在一个在短时间内被绘制出来的背景画面。但即使时间很短,如果重绘的面积较大的话花去的时间也是比较可观的,这个时间甚至可以大到足以让闪烁严重到让人无法忍受的地步。
另外,调用坦克的绘画方法在屏幕上直接绘图的时候,由于执行的语句比较多,程序不断地改变窗体中正在被绘制的图象,会造成绘制的缓慢,这也从一定程度上加剧了闪烁。
解决:
所有问题都在update()方法里,这要用到双缓冲技术解决,所谓双缓冲技术,就是先在内存中分配一个和我们动画窗口一样大小的BufferedImage对象,然后利用getGraphics()方法去获得BufferedImage对象双缓冲画笔,接着利用这个双缓冲画笔绘画我们需要绘画的所有内容,最后将它全部一次性的绘画到我门的窗口上.这样在我们的动画窗口上面是显示出来就非常的流畅了.避免了画面的闪烁。双缓冲的调用过程是repaint()——>update()——>paint(),而这个双缓冲要写在update()方法里,paint()方法里是你所有要绘画的内容。
现象:
在程序运行的过程中,程序在某一时刻突然窗口上所有画面都静止了,最小化,然后最大化后,窗口的所有对象都消失了。显然,绘画线程出现了问题,关闭按钮同样也没有响应,只能用任务管理器了,打开任务管理器后,发现javaw.exe进程的cpu使用率高达60%。
原因:
关掉程序后,现在就要查找原因。首先进入调试运行,看看现象会不会重现,同时,我也在观察程序cpu的使用大小、内存使用大小,我注意到内存的大小在不断的增加,而且变化速度较快,到达一定的值后,又突然降下来,应该是垃圾回收器在回收子弹对象。虽然造成程序停止的原因大概不会是内存使用的大小,但出现内存的抖动的现象不能够不重视。所以有了后面的第三个问题。
继续调试中,上面的现象又出现了,而且eclipse没有提示有任何错误,javaw.exe的cpu使用率高达90%,判断绘画线程应该进入了死循环,依次在程序中所有的循环语句中加入断点,果然,绘画线程执行到坦克的移动方法内部的while循环内出不来,导致绘画线程的绘画方法不会被执行,所以出现了上面的现象。
解决:
问题找到了,就好解决了,看看while循环语句块为什么会导致死循环,做出修改。
坦克、子弹、爆炸的绘画方法、移动方法我都放在了游戏绘画线程,调用顺序是执行完绘画方法后,马上执行移动方法。这里,绘画、移动是两个不同的功能模块,放在一起并不合适,对此,我在后面做了分开处理。
由于初学,刚开始设计游戏中的这些类的对象的管理时,首先想到的是完成功能,其它的一些时间效率、空间效率没有作过多考虑,才出现了内存抖动现象。
现象:
游戏运行的过程中,游戏消耗的内存急速增长,一段时间后,游戏消耗的内存又突然降下来许多,接着,又是急速增长,一段时间后,又降下来许多,如此反复。
原因:
首先,在高频率执行的代码里使用new 创建对象。
这是非常错误的编程方式。虽然,java有垃圾回收机制,但因为它是定时回收,所以并不是说一个对象一旦没有对象引用指向它,它就会马上被垃圾收集器回收。
以我的这个程序为例,因为要完成敌人坦克之间不会碰撞的功能,所以敌人的每个坦克在移动之前,都会判断如果以现在的方向移动,我的坦克的区域是否和队友坦克的区域相交。
注意,因为敌人坦克是不停移动的,所以每个敌人坦克的区域是不断变化的。现在需要一个方法来获得每个敌人坦克的区域,那么这个方法被调用的频率和移动的频率一样,这个频率是非常快的,以我的程序为例,每50毫秒调用一次坦克的移动方法,也就是说获得敌人坦克区域的方法也是50毫秒执行一次,如果在这个方法里new出来几个对象,内存的变化是非常大的,我原来没有意识到这个问题,在获得敌人坦克区域的方法里面用了new来创建对象,因为这个方法被执行的频率比较高,所以导致内存增速过快。
所以,尽可能的不要在高频率执行的代码里使用new来创建对象,如果能用成员变量就用成员变量。
其次,对程序的资源没有进行管理的概念,没有思考如何管理资源能够使内存消耗更小,内存消耗更加稳定。
以我的程序为例,对于坦克、子弹、爆炸的管理,毫无疑问,要用到容器。本程序中因为频繁的对容器的数据进行添加、移除操作,所以在容器的选择上,首选LinkedList
我的最初解决方案是:
我的坦克:在游戏初始化时,根据设置的参数(我的坦克命数),来向我的坦克容器添加我的坦克对象,这个数字(以我的程序为例)为5、10、15之一。
敌人坦克:在游戏初始化时,根据设置的参数(敌人坦克命数),来向敌人坦克容器添加敌人坦克对象,这个数字(以我的程序为例)为50、100、150之一。
子弹:在游戏初始化时,子弹容器为空,在游戏中每当坦克发射一次,new出来一个子弹对象添加到子弹容器里,当子弹出界或者击中坦克时,再从子弹容器中移除这颗子弹对象。
爆炸:在游戏初始化时,爆炸容器为空,在游戏中每当子弹击中坦克或者双方坦克相撞时,new出来一个爆炸对象添加到爆炸容器,当爆炸结束后,再从爆炸容器中移除这个爆炸对象。
解决:
针对第一点,在高频率执行的代码中,不要用new创建对象,用成员变量代替,其它的地方能够不用new的就不要用new创建对象。
针对第二点,改进后的方案是:
我的坦克:在游戏初始化时,只需要new一个我的坦克对象,以后也不需要new更多的我的坦克对象,我的坦克的生命条数用一个int类型计数,当我的坦克被打中后,马上执行坦克的销毁方法,只要我的坦克的命数大于零,几秒钟之后(定时器实现),就调用坦克的复活方法,这样我的坦克又可以重新作战了。
敌人坦克:在游戏初始化时,根据设置的参数(同一时间内敌人的最多数量),来向敌人坦克容器添加敌人坦克对象,这个数字(以我的程序为例)为5、8、10之一,同样,以后也不需要在new更多的敌人坦克,也是被打中后销毁,然后再复活(如果剩余敌人数量大于零)。
子弹:现在子弹容器有两个,在游戏初始化时,一个子弹容器为空,里面放正在飞行的子弹对象,另一个子弹容器添加一定数量的子弹对象(经过测试50个就够了,因为同一时间所有飞行的子弹对象个数基本不超过30个),里面的子弹对象是静止和不可显示的。当坦克发射时,从静止子弹容器取出来一个子弹对象来,给它的属性(飞行位置、方向等)赋值后,放入飞行子弹容器。当飞行的子弹对象出界或者击中坦克后,给它的属性值初始化为静止的子弹的默认值后,把它添加到静止子弹容器。
爆炸:爆炸容器也有两个,管理方式同子弹,经测试,放未引爆的爆炸容器的爆炸对象10个就已经足够。因为爆炸对象的管理方式同子弹对象管理方式大同小异,就不多做介绍了。
很明显,程序优化后,消耗的内存更加小,也没有内存的抖动现象。
最后还有一个问题,简单点说,就是多线程中的多个线程在同一时间内对同一个对象的同一个数据进行读取造成数据不安全性。
首先分析一下程序有没有可能存在这种情况。 因为要实现敌人坦克随机发射,就是间隔一段时间发射一次,而且这个时间不是一个固定值,而是在一个范围内的随机值,我用了另外一个线程来控制敌人坦克的随机发射,而我的坦克的发射是人为按键控制,在主线程中执行。坦克的发射要从静止子弹容器中取出一颗子弹来,现在就完全有可能我的坦克和敌人坦克同时访问游戏引擎对象的同一个数据(静止子弹容器),这就会造成问题。
当然我的程序中还有其他的这样的可能,我就不一一列举,道理是一样的。为了解决这个问题,就要用到synchronized关键字,来实现进程同步。
关于线程同步,网上有很多的参考资料,鉴于我能力有限,大家可以自己学习。