项目名称:坦克大战

项目背景:坦克大战是一款非常经典的游戏,也是学习面向对象编程的理想实例。现在面向对象的计算机编程语言很多,很多想法都可以通过编程来实现。本文的坦克大战有完整的界面,能够实现人机大战,它规则简单,玩家只需要消灭这些敌人,所以上手非常容易,用户不仅可以感受到游戏中的乐趣,它也给用户提供了一个展现自己高超技艺的场所。

项目分工:

黎丹静(2012211846):游戏开发,性能分析

段赛赛(2012211790):游戏测试,文档编写

项目基本信息:

编程语言:java

编译环境:JDK1.7  Eclipse4.3

操作系统:Win7

体系结构:x86  32bit

项目需求:

1.坦克能够四处移动

2.坦克能够打击敌人

3.敌人能移动

4.被击中之后能够模拟爆炸

5.坦克移动过程能够产生障碍

6.坦克能够增长生命

游戏功能介绍:玩家可以通过键盘“上”“下”“左”“右”键来移动坦克,“Ctrl”键发射子弹,“A”键向周围八个方向发射子弹,当子弹“碰到”敌方的坦克,敌方的坦克将会“爆炸”。当主战坦克被地方坦克攻击,那么血量就会减少,即坦克上方的红色条会变短,直到血量为0止,但主战坦克可以通过“吃”界面中的血块来补充血量,如果主战坦克死亡,可以按“F2”重新开始。为了增强趣味性,我们加上了四堵墙,敌人的坦克不能通过墙体,但主战坦克可以通过墙体。

游戏开发过程总结:游戏的开发并不是一次就能写到位的,需要分成多个版本来完善,本次游戏开发最终版本为TankWar2.9,将一些开发过程罗列如下,以便后来者(主要针对初学者,大神直接略过不用看)学习参考。

版本0.1:

功能:产生一个窗口

版本0.2:

功能:1.添加关闭窗口的事件处理;2.不允许窗口的大小改动

版本0.3:

功能:画出代表坦克的实心圆

版本0.4:

功能:让坦克动起来

步骤:1.讲位置改变为变量;2.启动线程不断重画;3.每次重画改变坦克位置

版本0.41:

功能:使用双缓冲消除闪烁现象

闪烁原因:1.刷新重画频率太快,paint方法还没有完成;2.逐条显示

解决办法:将所有东西画在虚拟图片上,一次性显示出来

版本0.5:

功能:代码重构(将以后可能需要多处改变的量定义为常量)

版本0.6:

功能:让坦克听从玩家的指挥

解决办法:1.添加键盘监听器类KeyMonitor;2.TankClient添加键盘监听器;3.针对不同的键改变坦克的位置,与重画线程结合产生不同方向运动

版本0.7:

功能:讲坦克独立包装成类

版本0.8:

功能:让主坦克向8个方向行走(1)

步骤:1.添加记录按键状态的布尔量;2.添加代表方向的量(使用枚举);3.根据按键状态确定Tank方向;4.根据方向进行下一步的移动(move)

版本0.9:

功能:让主坦克向8个方向行走(2)

步骤:1.处理键抬起的消息;2.修改TankClient相关代码

版本1.0:

功能:添加子弹类

版本1.1:

功能:根据主战坦克的方向和位置,打出子弹

版本1.2:

功能:为了解决坦克停下也能打出炮弹的问题,画出炮筒

版本1.3:

功能:打出多发炮弹

版本1.4:

功能:1.解决炮弹不消亡问题;2.解决坦克出界问题

版本1.5:

功能:画一辆敌人的坦克

版本1.6:

功能:将敌人坦克击毙

版本1.7:

功能:加入爆炸

版本1.8:

功能:添加多辆坦克

版本1.9:

功能:让敌军坦克更智能(1.让敌军坦克动起来;2.让敌军坦克向随机方向移动;3.让敌军坦克发射炮弹;4.敌军炮火不能太猛烈)

版本2.0:

功能:添加两堵墙

版本2.1:

功能:坦克不能相互穿越

版本2.2:

功能:处理超级炮弹

版本2.3:

功能:添加主战坦克的生命值

版本2.4:

功能:图形化表示主战坦克的生命值

版本2.5:

功能:添加血块

步骤:1.添加blood类;2.添加必要的方法;3.让blood对象固定轨迹运动, 并在一定时间后消失

版本2.6:

功能:最后的修正(1.敌人死光了重新加入;2.我军死掉了F2开始)

反思坦克的不足之处:第一,面向对象的组织不是很完美(1.细小的毛病:退出时控制线程结束;2.专门的GAME API;3.主战坦克和敌人坦克应当分开控制)。第二,界面不美观。反思之后进行以下版本代码的编写:

