Java第十一课——多线程实现飞机大战

Java第十一课——多线程实现飞机大战

一、补充讲解一下线程

在第九课的基础上补充两点:
1、启动线程使用start()方法而不直接使用rin()方法,因为线程是一直重复执行的,调用run方法只会执行一次,所以用start方法启动线程
2、当小球跑的很远,跑出窗体时,便可以把小球移出去,用remove()方法

list.remove(ball)

可以给小球加入一个getX()方法获取小球的x坐标,当x大于窗体长度时移除。因为小球是放在队列里面的,所以队列里可能有很多小球,那么可以用遍历的方式获取到要移除的小球在队列里的位置。值得一提的是:先放进去的小球在队列的前面,就像排队一样

for (int i = listmybullet.size() - 1; i > 0; i--){
	Ball b = list.get(i);
	if (b.getX() > frame.getWidth()){
		list.remove(i);
	}
}

二、飞机大战的实现思路

在完成飞机大战之前先明确要什么组件?或者说飞机大战有什么对象:需要有一个背景,自己的飞机,敌机,子弹,道具,Boss等等。还可能需要哪些方法?移动的方法,绘制图片的方法,获取坐标的方法(用于判断碰撞),碰撞了怎么处理的方法…
不难发现,几乎所有的对象都要用到以上的方法,于是可以写一个接口,包含以上所有方法,让所有组件的类连到这个接口就可以了
然后就确定哪些可以利用线程来更方便的实现。举个例子,我机只有一架,那实现的时候就只要创建一个对象,写他的移动方法和绘制方法就好了。但我机的子弹,假设是每过1秒自动发射1颗子弹,那么就可以利用第九节课讲的:利用队列实现,每过一秒给队列放进一个小球,最后启动。
另外,之前讲过的Timer和TimerTask可以用来实现一些比较简单的线程。举个例子:敌军飞机出现的数量会受到Boss的影响,Boss出现后肯定要减少敌机出现的数量。而道具,道具的出现与场上的情况没有什么联系,而且道具也是可以重复出现的。那么道具就可以用Timer来实现

三、飞机大战的具体实现

1、先创建一个接口:Entity,并在里面写所需要的方法

public interface Entity {
	public void move();

	public void draw(Graphics g);

	public int getX();

	public void getY();

	public void hit(int i);
}

这里的hit方法里的形参 i 是用于移除列队的。当子弹撞击后,获取到这个子弹在队列里的位置,然后用remove方法,敌机也是同理。
2、窗体
移动飞机的方式一般用键盘,所以我在这里我就和监听器就放在一起写了

public class Frame implements KeyListener {
	public void UI(){
		JFrame frame=new JFrame();
		frame.setSize(500, 1000);
		
		frame.setVisible(true);
		Graphics g=frame.getGraphics();
		
		frame.addKeyListener(this);
	}
	
	public static void main(String[] args) {
		new Frame().UI();
	}
	
	public void keyPressed(KeyEvent e) {
		int code = e.getKeyCode();
		//获取到按下的按钮后传给我机的move()
	}
	
	public void keyTyped(KeyEvent e) {}
	
	public void keyReleased(KeyEvent e) {}
}

这个窗体只是一个小框架,还缺了很多东西(启动线程),等创建好每个类后再回来修改
在窗体上有个很重要的东西:缓存。有了缓存画布之后就不会卡顿了,不然对象太多会非常卡
在UI()方法里:

public void UI(){
	...
	frame.addKeyListener(this);

	//创建缓存图片对象
	BufferedImage bufferImage = new BufferedImage(frame.getWidth(), 2 * frame.getHeight(), BufferedImage.TYPE_INT_RGB);

	//从缓存图片获取画布对象
	Graphics bufferedG=bufferImage.getGraphics();

	//在所有元素没有画完前,不显示
	//这里要把所有对象都找出来,调用他们的draw方法,同时move方法也可以在这里一起调用
	
	//把缓存图片显示出来
	g.drawImage(bufferImage, 0, -1000, null);
	try {
		Thread.sleep(20);
	} catch (InterruptedException e) {
	}
}

这段代码也缺失了一部分(找出所有对象调用draw(Graphics g))在最后会补全
另外,可以看到,缓存图片的高度是窗体的两倍高,原因是可以使背景和敌机从最顶上出现,也就是使敌机和背景可以无缝的进入窗体中而不是突然出现,同时他们出现的位置也得是负的,用move方法使他们"滑"进来,所以显示图片时从-1000开始画
3、背景
背景就需要一张图片,这里有飞机大战的素材库:
http://www.aigei.com/view/71627.html?order=down
在里面找一张背景图,可以根据背景来修改窗体参数,把图片复制进环境,可以用Image来获取复制进来的图片
新建一个背景类,并实现Entity接口。接口的move和draw方法可以帮助我们实现背景的动态效果。
draw方法里,只要当图片的坐标大于图片的高度时,把坐标清零就可以了,换句话说就是当图片画出窗体时重新在原点绘制一张。
move方法可以定义背景的移动速度,也可以根据场景来定义速度。另外因为飞机是从下往上飞的,但实际上,我们不控制飞机时,飞机在窗体上是不动的,动的是背景,所以背景是向下动的,那就要使 y+ > 0。

