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:文章篇幅较长,可能会有一些错误,望指正(抱拳)