版本2.7:

功能:修正上一版本不是很合理的地方,更改enum Direction为单独的类,区分不同炮弹的颜色

版本2.8:

功能:加入图片(1.在classpath中添加资源;2.运用反射)

版本2.9

功能:配置文件的使用(Properties类,Singleton模式)

系统模块设计:part1.开发游戏界面;part2.设置消息响应;part3:实现游戏逻辑。

系统结构:

java 射击游戏 java经典射击游戏大全_软件工程


java 射击游戏 java经典射击游戏大全_面向对象_02

系统工程的创建:

对上述工程架构介绍如下:


(1)src目录:主要是完成工程的Java代码编写。

(2)gen目录:系统自动生成的源代码目录,其中含有非常重要的R.java文件,它所包含的每一种静态内部类都对应着相应的一种资源。

(3)assets目录:data资源目录

(4)bin目录:输出文件夹。

(5)res目录:资源文件夹。

代码实现(仅提供部分主要代码):

TankClient.java:

import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;

public class TankClient extends Frame {
	public static final int GAME_WIDTH = 800;
	public static final int GAME_HEIGHT = 600;
	
	Integer integer1 = 1, integer2 = 2, integer3 = 3, integer4 = 4;
	
	Tank myTank = new Tank(50, 50, true, Direction.STOP, this);
	
	Wall w1 = new Wall(100, 200, integer1, this), w2 = new Wall(300, 100, integer2, this), w3 = new Wall(650, 200, integer3, this), w4 = new Wall(300, 450, integer4, this);
	
	List<Explode> explodes = new ArrayList<Explode>();
	List<Missile> missiles = new ArrayList<Missile>();
	List<Tank> tanks = new ArrayList<Tank>();
	
	Missile m = null;
	Image offScreenImage = null;
	Blood b = new Blood();
	