public class Background implements Entity {
	Image image = new ImageIcon("planebg.png").getImage();
	int x,y;
	public void move() {
		y += 2;
	}

	public void draw(Graphics g) {
		if (y - image.getHeight(null) >= 0) {
			y = 0;
		}
		g.drawImage(image, x, y, null);
		g.drawImage(image, x, y + image.getHeight(null), null);
	}
	
	public int getX() {
		return x;
	}

	public int getY() {
		return y;
	}

	public void hit(int i) {}
}

背景的坐标是相对缓存画布而定的,创建全局变量之后被初始化为0,这个0相对Frame是-1000,所以两个drawImage方法相互配合,初始化时一张图片在窗体上,另一张图片在窗体外,两张图片一起向下动,下面的图片画完了就会跑到上面重现画,上面的图片向下填补
背景类写完之后,就可以在Frame的把背景画进缓存

public class Frame implements KeyListener {
	Background bg=new Background();
	public void UI(){
		...
		while(true){
			...
			//在所有元素没有画完前,不显示
			bg.draw(bufferedG);
			bg.move();
			...
		}
	}
}

要注意的是:背景的对象不能在缓存线程(while(true))里面创建,因为背景只有一个,不能在线程里一直创建
4、我机
和背景一样实现Entity接口。
在素材库里找一张我机的图片,但这里和背景不同的是,我机是要控制大小的,不可能我机和背景一样大,可以通过这个方法来控制大小:
drawImage(Image img, int x, int y, int width, int height, ImageObserver observer)
如:把大小控制在90×90

g.drawImage(image, x, y, 90, 90, null);

我机也是要控制出现位置的:比如初始化时想让我机出现在正中间,就要算好我机的尺寸来调整
draw方法只要把图片绘制上去就可以了。当然会有个问题,飞机可以飞出窗体,那就可以写if语句来控制(下面的代码没有限制飞机能否飞出去)
move方法就要结合监听器来写,在Frame方法里获取到了code来控制我机移动,但要注意的是,move方法里不能带形参(Entity接口里move方法没有形参),所以code不能通过move方法的形参来传递。那可以另外写一个带形参的move方法,或是直接传值(下面的方法是直接赋值的)

public class Myplane implements Entity{
	int x=250;
	int y=1850;
	int speed=10;
	int code;//用于KeyListener的检测
	Image image=new ImageIcon("myplane.png").getImage();
	public void move() {
		if(code==KeyEvent.VK_UP){
			y -=speed;
		}
		if(code==KeyEvent.VK_DOWN){
			y+=speed;
		}
		if(code==KeyEvent.VK_LEFT){
			x-=speed;
		}
		if(code==KeyEvent.VK_RIGHT){
			x+=speed;
		}
	}
	
	public void draw(Graphics g) {
		g.drawImage(image, x-44, y, null);
	}
	
	public int getX() {
		return x;
	}
	
	public int getY() {
		return y;
	}
	
	public void hit(int i){}
}

这里要注意初始y坐标是大于窗体的高度的,因为这里的坐标是相对缓存画布而定的,而缓存画布是两倍的窗体高度。另外x,y坐标和g.drawImage(image, x-44, y, null)里面的值都是根据自己的飞机大小定的。hit方法在后面来补充
在窗体里:

public class Frame implements KeyListener {
	Background bg=new Background();
	Myplane mp=new Myplane();
	public void UI(){
		...
		while(true){
			...
			//在所有元素没有画完前,不显示
			bg.draw(bufferedG);
			bg.move();

			mp.draw(bufferedG);
			...
		}
	}
	...
	public void keyPressed(KeyEvent e) {
		int code = e.getKeyCode();
		//获取到按下的按钮后传给我机的move()
		mp.code=code;
		mp.move();
	}
	...
}

5、敌机
实现Entity接口
敌机的变化可以比较多,出现的位置,移动的方向,移动的速度,能否发射子弹等等,所以不太适合用Timer和TimerTask来用,下面的代码里我只写了一些简单的敌机条件
draw方法直接画出即可
move方法加了敌机不同的移动速度,用Random来实现随机。
另外敌机的位置坐标用构造方法来随机生成

