JAVA小游戏之飞机大战(超详细)

前言:在一定的java基础上就可以进行飞机大战小游戏的编写了。整个小游戏主要涉及到的基础知识为:类与对象,鼠标事件监听,线程、重绘等。

整体思路框架

设计一个初始界面,在开始后出现自己的战机和敌机且自己的战机不断地发射子弹。当子弹碰到敌机后加分,若自己碰到敌机则游戏结束。

需要的类
1.入口类 2.自己的战机类 3.敌机类 4.子弹类 5.背景类 (6.抽象类 7.数据类)
在设计类的时候可以注意到,战机、敌机和子弹都属于飞行物体,它们都拥有几个相同的属性和方法。例如XY坐标,长度、宽度、速度,移动方法,绘制方法等。因此可以建立一个抽象的飞行类作为父类,战机类、敌机类、子弹类继承父类的属性和方法,并重写其中的抽象方法,为我们的代码编写提供方便。

一、编写入口类(初始化界面)

首先,建立一个入口类(MainFrame)继承JPanel.实例化一个窗体,并设置窗体的各种属性。

public static void main(String[] args) {
		// TODO Auto-generated method stub
		JFrame jf = new JFrame();
		MainFrame mf = new MainFrame();			//入口类名为MainFrame
		jf.add(mf);
		
		jf.setTitle("飞机大战");
		jf.setDefaultCloseOperation(3);
		jf.setSize(700,1000);
		jf.setLocationRelativeTo(null);
	
		
		jf.setVisible(true);
		mf.iniUI();
	}

这时候,我们已经拥有了一个界面。其中,iniUI是一个初始化的方法,其中包括了监听和线程两个内容。当然,可以将监听和线程分开来写成监听类和线程类,但因为这个小游戏中用到的内容不多便写在了一起,避免传参出现不便和错误。

二、背景类(天空类)

根据思路,在初始化了界面之后我们需要一个背景类。即在界面中绘制一张背景图,通过图像坐标的改变,实现背景不断的移动,造成战机不断前进的状态景象。可以想象,只有一张图片很难在移动后实现循环的绘制,因此采用两张图片。当一张图片移出画面后另一张接上,同时改变前一张图片的坐标。如此形成循环往复。

//读取图片
	static {
		try {
			bgimg1 = ImageIO.read(Sky.class.getResourceAsStream("/picture/背景.jpg"));//相对位置
			bgimg2 = ImageIO.read(Sky.class.getResourceAsStream("/picture/背景.jpg"));
		}catch(IOException e){
			e.printStackTrace();
		}
	}
	
	//构造方法
	public Sky(){
		height = 1000;
		speed =1;
		y1 = 0;
		y2 = -height;		//从画面外开始画
	}
	
	//绘制背景图
	public void paint(Graphics g) {
		g.drawImage(bgimg1, 0, y1, null);
		g.drawImage(bgimg2, 0, y2, null);
	}
	
	//背景图的移动
	public void move() {
		y1+=speed;
		y2+=speed;
		if(y1>=height) {
			y1=-height+1;//此处的1为微小的调整,可根据实际情况修改
		}else if(y2>=height) {
			y2=-height+1;
		}
	}

在写完这个天空类之后,将其在入口类中实例化后调用它的move方法,就可以看到滚动的天空背景图片了。

三、抽象飞行体类

根据思路,总结出战机、敌机和子弹类都具有的几个共同的属性和方法为:

  • X和Y的坐标
  • 图片的宽度和高度
  • 绘画方法(根据需要重写)
  • 移动方法
  • 是否碰撞方法
  • 获取图片的方法(抽象方法需要重写)
  • 飞行体是否死亡和设置的方法
  • 飞行体是否出界的方法

注意抽象类的声明关键字为abstract

	public int x;
	public int y;//横纵坐标
	
	public int width;
	public int height;//宽度高度
	//绘画方法
public void paint(Graphics g) {
		g.drawImage(getImage(), x, y, null);
}
	//判断碰撞方法
public boolean isHit(Flyobject fly) {
		int x = obj.x-this.x;
		int y = obj.y-this.y;
		int r = 100;//判断半径
		return (x*x+y*y<r*r);
		//判断碰撞的方法可以自己定义 可以设置更为宽松或严格的标准
}
	//是否死亡
public boolean isdead() {
		return state == dead;
}
	
	//设置为死亡状态
public void setdead() {
		state = dead;
}
	//获取图片
	public abstract BufferedImage getImage();
	
//因为各自的移动方法和判断出界有较大不同 写在各自的类里了 当然也可以在这里定义
四、战机类、敌机类、子弹类

战机类、敌机类和子弹类都是飞行体这一抽象类的子类。因此继承飞行体这一父类之后可以直接使用里面定义好的属性值,重写里面的方法即可。

战机类

因为子弹的初始位置在战机的机头位置,因此将创建子弹的方法写在战机类里。实例化战机后调用该方法创建子弹。
继承飞行体抽象类。