	public void paint(Graphics g) {
		Color c = g.getColor();
		g.setColor(Color.WHITE);
		g.drawString("missiles count:" + missiles.size(), 10, 50);
		g.drawString("explodes count:" + explodes.size(), 10, 70);
		g.drawString("tanks count:" + tanks.size(), 10, 90);
		g.drawString("tanks life:" + myTank.getLife(), 10, 110);
		g.setColor(c);
		if(tanks.size() <= 0) {
			for(int i = 0; i < Integer.parseInt(PropertyMgr.getProperty("reProduceTankCount")); i++) {
				tanks.add(new Tank(50 + 40 * (i + 1), 50, false, Direction.D, this));
			}
		}
		for(int i = 0; i < missiles.size(); i++) {
			Missile m = missiles.get(i);
			m.hitTanks(tanks);
			m.hitTank(myTank);
			m.hitWall(w1, 1);
			m.hitWall(w2, 2);
			m.hitWall(w3, 3);
			m.hitWall(w4, 4);
			m.draw(g);
		}
		for(int i = 0; i < explodes.size(); i++) {
			Explode e = explodes.get(i);
			e.draw(g);
		}
		for(int i = 0; i < tanks.size(); i++) {
			Tank t = tanks.get(i);
			t.collidesWithWall(w1, 1);
			t.collidesWithWall(w2, 2);
			t.collidesWithWall(w3, 3);
			t.collidesWithWall(w4, 4);
			t.collidesWithTanks(tanks);
			t.draw(g);
		}
		myTank.draw(g);
		myTank.eat(b);
		w1.draw(g);
		w2.draw(g);
		w3.draw(g);
		w4.draw(g);
		b.draw(g);
	}
	public void update(Graphics g) {
		if(offScreenImage == null) {
			offScreenImage = this.createImage(GAME_WIDTH, GAME_HEIGHT);
		}
		Graphics gOffScreen = offScreenImage.getGraphics();
		Color c = gOffScreen.getColor();
		gOffScreen.setColor(Color.BLACK);
		gOffScreen.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
		gOffScreen.setColor(c);
		paint(gOffScreen);
		g.drawImage(offScreenImage, 0, 0, null);
	}
	public void launchFrame() {
		//显示游戏主窗口
		int initTankCount = Integer.parseInt(PropertyMgr.getProperty("initTankCount"));
		for(int i = 0; i < initTankCount; i++) {
			tanks.add(new Tank(50 + 40 * (i + 1), 50, false, Direction.D, this));
		}
		
		this.setLocation(400, 300);
		this.setSize(GAME_WIDTH, GAME_HEIGHT);
		this.setTitle("TankWar");
		this.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				System.exit(0);
			}});
		this.setResizable(false);
		this.setBackground(Color.GREEN);
		this.addKeyListener(new KeyMonitor());
		this.setVisible(true);
		new Thread(new PaintThread()).start();
	}
	public static void main(String[] args) {
		TankClient tc = new TankClient();
		tc.launchFrame();
	}
	private class PaintThread implements Runnable {
		public void run() {
			while(true) {
				repaint();
				try {
					Thread.sleep(50);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	private class KeyMonitor extends KeyAdapter {
		public void keyReleased(KeyEvent e) {
			myTank.keyReleased(e);
		}
		public void keyPressed(KeyEvent e) {
			myTank.keyPressed(e);
		}
	}
}


Tank.java:

import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Tank {
	public static final int XSPEED = 5;
	public static final int YSPEED = 5;
	public static final int WIDTH = 30;
	public static final int HEIGHT = 30;
	
	TankClient tc;
	
	private boolean good;
	
	private boolean live = true;
	private int life = 100;
	
	private int x, y;
	private int oldX, oldY;
	
	private BloodBar bb = new BloodBar();
	
	private static Random r = new Random();
	
	private boolean bL = false, bU = false, bR = false, bD = false;
	
	
	private Direction dir = Direction.STOP;
	private Direction ptDir = Direction.D;
	private int step = r.nextInt(18) + 3;
	
	private static Toolkit tk = Toolkit.getDefaultToolkit();
	private static Image[] tankImages = null;
	private static Map<String, Image> imgs = new HashMap<String, Image>();
	static {
		tankImages = new Image[] {
				tk.getImage(Tank.class.getClassLoader().getResource("images/tankL.gif")),
				tk.getImage(Tank.class.getClassLoader().getResource("images/tankR.gif")),
				tk.getImage(Tank.class.getClassLoader().getResource("images/tankU.gif")),
				tk.getImage(Tank.class.getClassLoader().getResource("images/tankD.gif")),
				tk.getImage(Tank.class.getClassLoader().getResource("images/tankLU.gif")),
				tk.getImage(Tank.class.getClassLoader().getResource("images/tankLD.gif")),
				tk.getImage(Tank.class.getClassLoader().getResource("images/tankRU.gif")),
				tk.getImage(Tank.class.getClassLoader().getResource("images/tankRD.gif"))
		};
		imgs.put("L", tankImages[0]);
		imgs.put("R", tankImages[1]);
		imgs.put("U", tankImages[2]);
		imgs.put("D", tankImages[3]);
		imgs.put("LU", tankImages[4]);
		imgs.put("LD", tankImages[5]);
		imgs.put("RU", tankImages[6]);
		imgs.put("RD", tankImages[7]);
		
	}
	
	public Tank(int x, int y, boolean good) {
		this.x = x;
		this.y = y;
		this.oldX = x;
		this.oldY = y;
		this.good = good;
	}
	public Tank(int x, int y, boolean good, Direction dir, TankClient tc) {
		this(x, y, good);
		this.dir = dir;
		this.tc = tc;
	}
	public void draw(Graphics g) {
		if(!live){
			if(!good) {
				tc.tanks.remove(this);
			}
			return;
		}
		if(good) bb.draw(g);
		switch(ptDir) {
		case L:
			g.drawImage(imgs.get("L"), x, y, null);
			break;
		case LU:
			g.drawImage(imgs.get("LU"), x, y, null);
			break;
		case U:
			g.drawImage(imgs.get("U"), x, y, null);
			break;
		case RU:
			g.drawImage(imgs.get("RU"), x, y, null);
			break;
		case R:
			g.drawImage(imgs.get("R"), x, y, null);
			break;
		case RD:
			g.drawImage(imgs.get("RD"), x, y, null);
			break;
		case D:
			g.drawImage(imgs.get("D"), x, y, null);
			break;
		case LD:
			g.drawImage(imgs.get("LD"), x, y, null);
			break;
		case STOP:
			break;
		}
		move();
	}
	void move() {
		this.oldX = x;
		this.oldY = y;
		
		switch(dir) {
		case L:
			x -= XSPEED;
			break;
		case LU:
			x -= XSPEED;
			y -= YSPEED;
			break;
		case U:
			y -= YSPEED;
			break;
		case RU:
			x += XSPEED;
			y -= YSPEED;
			break;
		case R:
			x += XSPEED;
			break;
		case RD:
			x += XSPEED;
			y += YSPEED;
			break;
		case D:
			y += YSPEED;
			break;
		case LD:
			x -= XSPEED;
			y += YSPEED;
			break;
		case STOP:
			break;
		}
		if(this.dir != Direction.STOP) {
			this.ptDir = this.dir;
		}
		if(x < 0) x = 0;
		if(y < 0) y = 0;
		if(x + Tank.WIDTH > TankClient.GAME_WIDTH) x = TankClient.GAME_WIDTH - Tank.WIDTH;
		if(y + Tank.HEIGHT > TankClient.GAME_HEIGHT) y = TankClient.GAME_HEIGHT - Tank.HEIGHT;
		if(!good) {
			Direction[] dirs = Direction.values();
			if(step == 0) {
				step = r.nextInt(18) + 3;
				int rn = r.nextInt(dirs.length);
				dir = dirs[rn];
			}
			step --;
			if(r.nextInt(40) > 38) {
				this.fire();
			}
		}
		
	}
	public void keyPressed(KeyEvent e) {
		//根据键盘输入控制方向
		int key = e.getKeyCode();
		switch(key) {
			case KeyEvent.VK_F2:
				if(!this.live) {
					this.live = true;
					this.life = 100;
				}
				break;
			case KeyEvent.VK_LEFT:
				bL = true;
				break;
			case KeyEvent.VK_UP:
				bU = true;
				break;
			case KeyEvent.VK_RIGHT:
				bR = true;
				break;
			case KeyEvent.VK_DOWN:
				bD = true;
				break;
		}
		locateDirection();
	}
	void locateDirection() {
		//确定坦克的方向
		if(bL && !bU && !bR && !bD) dir = Direction.L;
		else if(bL && bU && !bR && !bD) dir = Direction.LU;
		else if(!bL && !bU && bR && !bD) dir = Direction.R;
		else if(!bL && bU && bR && !bD) dir = Direction.RU;
		else if(!bL && bU && !bR && !bD) dir = Direction.U;
		else if(!bL && !bU && !bR && bD) dir = Direction.D;
		else if(bL && !bU && !bR && bD) dir = Direction.LD;
		else if(!bL && !bU && bR && bD) dir = Direction.RD;
		else if(!bL && !bU && !bR && !bD) dir = Direction.STOP;
	}
	private void stay () {
		x = oldX;
		y = oldY;
	}
	public void keyReleased(KeyEvent e) {
		//根据键盘释放控制方向
		int key = e.getKeyCode();
		switch(key) {
		case KeyEvent.VK_CONTROL:
			tc.m = fire();
			break;
		case KeyEvent.VK_LEFT:
			bL = false;
			break;
		case KeyEvent.VK_UP:
			bU = false;
			break;
		case KeyEvent.VK_RIGHT:
			bR = false;
			break;
		case KeyEvent.VK_DOWN:
			bD = false;
			break;
		case KeyEvent.VK_A:
			superFire();
			break;
		}
		locateDirection();
	}
	public boolean isGood() {
		return good;
	}
	public void setLife(int life) {
		this.life = life;
	}
	public int getLife() {
		return life;
	}
	public boolean isLive() {
		return live;
	}
	public void setLive(boolean live) {
		this.live = live;
	}
	public Missile fire() {
		//打出子弹
		if(!live) return null;
		int x = this.x + Tank.WIDTH/2 - Missile.WIDTH/2;
		int y = this.y + Tank.HEIGHT/2 - Missile.HEIGHT/2;
		Missile m = new Missile(x, y, good, ptDir, this.tc);
		tc.missiles.add(m);
		return m;
	}
	public Missile fire(Direction dir) {
		if(!live) {
			return null;
		}
		int x = this.x + Tank.WIDTH/2 - Missile.WIDTH/2;
		int y = this.y + Tank.HEIGHT/2 - Missile.HEIGHT/2;
		Missile m = new Missile(x, y, good, dir, this.tc);
		tc.missiles.add(m);
		return m;
	}
	public Rectangle getRect() {
		return new Rectangle(x, y, tankImages[0].getWidth(null), tankImages[0].getHeight(null));
	}
	public boolean collidesWithWall (Wall w, Integer integer) {
		//坦克与墙相撞
		if(this.live && this.getRect().intersects(w.getRect(integer))) {
			this.stay();
			return true;
		}
		return false;
	}
	public boolean collidesWithTanks (java.util.List<Tank> tanks) {
		for(int i = 0; i < tanks.size(); i++) {
			Tank t = tanks.get(i);
			if(this != t) {
				if(this.live && t.isLive() && this.getRect().intersects(t.getRect())) {
					this.stay();
					t.stay();
					return true;
				}
			}
		}
		return false;
	}
	private void superFire() {
		Direction[] dirs = Direction.values();
		for(int i = 0; i < 8; i++) {
			fire(dirs[i]);
		}
	}
	private class BloodBar {
		//血量
		public void draw(Graphics g) {
			Color c = g.getColor();
			g.setColor(Color.RED);
			g.drawRect(x, y - 10, WIDTH, 10);
			int w = WIDTH * life/100;
			g.fillRect(x, y - 10, w, 10);
			g.setColor(c);
		}
	}
	public boolean eat(Blood b) {
		//充血
		if(this.live && b.isLive() && this.getRect().intersects(b.getRect())) {
			this.life = 100;
			b.setLive(false);
			return true;
		}
		return false;
	}
}

项目测试:

对程序各个模块进行测试:

1)开始游戏模块:

测试方法:运行游戏。

测试结果:直接转至游戏主界面的功能。

2)发射子弹模块:

测试方法:点击“A”键盘,进行测试。

测试结果:实现了坦克发射子弹的功能。

3)攻击坦克模块:

测试方法:点击“A”键盘,并击中敌方坦克,进行测试。

测试结果:敌方坦克爆炸

4)对方向测试:

测试方法:点击“上”“下”“左”“右”键,进行测试。

测试结果:坦克根据用户的指令进行转动和移动。

游戏系统性能分析 经过对系统进行测试和运行。总结出游戏系统性能如下:

1、界面友好,游戏操作方便

      界面简单,功能较完善,游戏操作简单。

2、系统响应较快,运行较稳定

      在系统运行时,游戏过程中的消息响应处理很快,且系统整体运行安全且稳定。

3、部分系统功能仍需完善

      由于开发时间和技术等方面的原因,没有添加声音等效果,还可以进一步完善。


运行截图:

java 射击游戏 java经典射击游戏大全_面向对象_03

java 射击游戏 java经典射击游戏大全_游戏开发_04



java 射击游戏 java经典射击游戏大全_java 射击游戏_05


性能测试:

程序运行前:

java 射击游戏 java经典射击游戏大全_软件工程_06

VisualVM 通过检测 JVM 中加载的类和对象信息等帮助我们分析内存使用情况,我们可以通过 VisualVM 的监视标签和 Profiler 标签对应用程序进行内存分析。在监视标签内,我们可以看到实时的应用程序内存堆以及永久保留区域的使用情况。