public class Enemyplane implements Entity{
	int x;
	int y=600;
	Random rand=new Random();
	int a=rand.nextInt(2);//敌机速度判定 
	Image image=new ImageIcon("enemyplane.png").getImage();
	public Enemyplane() {
		Random randy=new Random();
		this.x=randy.nextInt(340)+80;
	}
	
	public void move() { 
		if(a==0){
			y+=2;
		}else if(a==1){
			y+=4;
		}
	}

	public void draw(Graphics g) {
		g.drawImage(image, x, y, 90, 90, null); 
	}

	public int getX() {
		return x;
	}

	public int getY() {
		return y;
	}
}

敌机和背景、我机不同的是,敌机有很多架,那需要一个队列来实现出现敌机的多线程
先在Frame里创建队列:

ArrayList<Entity> listenemy =new ArrayList<Entity>();

另外写一个类用来控制小球的移动并继承Thread重写run方法

public class EnemyplaneMove extends Thread {
	private ArrayList<Entity> listenemy;
	public EnemyplaneMove (ArrayList<Entity> listenemy) {
		this.listenemy=listenemy;
	}

	public void run(){
		while(true){
			Enemyplane ep=new Enemyplane();
			listenemy.add(ep);
		}
		
		try{
			Thread.sleep(1000);//敌机出现的间隔
		}catch(Exception e){
		}
	}
}

然后在窗体里启动:

public class Frame implements KeyListener {
	ArrayList<Entity> listenemy =new ArrayList<Entity>();

	Myplane mp=new Myplane();
	Background bg=new Background();
	public void UI(){
		...
		Graphics g=frame.getGraphics();
		
		EnemyplaneMove em=new EnemyplaneMove(listenemy);
		em.start();

		while(true){
			...
			//在所有元素没有画完前,不显示
			bg.draw(bufferedG);
			bg.move();
			//找到所有的敌机
			for(int i=0;i<listenemy.size();i++){
				Entity entity=listenemy.get(i);   
				entity.draw(bufferedG);
				entity.move();
			}
			
			mp.draw(bufferedG);
			...
		}
	}
}

6、子弹
子弹可以有一些变化:比如不同颜色子弹的威力不同,飞行方向不同,飞行速度不同等等。而子弹在不同的对象上的表现也不同,比如我机的子弹可以有较多的变化:多方向,连发,而敌机的子弹变化肯定会比我机少,而Boss的子弹也会有很多变化。所以子弹类可以只有一个,用来专门处理子弹的不同,子弹的移动就需要根据不同对象来写多个不同的移动方法。
子弹类:实现Entity接口
draw方法和move方法都很简单,毕竟子弹类只管子弹本身的特性,不管是谁发出的。换句话说,子弹的飞行轨迹,威力,大小,颜色都是子弹自己的性质,从哪里飞出去,什么时候可以发射子弹,发射多少颗子弹,向哪个方向发射子弹就是子弹移动类管的事了

public class Bullet implements Entity{
	int m;//0表示子弹向上飞,1表示子弹向左上飞,2表示子弹向右上飞
	int x;
	int y;
	Image image=new ImageIcon("bullet1.png").getImage();
	public void move() {
		if(m==0){
			y-=9;
		}else if(m==1){
			y-=7;
			x-=2
		}else if(m==2){
			y-=7;
			x+=2;
		}
	}
	
	public void draw(Graphics g) {
		g.drawImage(image, x, y, null);  
	}
	
	public int getX() {  
		return x;
	}

	public int getY() {  
		return y;
	}

	public void hit(int i){}
}

然后在Frame类创建子弹的队列

ArrayList<Entity> listmybullet =new ArrayList<Entity>();

创建控制我机子弹移动的类,继承Thread。
我机的子弹是从我机身上发出的,那么就要获取到我机的坐标,再根据子弹的大小调整子弹发射时的位置。可以利用构造方法来获取我机的对象

public class MyBulletMove extends Thread{
	private Myplane mp;
	private ArrayList<Entity> listmybullet;
	public MyBulletMove (ArrayList<Entity> listmybullet, Myplane mp){
		this.listmybullet=listmybullet;
		this.mp=mp;
 	}
 	public void run(){
		while(true){
			Bullet upbullet=new Bullet();
			upbullet.m=0;//向上飞
			upbullet.x=mp.x-11-mp.a;
			upbullet.y=mp.y;

			listmybullet.add(upbullet);

			try{
				Thread.sleep(400);//子弹发射的间隔
			}catch(Exception e){
			}
		}
	}
}

然后就是在窗体里启动,并在缓存里获取所有对象

public class Frame implements KeyListener {	
	ArrayList<Entity> listenemy =new ArrayList<Entity>();
	ArrayList<Entity> listmybullet =new ArrayList<Entity>();