private static BufferedImage heroimage;
	static {
		try {
			heroimage = ImageIO.read(Hero.class.getResourceAsStream("/picture/LiPlane.png"));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}//读取图片
	//构造方法
	public Hero() {
		x = 280;
		y = 800;
		width = heroimage.getWidth();
		height = heroimage.getHeight();
	}
	
	public void paint(Graphics g) {
		g.drawImage(getImage(), x, y, null);
	}
	
	public void move(int x,int y) {
		this.x = x - width/2;
		this.y = y - height/2;
		
		//此处是左右边界判断
		if(this.x >= 700-width) {
			this.x = 700-width;
		}else if(this.x <= 0){
			this.x = 0;
		}
		
		//此处是上下边界判断
		if(this.y >= 1000-height) {
			this.y = 1000-height;
		}else if(this.y<=0){
			this.y = 0;
		}
	}
	
	//创建子弹方法
	public Bullet creatBullet() {
		Bullet b = new Bullet(this.x+this.width/2-2,this.y-2);
		//调用Bullet构造方法 具体可见下文中bullet类
		return b;
	}

	@Override
	public BufferedImage getImage() {
		return heroimage;
	}
敌机类

在创建敌机类之前应该想到,敌机可以用数组或者用ArrayList来存储,且敌机出现的位置应该是纵坐标一致,横坐标随机。
继承飞行体抽象类。

private static BufferedImage enemyimage;
	static {
		try {
			enemyimage = ImageIO.read(Enemy.class.getResourceAsStream("/picture/敌机1.png"));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}//创建图片
	
	private int enemyspeed;		//飞行速度
	
	//构造方法
	public Enemy() {
		width = enemyimage.getWidth();
		height = enemyimage.getHeight();
		Random rand = new Random();
		x = (int)(rand.nextInt(700-width));		//随机产生横坐标
		y = -height;
		enemyspeed = 2;							//给定飞行速度为2
	}
	
	public void move() {
		y+=enemyspeed;
	}
	//是否出界
	public boolean outofbound() {
		return y > 1000;
	}

	@Override
	public BufferedImage getImage() {
		return enemyimage;
	}
子弹类

子弹类的创建与敌机类是大同小异的,同样需要数组或ArrayList来存储子弹对象。
继承飞行体抽象类。

private static BufferedImage bulletImage;
	
	private int bulletspeed;		//子弹速度
	
	//构造方法
	public Bullet(int x,int y) {
		x=x;
		y=y;
		height=bulletImage.getHeight();
		width=bulletImage.getWidth();
		bulletspeed = 3;
	}
	
	static {
		try {
			bulletImage = ImageIO.read(Bullet.class.getResourceAsStream("/picture/zd.png"));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void paint(Graphics g) {
		g.drawImage(getImage(), x, y, null);
	}
	
	public void move() {
		this.y-=bulletspeed;
	}
	
	public boolean outofbound() {
		return this.y <height;
	}

	public BufferedImage getImage() {
		return bulletImage;
	}
五、回到入口类中

准备工作已经完成,现在可以回到入口类中完成整个代码了。梳理整个过程我们应该可以得知,我们还应该解决至少以下几个疑问:

  1. 如何创建子弹和敌机?
  2. 如何绘制战机、敌机和子弹?
  3. 怎样调用它们的移动方法?
  4. 在子弹碰撞敌机后,如何将它们俩从各自的ArrayList中去除?
  5. 如何让它们周而复始地自己完成运动?
  6. 得分机制如何?

因此我们需要以下方法来解决这些问题,并通过这些方法的调用,最终完成我们的飞机大战小游戏。

———————————————————————————————————
实例化sky 和 hero(战机)
创建子弹和敌机的动态数组
定义得分 和 子弹与敌机的计数器
定义战机的状态

private Hero hero = new Hero();
private Sky sky = new Sky();

public ArrayList<Bullet> bullets = new ArrayList<Bullet>();
public ArrayList<Enemy> enemies = new ArrayList<Enemy>();

//飞机的得分
private int score = 0;

//子弹和敌机的计数器,初始化设定为0
private int bulletCount = 0;
private int enemiesCount = 0;

//定义战机的状态 开始为0运行为1死亡为2 STATE为当前状态
public static int BEGIN = 0;
public static int RUNNING = 1;
public static int OVER = 2;
public static int STATE = 0;

在这里将移动的方法都写到一个方法里,通过sky对象调用sky类里的move方法。将动态数组中的每个子弹和敌机都取出来,分别调用它们各自的移动方法。

//移动方法
	public void moveAction() {
		//背景移动
		sky.move();
		//发射子弹
		for(int i=0;i<bullets.size();i++) {
			bullets.get(i).move();
		}
		//敌机飞行
		for(int i=0;i<enemies.size();i++) {
			enemies.get(i).move();
		}
	}

通过已经实例化的对象调用各种绘画方法

public void paint(Graphics g) {
		super.paint(g);
		sky.paint(g);
		//开始状态时绘制背景图(logoImage)
		if(STATE == 0) {
			g.drawImage(logoImage, 0, 0, null);
		}
		//绘制己方飞机
		hero.paint(g);
		//绘画得分
		if(STATE!=0) {
			paintScore(g);
		}
		for(int i= 0 ;i< bullets.size();i++) {
			bullets.get(i).paint(g);
		}
		for(int i= 0;i<enemies.size();i++) {
			enemies.get(i).paint(g);
		}
		//overImage为结束时候的画面 即如果战机状态为死亡则绘制结束画面
		if(STATE==2) {
			g.drawImage(overImage, 6, 400, null);
		}
	}
	
	//绘制分数的方法
	public void paintScore(Graphics g) {
		Font font = new Font(Font.SANS_SERIF,Font.BOLD,30);	//设置字体
		g.setColor(Color.RED);	//设置颜色
		g.setFont(font);
		g.drawString("SCORE:"+score, 15, 45);		//绘画文字
	}

创建和移除子弹的方法
因为创建和移除敌机的方法几乎完全相同,因此不再赘述

public void createBullet() {
		bulletCount++;
		//此处的50即为控制子弹生成的速率的参数
		if(bulletCount % 50==0) {
			Bullet b = hero.creatBullet();
			bullets.add(b);		//在动态数组中加入新的子弹
			bulletCount=0;
		}
	}
	
	public void removeBullet() {
		for(int i=0;i<bullets.size();i++) {
			if(bullets.get(i).outofbound()==true || bullets.get(i).isdead()==true) {
				bullets.remove(i);
			}
		}
		//如果子弹超过边界或者子弹已经判定为死亡状态则移除子弹
	}

处理碰撞情况的方法(自己和敌机碰撞、敌机和子弹碰撞)

//子弹和敌机碰撞
public void bulletHitEnemy() {
		for(int i=0;i<bullets.size();i++) {
			Bullet bullet = bullets.get(i);
			for(int j = 0;j<enemies.size();j++) {
				Flyobject enemy = enemies.get(j);
				if(bullet.isHit(enemy)) {
					//设置子弹和敌机的状态为死亡
					bullet.setdead();
					enemy.setdead();
					//加分
					score += 2;
				}
			}
		}
	}
	//自己和敌机碰撞
	public void herohitenemy() {
		for(int i=0;i<enemies.size();i++) {
			Flyobject enemy = enemies.get(i);
			if(hero.isHit(enemy)) {
				//设置状态为死亡	
				enemy.setdead();
				hero.setdead();
				STATE = OVER;
			}
		}
	}

最后的准备工作也都已经完成了,现在只需要增加界面的监听并在线程中调用它们即可,在文章最前提到的iniUI 方法将实现它们。

public void iniUI() {
		
		MouseAdapter adapter = new MouseAdapter() {
			public void mouseClicked(MouseEvent e) {
				if(STATE==BEGIN) {
					STATE = RUNNING;
				}else if(STATE==OVER) {
					STATE = BEGIN;
					score = 0;
					//重新初始化自己的飞机
					hero = new Hero();
					//重新初始化子弹数组
					bullets = new ArrayList<Bullet>();
					//重新初始化敌机数组
					enemies = new ArrayList<Enemy>();
				}
			}
			
			public void mouseMoved(MouseEvent e) {
				if(STATE==RUNNING) {
					 int x1 = e.getX();
					 int y1 = e.getY();
					 hero.move(x1,y1);
				}
			}
		};
		
		this.addMouseListener(adapter);
		this.addMouseMotionListener(adapter);
		
		//定义游戏的定时器
		//所要执行的任务 延迟时间(毫秒) 执行各后续任务的时间间隔(毫秒)
		Timer timer = new Timer();
		timer.schedule(new TimerTask(){
			public void run() {
				if(STATE == RUNNING) {
					moveAction();
					createBullet();
					removeBullet();
					createEnemy();
					removeEnemy();
					
					//子弹和敌机碰撞的方法
					bulletHitEnemy();
					//自己和敌机碰撞的方法
					herohitenemy();
				}
				repaint();
			}
		},10,10);
		
	}

此处有两点需要特别提示的地方。一是没有新建监听类进行监听。adapter是适配器的意思,也就是使用了MouseAdapter类了以后只需要重写自己需要的监听方法即可,不需要将所有的方法都进行重写。二是没有新建线程类,而是使用了Timer类。其实,Timer类实现了Runnable的接口,使用时相当于一个线程。schedule的具体意义在代码段里的注释中已经注明。
此时,已经完成了所有的编写。当然还有很多值得改进的地方,可以将一些常用的方法和数据都写到各自的类里,使入口类的书写更为清晰简洁。

改进思路
  1. 增加敌机被击中后的爆炸动画效果
  2. 抽象类实例化不同的敌机类,得分也设置为不同
  3. 设置奖励系统
  4. 设置生命系统和等级闯关系统,达到一定分数后增加难度
  5. 提供界面使玩家自选难度

如有问题,希望各位大佬指正!

发布了14 篇原创文章 · 获赞 11 · 访问量 2539

猜你喜欢

转载自blog.csdn.net/qq_43425914/article/details/88930716