内存堆使用情况:

java 射击游戏 java经典射击游戏大全_游戏开发_07

永久保留区域使用结果:

java 射击游戏 java经典射击游戏大全_软件工程_08

程序运行后:


java 射击游戏 java经典射击游戏大全_面向对象_09

内存堆使用情况:


java 射击游戏 java经典射击游戏大全_java_10

永久保留区域使用结果:

java 射击游戏 java经典射击游戏大全_java 射击游戏_11

通过内存性能分析结果,我们可以查看哪些对象占用了较多的内存,存活的时间比较长等,以便做进一步的优化。

内存分析结果:


java 射击游戏 java经典射击游戏大全_java 射击游戏_12

VisualVM 能够监控应用程序在一段时间的 CPU 的使用情况,显示 CPU 的使用率、方法的执行效率和频率等相关数据帮助我们发现应用程序的性能瓶颈。我们可以通过 VisualVM 的监视标签和 Profiler 标签对应用程序进行 CPU 性能分析。

在监视标签内,我们可以查看 CPU 的使用率以及垃圾回收活动对性能的影响。过高的 CPU 使用率可能是由于我们的项目中存在低效的代码,可以通过 Profiler 标签的 CPU 性能分析功能进行详细的分析。如果垃圾回收活动过于频繁,占用了较高的 CPU 资源,可能是由内存不足或者是新生代和旧生代分配不合理导致的等。

CPU使用情况:


java 射击游戏 java经典射击游戏大全_java 射击游戏_13

CPU性能分析:


java 射击游戏 java经典射击游戏大全_java_14

线程分析:

Java 语言能够很好的实现多线程应用程序。当我们对一个多线程应用程序进行调试或者开发后期做性能调优的时候,往往需要了解当前程序中所有线程的运行状态,是否有死锁、热锁等情况的发生,从而分析系统可能存在的问题。

在 VisualVM 的监视标签内,我们可以查看当前应用程序中所有活动线程和守护线程的数量等实时信息。

活跃线程情况:

java 射击游戏 java经典射击游戏大全_游戏开发_15

线程时间线视图:

java 射击游戏 java经典射击游戏大全_java 射击游戏_16

小结:在对系统进行测试的过程中,发现了不少问题和缺陷,之后及时对其进行了修正。总体上,本次系统的设计与开发达到了预期目标,基本实现了系统设计时的各项需求,完成后的系统其性能也很安全稳定。并且很好的将软件工程的理论和实验内容应用到本项目中。