	Myplane mp=new Myplane();
	Background bg=new Background();
	public void UI(){
		...
		Graphics g=frame.getGraphics();

		EnemyplaneMove em=new EnemyplaneMove(listenemy);
		em.start();

		MyBulletMove bm=new MyBulletMove(listmybullet, mp);
		bm.start();
		
		while(true){
			...
			//在所有元素没有画完前,不显示
			bg.draw(bufferedG);
			bg.move();
			//找到所有的敌机
			for(int i=0;i<listenemy.size();i++){
				Entity entity=listenemy.get(i);
				   
				entity.draw(bufferedG);
				entity.move();
			}
			//找到所有我机子弹
   			for(int i=0;i<listmybullet.size();i++){
				Entity entity=listmybullet.get(i);
				
				entity.draw(bufferedG);
				entity.move();
			}
			
			mp.draw(bufferedG);
			...
		}
	}
}

7、碰撞判断
碰撞判断也是一个线程,每过多少秒就检测碰撞,这个类需要的对象有我机,所有子弹,所有敌机,通过构造方法获取我机,两个队列。

public class Checkforcrash extends Thread{
	private ArrayList<Entity> listmybullet;
	private ArrayList<Entity> listenemy;
	private Myplane mp;
	public Checkforcrash(ArrayList<Entity> listenemy,ArrayList<Entity> listmybullet, Myplane mp) {
		this.listenemy=listenemy;
		this.listmybullet=listmybullet;
		this.mp=mp;
	}
	
	public void run() {
		while (true) {
			for (int i = listmybullet.size() - 1; i > 0; i--) {
				Entity temA = listmybullet.get(i);
				for (int j = 0; j < listenemy.size(); j++) {
					Entity temB = listenemy.get(j);
					if (Tools.enemymycrash(temB, mp)) {
						temA.hit(j);
						mp.hit(j);
					}
					if (Tools.bulletenemycrash(temA, temB)) {
						temA.hit(i);
						temB.hit(j);
					}
				}
			}
		}
	}
 }

这里有另外一个类Tools来辅助判断,因为判断条件包括x和y的坐标差的绝对值,比较长,直接放进去可读性比较低,另外,Tools类可以与当前类配合实现道具的拾取。
在判断绝对值时,先要根据对象的图片大小来确定图片中心点,然后再确定二者相差多少时为撞上了

public class Tools {
	// 敌机我机碰撞
	public static boolean enemymycrash(Entity e, Myplane mp) {
		if (Math.abs((e.getX() + 45) - (mp.getX() - 1)) < 40 && Math.abs((e.getY() + 45) - (mp.getY() + 55)) < 40) {
			return true;
		} else {
			return false;
		}
	}
	
	// 敌机子弹相撞
	public static boolean bulletenemycrash(Entity ea, Entity eb) {
		if ((ea instanceof Enemyplane) && (eb instanceof Bullet)) {
			if (Math.abs((ea.getX() + 45) - (eb.getX() + 10)) < 20 && Math.abs((ea.getY() + 45) - eb.getY()) < 20) {
				return true;
			}
		} else if ((ea instanceof Bullet) && (eb instanceof Enemyplane)) {
			if (Math.abs((ea.getX() + 10) - (eb.getX() + 45)) < 20 && Math.abs(ea.getY() - (eb.getY() + 45)) < 20) {
				return true;
			}
		}
		return false;
	}
}

A instanceof B 用于判断A是否是B类的对象
最后便是实现各自的hit方法:
(1)子弹:

public class Bullet implements Entity{
	...
	ArrayList<Entity> list;
	...
	public void hit(int i){
		//移除队列
		list.remove(i);
	}
}

(2)子弹移动:

public class MyBulletMove extends Thread{
	...
	public void run(){
		while(true){
			Bullet upbullet=new Bullet();
			...   
			upbullet.list=listmybullet;
			listmybullet.add(upbullet);
			...
		}
	}
}

upbullet.list=listmybullet;也可以用构造方法
(3)敌机

public class Enemyplane implements Entity{
	...
	ArrayList<Entity> list;
	
	public void hit(int i) {
		list.remove(i);
	}
}

(4)敌机移动

public class EnemyplaneMove extends Thread {
	...
	public void run(){
		while(true){
			Enemyplane ep=new Enemyplane();
			listenemy.add(ep);
			ep.list=listenemy;
		...
		}
	}
}

(5)我机
可以找一张飞机残骸的图片,把image换掉

public class Myplane implements Entity{
	...
	public void hit(int i) {
		image = new ImageIcon("brokenplane.png").getImage();
	}
}

到这里飞机大战的雏形就结束了!道具的Boss同上类似,可以自己添加
PS:文章篇幅较长,可能会有一些错误,望指正(抱拳)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值