java项目笔记 - 第18章:坦克大战2.2

在坦克大战2.2版本中,增加了我方坦克发射多颗子弹、敌方坦克移动发射以及我方坦克被击中爆炸的效果。我方坦克现在能连续发射子弹,限制最多5颗子弹同时存在。敌方坦克在子弹消亡后可再次发射,最多可同时发射5颗子弹。当敌方坦克击中我方坦克时,坦克会消失并显示爆炸效果。游戏通过多线程实现了子弹和坦克的动态行为,同时进行了碰撞检测,确保了游戏的流畅性和互动性。
摘要由CSDN通过智能技术生成

总体内容(增加的功能)

在这里插入图片描述


1. 我方坦克发射一颗子弹

需求:我方坦克在发射的子弹消亡后,才能继续发射新子弹

思路

在按下J键时,判断我方坦克对象的子弹,是否已经消亡。
若已经消亡,才调用shootEnemyTank():这个方法的作用是创建+启动子弹线程

实现

Mypanel类:
在MyPanel类的keyPress()中修改,按下J键时的处理方式

//按下J键时,我方坦克发射子弹
if (e.getKeyCode() == KeyEvent.VK_J) {
    //- 出现的问题:
    //-- 按一下J,只会调用一次keyPressed(),那面板只会重绘一次
    //-- 因此按一下J,只能看到一个不会动小球
    //-- 解决方法:让面板每隔100ms,自动重绘=>用多线程(面板实现Runnable接口)
    
    //- 实现:当子弹消亡时,才会发射新子弹
    //-- 方法:当子弹线程为空 或 子弹消亡时,调用shootEnemyTank()
    if (myTank.shoot==null||!(myTank.shoot.isLive)) {
        myTank.shootEnemyTank();
    }
}

2. 我方坦克发射多颗子弹

需求:我方坦克可以发射多颗子弹(在面板上最多5颗)

思路

  1. 在MyTank类中增加一个子弹集合:子弹集合存放我方坦克发射的多发子弹。每按一下J都会调用shootEnemyTank()来创建一个子弹线程加入子弹集合,并启动它
  2. 画出我方坦克发射的多发子弹:点击J,创建启动子弹线程后,要画出这颗子弹,那就在paint()中循环遍历shoots集合画出,如果存在就画出,不存在就从集合中移除
  3. 判断每颗子弹是否击中:循环遍历shoots集合中的每颗子弹,和每个敌方坦克做判断。=>两次遍历

实现

MyTank类和MyPanel类

  1. 在MyTank类中:①增加一个属性:shoots集合(存放多颗子弹) ②shootEnemyTank()中,把新创建的子弹 加入集合 ③shootEnemyTank()中,控制只能有5颗子弹
  2. 在MyPanel类中:①画子弹:循环子弹集合画出存活的子弹,不存活的子弹被移除 ②击中坦克:遍历子弹集合,遍历敌方坦克集合,判断每颗子弹是否击中敌方的某个坦克
package com.wpz.tankgame2_2;

import java.util.Vector;

/**
 * @author 王胖子
 * @version 1.0
 * 我的坦克
 * 1. 动机描述:用户按下J键,就发射子弹开始射击
 * 2. 为什么要把创建线程写到MyTank类中:因为射击是MyTank专有的,敌人的坦克没有,
 * 所以就在这个类中,①定义子弹发射的线程 ②创建线程 ③为线程中子弹的x,y,direction赋值 ④启动线程
 */
public class MyTank extends Tank {
//    Shoot shoot;//子弹发射的线程=>一颗子弹
    Vector<Shoot> shoots = new Vector<>();//存放多颗子弹的集合

    public MyTank(int x, int y) {
        super(x, y);
    }

    public void shootEnemyTank() {//射击
        //控制只能有5颗子弹=>5颗时不再创建子弹线程并退出该方法
        if (shoots.size() == 5) {
            return;
        }
        //创建线程:根据坦克的位置和方向 来创建 子弹射击的线程
        Shoot shoot = null;
        // - 判断坦克的方向,来创建线程
        if (getDirection() == 0) {//上
            shoot = new Shoot(getX() + 20, getY(), 0);
        } else if (getDirection() == 1) {//右
            shoot = new Shoot(getX() + 60, getY() + 20, 1);
        } else if (getDirection() == 2) {//下
            shoot = new Shoot(getX() + 20, getY() + 60, 2);
        } else if (getDirection() == 3) {//左
            shoot = new Shoot(getX(), getY() + 20, 3);
        }
        shoots.add(shoot);//- 把子弹加入集合
        //- 启动线程
        shoot.start();
    }
}

MyPanel中paint() 画子弹的代码修改

//画我方坦克的子弹=>如果子弹线程非空且子弹存在
//        if (myTank.shoot != null && myTank.shoot.isLive) {//单颗子弹
//            g.setColor(Color.white);
//            g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);
//        }
for (int i = 0; i < myTank.shoots.size(); i++) {//多颗子弹
    //- 取出
    Shoot shoot = myTank.shoots.get(i);
    //- 如果存在就画出子弹,不存在就从集合中移除
    if (shoot.isLive) {
        g.setColor(Color.white);
        g.fillOval(shoot.x, shoot.y, 2, 2);
    } else {
        myTank.shoots.remove(shoot);
    }
}

MyPanel中 击中敌人坦克的代码修改
①新建一个方法hitEnemyTank()

public void hitEnemyTank() {//我方坦克发射多颗子弹:遍历每颗子弹判断是否击中
   for (int i = 0; i < myTank.shoots.size(); i++) {
       //取出子弹
       Shoot shoot = myTank.shoots.get(i);
       //判断该颗子弹是否击中坦克=>遍历坦克
       for (int j = 0; j < enemyTanks.size(); j++) {
           EnemyTank enemyTank = enemyTanks.get(j);
           hitTank(shoot, enemyTank);
       }
   }
}

②在run()中调用该方法

//在run中写
hitEnemyTank();//多颗子弹

③按下J键时,不用判断子弹是否存活才发射=>KeyPress()中

//按下J键时,我方坦克发射子弹
if (e.getKeyCode() == KeyEvent.VK_J) {
    //- 出现的问题:
    //-- 按一下J,只会调用一次keyPressed(),那面板只会重绘一次
    //-- 因此按一下J,只能看到一个不会动小球
    //-- 解决方法:让面板每隔100ms,自动重绘=>用多线程(面板实现Runnable接口)

    //- 实现:当子弹消亡时,才会发射新子弹
    //-- 方法:当子弹线程为空 或 子弹消亡时,调用shootEnemyTank()
//            if (myTank.shoot==null||!(myTank.shoot.isLive)) {//发射单颗(销毁了才会发第二颗)
//                myTank.shootEnemyTank();
//            }
    myTank.shootEnemyTank();//发射多颗子弹:无需判断
}

效果图
在这里插入图片描述


3. 敌方移动发射

需求:让敌人坦克发射的子弹消亡后,才可以再发射子弹
扩展:敌人坦克可以发多颗子弹

实现

EnemyTank类

  1. EnemyTank类的run()方法中:添加判断,如果该敌人坦克仍存在且子弹集合为空,就创建+启动一个子弹并加入子弹集合
  2. 扩展:发射多颗子弹=>在run方法中判断,敌人坦克仍存在且子弹集合中的子弹个数小于5,就继续创建+启动子弹并加入子弹集合
  3. 想法:一开始想到在画子弹的地方:当子弹消失就移除,并再创建一个子弹,但是创建过程得根据坦克方向来确定子弹的位置,代码太多,且维护性差,所以就在EnemyTank类中书写了

在这里插入图片描述

package com.wpz.tankgame2_2;

import java.util.Vector;

/**
 * @author 王胖子
 * @version 1.0
 * 敌人的坦克
 * 1. 动机:让敌人坦克可以自由随机移动
 * 2. 方法:把敌人坦克类当作线程使用,使得坦克可以根据run()方法的业务代码来运行
 */
public class EnemyTank extends Tank implements Runnable {
    //千万注意要初始化这个Vector...不然会报空指针
    Vector<Shoot> shoots = new Vector();//一个敌方坦克可以发射多颗子弹,所以这里用集合
    boolean isLive = true;//敌人坦克是否存在

    public EnemyTank(int x, int y) {
        super(x, y);
        //每创建一个敌方坦克都分配一个子弹线程并启动=>所以把(创建+启动)线程 写到初始化敌人坦克里面写
        // 不能在这个构造器中创建线程,因为初始化EnemyTank时,只给x,y赋值了,没给direction赋值,
        // 而初始化子弹线程时,需要EnemyTank的方向,如果在这个类中创建线程,会报空指针异常
        //也就是下面这两句:
        //EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);
        //shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());
    }

    @Override
    public void run() {
        while (true) {
            //如果该敌人坦克仍存活着且子弹集合为空,就创建+启动一个新子弹线程并加入子弹集合
            //如果想让坦克发多颗子弹(最多5颗):if (isLive && shoots.size() < 5)=>不够5颗的话 就继续创建子弹加入子弹集合
            if (isLive && shoots.size() == 0) {
                Shoot s = null;
                switch (getDirection()) {//根据坦克方向定子弹位置 来创建子弹
                    case 0://上
                        s = new Shoot(getX() + 20, getY(), 0);
                        break;
                    case 1://右
                        s = new Shoot(getX() + 60, getY() + 20, 1);
                        break;
                    case 2://下
                        s = new Shoot(getX() + 20, getY() + 60, 2);
                        break;
                    case 3://左
                        s = new Shoot(getX(), getY() + 20, 3);
                        break;
                }
                new Thread(s).start();//启动
                shoots.add(s);//加入
            }
            /*
                写多线程时要考虑三个问题:①处理什么业务逻辑 ②什么时候被创建+启动 ③什么时候结束
                敌人坦克这个线程中:①业务:敌人坦克朝当前方向走+改变方向
                    ②结束:敌人坦克消失时,线程结束
                    ③创建+启动:敌人坦克被初始化完成后就可以使用这个线程了
             */
            //思路:先向坦克的当前方向走30步=>然后转变方向
            //- 向当前方向移动30步
            switch (getDirection()) {
                case 0://for (int i = 0; i < 30; i++) {
                        moveUp();//使用父类中移动的方法
                        try {
                            Thread.sleep(50);//休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 1://for (int i = 0; i < 30; i++) {
                        moveRight();
                        try {
                            Thread.sleep(50);//休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 2://for (int i = 0; i < 30; i++) {
                        moveDown();
                        try {
                            Thread.sleep(50);//休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 3://for (int i = 0; i < 30; i++) {
                        moveLeft();
                        try {
                            Thread.sleep(50);//休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
            }
            //- 转变方向(方向为0-3随机)
            setDirection((int) (Math.random() * 4));
            //- 注意!写并发程序,一定要考虑清楚,该线程什么时候结束
            //-- 当坦克不再存在时,这个线程就结束了
            if (!(isLive)) {
                break;//退出线程
            }
        }
    }
}


4. 我方被击中爆炸

需求:当敌人坦克击中我方坦克时,我方坦克消失,并出现爆炸效果。

想法

  1. 在MyPanel的run()中,判断敌人坦克是否击中我方坦克
  2. 所以另外写一个方法hitMyTank():方法内遍历每个敌人坦克+遍历每个敌人坦克的每颗子弹,调用hitTank()方法进行判断
  3. 但是发现一个问题:hitTank(s,myTank);出现问题,因为hitTank()的参数是EnemyTankpublic void hitTank(Shoot s, EnemyTank enemyTank),所以把EnemyTank改成他们的父类Tank
  4. 但是又有一个错误:Tank不能访问isLive,所以给Tank中增加一个isLive属性,默认为true,再把敌人坦克类中的isLive删除
  5. 画我方坦克:在我方坦克存在时画
  6. 我方坦克发射子弹:增加条件,在我方坦克存在时才能创建子弹线程
  7. 在MyPanel的run()中,调用hitMyTank()
  8. 可以把打敌人坦克的代码放到hitEnemyTank()方法中,然后在run()中调用

实现

上面的想法写的意思差不多了,这里不再阐述

在MyPanel类中:
① 增加hitMyTank()

public void hitMyTank(){
    for (int i = 0; i < enemyTanks.size(); i++) {//判断每个坦克的每颗子弹 是否击中我方坦克
        EnemyTank enemyTank = enemyTanks.get(i);//取出敌人坦克
        for (int j = 0; j < enemyTank.shoots.size(); j++) {
            Shoot s = enemyTank.shoots.get(j);//取出敌人子弹
            //如果我方坦克存在 且 该颗子弹存在 就判断子弹是否集中我方坦克
            if (myTank.isLive&&s.isLive){
                hitTank(s,myTank);
            }
        }
    }
}

②修改hitTank

public void hitTank(Shoot s, Tank tank) {
    // - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向
    switch (tank.getDirection()) {
        case 0://坦克向上
        case 2://坦克向下
            if (s.x > tank.getX() && s.x < tank.getX() + 40 &&
                    s.y > tank.getY() && s.y < tank.getY() + 60) {
                s.isLive = false;//子弹消失
                tank.isLive = false;//敌人坦克消失
                enemyTanks.remove(tank);//从集合中移除被击中的坦克
                //击中时,创建bomb对象,放入bombs集合中
                Bomb bomb = new Bomb(tank.getX(), tank.getY());
                bombs.add(bomb);
            }
        case 1://坦克向右
        case 3://坦克向左
            if (s.x > tank.getX() && s.x < tank.getX() + 60 &&
                    s.y > tank.getY() && s.y < tank.getY() + 40) {
                s.isLive = false;//子弹消失
                tank.isLive = false;//敌人坦克消失
                enemyTanks.remove(tank);//从集合中移除被击中的坦克
                //击中时,创建bomb对象,放入bombs集合中
                Bomb bomb = new Bomb(tank.getX(), tank.getY());
                bombs.add(bomb);
            }
    }
}

③在paint()方法中,画我方坦克时增加判断

 if (myTank!=null&&myTank.isLive) {
   drawTank(myTank.getX(), myTank.getY(), g, myTank.getDirection(), 0);//画出我的坦克
}

④ run()中调用

@Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //- 判断是否击中敌人坦克
            hitEnemyTank();
//            hitEnemyTank();//多颗子弹
			// - 判断敌方坦克是否击中我方坦克
            hitMyTank();
            this.repaint();
        }
    }

⑤在Keypress()的J键处理中:

 if (myTank.isLive&&myTank.shoot==null||!(myTank.shoot.isLive)) {//发射单颗(销毁了才会发第二颗)
  myTank.shootEnemyTank();
}

在Tank类中:
⑥增加属性

boolean isLive = true;

版本2.2的全部代码

Tank类

package com.wpz.tankgame2_2;

/**
 * @author 王胖子
 * @version 1.0
 * 因为该游戏会有很多坦克
 * 所以先抽象成一个父类
 */
public class Tank {
    private int x;//坦克横坐标
    private int y;//坦克纵坐标
    //将坦克的方向写到父类中 这样任意坦克在不同方法中 都可以设置移动方向
    private int direction;//坦克的方向(0:上,1:右,2:下,3:左)-->默认是0
    private int speed = 2;//移动速度
    boolean isLive = true;

    public Tank(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getDirection() {
        return direction;
    }

    public void setDirection(int direction) {
        this.direction = direction;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getSpeed() {
        return speed;
    }

    public void setSpeed(int speed) {
        this.speed = speed;
    }

    //将坦克移动时的坐标改变封装到 父类的方法中
    //直接改变坐标的x/y值(取代myTank.setY(myTank.getY()-2);)
    //先死后活:开始时坐标是加减固定值-->可以扩展成变化的--引入变量-->speed(可以在初始化坦克对象时赋值)
    //- 让坦克(我方+敌方)都在规定的范围内(窗口内)移动
    //-- 思路:先判断一下坦克当前的坐标是否超出边界,若没有超,就让坦克移动
    public void moveUp() {
        //判断坦克是否超出边界:若没超就移动
        if (getY() > 0) {
            y -= speed;
        }
    }

    public void moveRight() {
        if (getX() + 60 < 1000) {//本来是<1000的,如果坦克会超出一点边界就改成980
            x += speed;
        }
    }

    public void moveDown() {
        if (getY() + 60 < 750) {//本来是750,如果会超改为710,把窗口放大就不会超了
            y += speed;
        }
    }

    public void moveLeft() {
        if (getX() > 0) {
            x -= speed;
        }
    }
}

MyTank类

package com.wpz.tankgame2_2;

import java.util.Vector;

/**
 * @author 王胖子
 * @version 1.0
 * 我的坦克
 * 1. 动机描述:用户按下J键,就发射子弹开始射击
 * 2. 为什么要把创建线程写到MyTank类中:因为射击是MyTank专有的,敌人的坦克没有,
 * 所以就在这个类中,①定义子弹发射的线程 ②创建线程 ③为线程中子弹的x,y,direction赋值 ④启动线程
 */
public class MyTank extends Tank {
    Shoot shoot;//子弹发射的线程=>一颗子弹
//    Vector<Shoot> shoots = new Vector<>();//存放多颗子弹的集合

    public MyTank(int x, int y) {
        super(x, y);
    }

    public void shootEnemyTank() {//射击
        //控制只能有5颗子弹=>5颗时不再创建子弹线程并退出该方法
//        if (shoots.size() == 5) {
//            return;
//        }
        //创建线程:根据坦克的位置和方向 来创建 子弹射击的线程
//        Shoot shoot = null;
        // - 判断坦克的方向,来创建线程
        if (getDirection() == 0) {//上
            shoot = new Shoot(getX() + 20, getY(), 0);
        } else if (getDirection() == 1) {//右
            shoot = new Shoot(getX() + 60, getY() + 20, 1);
        } else if (getDirection() == 2) {//下
            shoot = new Shoot(getX() + 20, getY() + 60, 2);
        } else if (getDirection() == 3) {//左
            shoot = new Shoot(getX(), getY() + 20, 3);
        }
//        shoots.add(shoot);//- 把子弹加入集合
        //- 启动线程
        shoot.start();
    }
}

EnemyTank类

package com.wpz.tankgame2_2;

import java.util.Vector;

/**
 * @author 王胖子
 * @version 1.0
 * 敌人的坦克
 * 1. 动机:让敌人坦克可以自由随机移动
 * 2. 方法:把敌人坦克类当作线程使用,使得坦克可以根据run()方法的业务代码来运行
 */
public class EnemyTank extends Tank implements Runnable {
    //千万注意要初始化这个Vector...不然会报空指针
    Vector<Shoot> shoots = new Vector();//一个敌方坦克可以发射多颗子弹,所以这里用集合
//    boolean isLive = true;//敌人坦克是否存在

    public EnemyTank(int x, int y) {
        super(x, y);
        //每创建一个敌方坦克都分配一个子弹线程并启动=>所以把(创建+启动)线程 写到初始化敌人坦克里面写
        // 不能在这个构造器中创建线程,因为初始化EnemyTank时,只给x,y赋值了,没给direction赋值,
        // 而初始化子弹线程时,需要EnemyTank的方向,如果在这个类中创建线程,会报空指针异常
        //也就是下面这两句:
        //EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);
        //shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());
    }

    @Override
    public void run() {
        while (true) {
            //如果该敌人坦克仍存活着且子弹集合为空,就创建+启动一个新子弹线程并加入子弹集合
            //如果想让坦克发多颗子弹(最多5颗):if (isLive && shoots.size() < 5)=>不够5颗的话 就继续创建子弹加入子弹集合
            if (isLive && shoots.size() == 0) {
                Shoot s = null;
                switch (getDirection()) {//根据坦克方向定子弹位置 来创建子弹
                    case 0://上
                        s = new Shoot(getX() + 20, getY(), 0);
                        break;
                    case 1://右
                        s = new Shoot(getX() + 60, getY() + 20, 1);
                        break;
                    case 2://下
                        s = new Shoot(getX() + 20, getY() + 60, 2);
                        break;
                    case 3://左
                        s = new Shoot(getX(), getY() + 20, 3);
                        break;
                }
                new Thread(s).start();//启动
                shoots.add(s);//加入
            }
            /*
                写多线程时要考虑三个问题:①处理什么业务逻辑 ②什么时候被创建+启动 ③什么时候结束
                敌人坦克这个线程中:①业务:敌人坦克朝当前方向走+改变方向
                    ②结束:敌人坦克消失时,线程结束
                    ③创建+启动:敌人坦克被初始化完成后就可以使用这个线程了
             */
            //思路:先向坦克的当前方向走30步=>然后转变方向
            //- 向当前方向移动30步
            switch (getDirection()) {
                case 0://for (int i = 0; i < 30; i++) {
                        moveUp();//使用父类中移动的方法
                        try {
                            Thread.sleep(50);//休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 1://for (int i = 0; i < 30; i++) {
                        moveRight();
                        try {
                            Thread.sleep(50);//休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 2://for (int i = 0; i < 30; i++) {
                        moveDown();
                        try {
                            Thread.sleep(50);//休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 3://for (int i = 0; i < 30; i++) {
                        moveLeft();
                        try {
                            Thread.sleep(50);//休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
            }
            //- 转变方向(方向为0-3随机)
            setDirection((int) (Math.random() * 4));
            //- 注意!写并发程序,一定要考虑清楚,该线程什么时候结束
            //-- 当坦克不再存在时,这个线程就结束了
            if (!(isLive)) {
                break;//退出线程
            }
        }
    }
}

Bomb类(爆炸效果)

package com.wpz.tankgame2_2;

/**
 * @author 王胖子
 * @version 1.0
 * 实现子弹爆炸效果的类=>炸弹类(爆炸类)
 * 1. 动机:当击中坦克时,出现爆炸效果
 * 2. 解决方法的概括:在被击中的坦克 的位置上逐个画出三张爆炸效果的图
 * 3. 具体实现:写一个爆炸类:一个爆炸类可以控制三张图片的显示
 * ①控制图片位置:根据击中坦克的位置确定爆炸效果图片的位置
 * ②控制图片的出现时间:根据不同的爆炸时期,画出不同的爆炸效果=>爆炸效果总时长为9,时长分三段显示三张图片
 * ③控制画完的图片消失:如果显示时间减为0,就把这个爆炸类对象从bombs集合中移除
 * 综上该类需要的属性:①图片显示的位置xy ②控制图片逐个显示+显示的时间:life
 * ③显示时间为0时,就让图片都不再绘制且把这个类从集合中移除:isLive
 */
public class Bomb {
    int x, y;//爆炸效果图片的坐标
    int life = 9;//爆炸效果的生命周期
    boolean isLive = true;//是否存在(生命周期是否结束(<=0时结束))

    public Bomb(int x, int y) {
        this.x = x;
        this.y = y;
    }

    //减少生命值,配合出现图片的爆炸效果,
    //- 当生命值减少为0时,爆炸效果显示完毕,控制让炸弹不再显示isLive=false(在paint()中控制)
    public void lifeDown() {
        if (life > 0) {
            life--;
        } else {
            isLive = false;//
        }
    }
}

Shoot类(子弹类)

package com.wpz.tankgame2_2;

/**
 * @author 王胖子
 * @version 1.0
 * 控制子弹发射的线程
 * 1. 动机描述:如果用户按下J键,则启动该线程开始发射子弹,若子弹碰壁则结束线程
 * 2. 启动线程时得明确:①子弹在哪(根据坦克的位置) ②子弹往哪打(根据坦克的方向)
 * ③什么时候绘制子弹(根据子弹是否存在=>线程创建子弹就存在了,一直到碰壁/敌人坦克时销毁)
 * ④子弹发射的速度
 * 3. 根据以上四条,这个线程中需要的属性有:横纵坐标,子弹方向,子弹是否存在,子弹发射的速度
 */
public class Shoot extends Thread {
    int x;//子弹的横坐标
    int y;//子弹的纵坐标
    int direction;//子弹的方向
    int speed = 4;//子弹发射的速度
    boolean isLive = true;//子弹是否存在=>线程创建后子弹就存在,所以默认为true

    //构造器:需要这三个属性是因为 这几个需要根据我方坦克的x,y和direction来定
    public Shoot(int x, int y, int direction) {
        this.x = x;
        this.y = y;
        this.direction = direction;
    }

    @Override
    public void run() {
        //该线程的任务是:
        // ①控制子弹的移动(根据子弹的方向去移动)
        // ②控制子弹是否存在
        while (true) {
            try {
                Thread.sleep(50);//休眠一下,不然子弹一下子就打到墙上了
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // - 根据方向改变x,y坐标
            if (direction == 0) {//上
                y -= speed;
            } else if (direction == 1) {//右
                x += speed;
            } else if (direction == 2) {//下
                y += speed;
            } else if (direction == 3) {//左
                x -= speed;
            }
//            System.out.println("x = " + x + " y = " + y);//测试
            //- 如果碰到墙壁,则线程结束(break),子弹消失(置false,面板中不绘制子弹)
            //- 解决子弹打到坦克时,线程还未退出的问题:判断的条件增加isLive
            //把正确条件取反 就得到了它反面的条件(正确条件为:没碰到墙壁且子弹存在)
            if (!(x >= 0 && x <= 1000 && y >= 0 && y <= 750 && isLive)) {
                isLive = false;//子弹消失
                System.out.println(Thread.currentThread().getName()+"线程结束");
                break;//线程结束
            }
        }
    }
}

MyPanel类(面板类)

package com.wpz.tankgame2_2;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;

/**
 * @author 王胖子
 * @version 1.0
 * 坦克大战的绘图区域
 * 1. 在构造器中:初始化坦克(我方+敌方)
 * 2. 在paint()中:为该面板绘图
 * 3. 在keyPressed()中:对键盘按键进行处理(移动+发射子弹+重绘面板)
 */
//为了让面板不停重绘子弹,让面板实现Runnable接口,当作一个线程使用(在run()中写重绘)
public class MyPanel extends JPanel implements KeyListener, Runnable {
    MyTank myTank = null;//定义一个自己的坦克
    Vector<EnemyTank> enemyTanks = new Vector<>();//定义敌人的坦克,放到Vector中
    int enemyTankSize = 3;//敌人坦克的数量
    //定义炸弹,放到Vector中=>炸弹爆炸不属于坦克属性,属于面板的属性
    //- 当子弹击中坦克时,加入一个Bomb对象到bombs:被击中的每个坦克都有一个Bomb对象,加入集合中统一管理
    Vector<Bomb> bombs = new Vector<>();
    //定义三张炸弹爆炸的图片,用于显示炸弹爆炸效果
    Image image1 = null;
    Image image2 = null;
    Image image3 = null;

    public MyPanel() {
        myTank = new MyTank(500, 100);//初始化自己的坦克
        myTank.setSpeed(5);//设置坦克移动的速度
        //初始化敌人的坦克(注意:使用循环来添加。因为敌人坦克数量多,不要一个一个add)
        for (int i = 0; i < enemyTankSize; i++) {
            // - 创建一个敌人的坦克
            EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);
            // - 设置方向
            enemyTank.setDirection(2);
            // - 创建敌方坦克时,就为这个坦克(创建+启动)一个子弹线程,并把这个子弹线程添加到自己的子弹集合中
            Shoot shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());
            shoot.start();
            enemyTank.shoots.add(shoot);
            // - 启动敌人坦克线程,让坦克动起来
            new Thread(enemyTank).start();
            // - 加入
            enemyTanks.add(enemyTank);
        }
        //初始化三个爆炸的图片对象
        image1 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_1.gif"));
        image2 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_2.gif"));
        image3 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_3.gif"));
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        //绘图区域:填充矩形,默认是黑色
        g.fillRect(0, 0, 1000, 750);
        //画出自己的坦克->封装到画坦克的方法中
        // - 对direction做了修改,把它放到了tank父类中(使用get()方法访问)
        if (myTank != null && myTank.isLive) {
            drawTank(myTank.getX(), myTank.getY(), g, myTank.getDirection(), 0);//画出我的坦克
        }
        //画敌人的坦克和子弹->遍历集合
        for (int i = 0; i < enemyTanks.size(); i++) {
            // - 取出坦克
            EnemyTank enemyTank = enemyTanks.get(i);
            // - 判断坦克是否存在,存在时才画这个坦克和它的子弹
            if (enemyTank.isLive) {
                drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirection(), 1);
                //画敌方坦克的子弹 => 画出敌方坦克时,也画出每个坦克的(多颗)子弹(遍历每个坦克的子弹集合)
                for (int j = 0; j < enemyTank.shoots.size(); j++) {
                    // - 取出子弹
                    Shoot shoot = enemyTank.shoots.get(j);
                    if (shoot.isLive) {// - 如果子弹存在就画
                        g.setColor(Color.white);
                        g.fillOval(shoot.x, shoot.y, 2, 2);
                    } else {
                        // - 如果子弹不存在,就把该子弹从Vector中移除
                        // -- 按照每个敌方坦克等到一颗子弹销毁后,才能发射另一颗的解决方法:
                        // 因为子弹不存活时,就会被移出集合,即子弹销毁后,集合是空的,所以就在EnemyTank中
                        // 判断集合是否为空,若为空,就创建新子弹线程并加入集合=>注意要判断当前坦克的方向来创建坦克
                        enemyTank.shoots.remove(j);
                    }
                }
            }
        }
        //画我方坦克的子弹=>如果子弹线程非空且子弹存在
        if (myTank.shoot != null && myTank.shoot.isLive) {//单颗子弹
            g.setColor(Color.white);
            g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);
        }
//        for (int i = 0; i < myTank.shoots.size(); i++) {//多颗子弹
//            //- 取出
//            Shoot shoot = myTank.shoots.get(i);
//            //- 如果存在就画出子弹,不存在就从集合中移除
//            if (shoot.isLive) {
//                g.setColor(Color.white);
//                g.fillOval(shoot.x, shoot.y, 2, 2);
//            } else {
//                myTank.shoots.remove(shoot);
//            }
//        }
        //画子弹爆炸效果的图片
        //- 遍历bombs集合,画出每个(不用判断是否为空,因为size=0时,循环0次)
        for (int i = 0; i < bombs.size(); i++) {
            //- 取出一个爆炸类(一个爆炸类可以控制三张图片的显示)
            try {
                Thread.sleep(50);//休眠50ms,不然执行速度太快,第一个坦克爆炸没有效果
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Bomb bomb = bombs.get(i);
            //- 因为run()中不断重绘,所以不停执行这个for循环,life是一直减少的
            if (bomb.life > 6) {//- 不同的爆炸时期,画出不同的爆炸效果
                //- bomb对象在hitTank()中初始化过了
                g.drawImage(image1, bomb.x, bomb.y, 60, 60, this);
            } else if (bomb.life > 3) {
                g.drawImage(image2, bomb.x, bomb.y, 60, 60, this);
            } else {
                g.drawImage(image3, bomb.x, bomb.y, 60, 60, this);
            }
            bomb.lifeDown();//- 让图片显示的生命值减少
            //- 在lifeDown()中设置了如果life=0,则isLive=false
            //-- 当life=0时,爆炸效果显示完毕,那就把该bomb对象从集合中移除,下次就不会再遍历到他了
            if (!(bomb.isLive)) {
                bombs.remove(bomb);
            }
        }
    }

    /**
     * 该项目有两种类型坦克:①我方②敌方 -> 不同类型的坦克,颜色不同
     * 该项目有四种移动方向:不同移动方向使用画笔绘制的坦克是不同的
     *
     * @param x         坦克左上角x坐标
     * @param y         坦克左上角y坐标
     * @param g         画笔
     * @param direction 坦克的移动方向(上下左右)
     * @param type      坦克的类型(敌方/我方)
     */
    public void drawTank(int x, int y, Graphics g, int direction, int type) {
        //两种类型的坦克①我方②敌方 -> 不同类型的坦克,颜色不同
        switch (type) {
            case 0://我方
                g.setColor(Color.CYAN);
                break;
            case 1://敌方
                g.setColor(Color.YELLOW);
                break;
        }
        //四种移动方向:不同移动方向使用画笔绘制的坦克是不同的
        //direction:0:向上,1:向右,2:向下,3:向左
        switch (direction) {
            case 0://向上
                g.fill3DRect(x, y, 10, 60, false);//画坦克左边轱辘
                g.fill3DRect(x + 30, y, 10, 60, false);//画坦克右边轱辘
                g.fill3DRect(x + 10, y + 10, 20, 40, false);//画坦克的身体
                g.fillOval(x + 10, y + 20, 20, 20);//画坦克的圆盖
                g.drawLine(x + 20, y, x + 20, y + 30);//画坦克的炮筒
                break;
            case 1://向右
                g.fill3DRect(x, y, 60, 10, false);//画坦克上边轱辘
                g.fill3DRect(x, y + 30, 60, 10, false);//画坦克下边轱辘
                g.fill3DRect(x + 10, y + 10, 40, 20, false);//画坦克的身体
                g.fillOval(x + 20, y + 10, 20, 20);//画坦克的圆盖
                g.drawLine(x + 30, y + 20, x + 60, y + 20);//画坦克的炮筒
                break;
            case 2://向下
                g.fill3DRect(x, y, 10, 60, false);//画坦克左边轱辘
                g.fill3DRect(x + 30, y, 10, 60, false);//画坦克右边轱辘
                g.fill3DRect(x + 10, y + 10, 20, 40, false);//画坦克的身体
                g.fillOval(x + 10, y + 20, 20, 20);//画坦克的圆盖
                g.drawLine(x + 20, y + 30, x + 20, y + 60);//画坦克的炮筒
                break;
            case 3://向左
                g.fill3DRect(x, y, 60, 10, false);//画坦克左边轱辘
                g.fill3DRect(x, y + 30, 60, 10, false);//画坦克右边轱辘
                g.fill3DRect(x + 10, y + 10, 40, 20, false);//画坦克的身体
                g.fillOval(x + 20, y + 10, 20, 20);//画坦克的圆盖
                g.drawLine(x + 30, y + 20, x, y + 20);//画坦克的炮筒
                break;
        }
    }

    //判断是否击中,若击中则坦克消失(这里按判断我方击中敌方)
    // - 什么时候判断我方坦克是否击中敌方坦克?=> 在MyPanel的run()中,每隔100ms判断一次。
    // -- 开始时想到当按下J时判断,但是这样的只会判断一次,而且里面的内容(包括:子弹的位置和坦克的位置都只获取了一次),那这是永远判断不成功的
    //把EnemyTank改成Tank,使得MyTank也能使用这个方法,给Tank类写一个isLive属性(因为tank访问不到子类特有的属性)
    public void hitTank(Shoot s, Tank tank) {
        // - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向
        switch (tank.getDirection()) {
            case 0://坦克向上
            case 2://坦克向下
                if (s.x > tank.getX() && s.x < tank.getX() + 40 &&
                        s.y > tank.getY() && s.y < tank.getY() + 60) {
                    s.isLive = false;//子弹消失
                    tank.isLive = false;//敌人坦克消失
                    enemyTanks.remove(tank);//从集合中移除被击中的坦克
                    //击中时,创建bomb对象,放入bombs集合中
                    Bomb bomb = new Bomb(tank.getX(), tank.getY());
                    bombs.add(bomb);
                }
            case 1://坦克向右
            case 3://坦克向左
                if (s.x > tank.getX() && s.x < tank.getX() + 60 &&
                        s.y > tank.getY() && s.y < tank.getY() + 40) {
                    s.isLive = false;//子弹消失
                    tank.isLive = false;//敌人坦克消失
                    enemyTanks.remove(tank);//从集合中移除被击中的坦克
                    //击中时,创建bomb对象,放入bombs集合中
                    Bomb bomb = new Bomb(tank.getX(), tank.getY());
                    bombs.add(bomb);
                }
        }
    }

    public void hitEnemyTank() {
        //我方坦克发射多颗子弹:遍历每颗子弹判断是否击中
//        for (int i = 0; i < myTank.shoots.size(); i++) {
//            //取出子弹
//            Shoot shoot = myTank.shoots.get(i);
//            //判断该颗子弹是否击中坦克=>遍历坦克
//            for (int j = 0; j < enemyTanks.size(); j++) {
//                EnemyTank enemyTank = enemyTanks.get(j);
//                hitTank(shoot, enemyTank);
//            }
//        }
        //单颗
        //-- 如果我方发射线程不为空且子弹存在,再判断是否击中
        // 在调用run()之前,myTank已经进行了初始化,所以myTank不为空,但是shoot线程是在按下J键时创建的,
        // 程序刚开始不按J键时,shoot为空,所以不写myTank.shoot != null这个条件,会报空指针异常
        //- 遇到一个问题:子弹打到坦克时,子弹的isLive=false,所以子弹不再绘制,但是子弹的xy还在继续变化
        // 这是因为在isLive=false时,发射线程并没有退出=>前面设置的是 子弹碰到墙壁后退出线程,所以要为退出线程增加一个条件
        if (myTank.shoot != null && myTank.shoot.isLive) {//单颗子弹
            // -- 遍历敌方坦克,判断击中哪个
            for (int i = 0; i < enemyTanks.size(); i++) {
                EnemyTank enemyTank = enemyTanks.get(i);
                hitTank(myTank.shoot, enemyTank);
            }
        }
    }

    public void hitMyTank() {
        for (int i = 0; i < enemyTanks.size(); i++) {//判断每个坦克的每颗子弹 是否击中我方坦克
            EnemyTank enemyTank = enemyTanks.get(i);//取出敌人坦克
            for (int j = 0; j < enemyTank.shoots.size(); j++) {
                Shoot s = enemyTank.shoots.get(j);//取出敌人子弹
                //如果我方坦克存在 且 该颗子弹存在 就判断子弹是否集中我方坦克
                if (myTank.isLive && s.isLive) {
                    hitTank(s, myTank);
                }
            }
        }
    }

    //事件处理方法(对按键进行监听)
    @Override
    public void keyPressed(KeyEvent e) {
        //判断事件(当按下WDSA键时进行处理)
        if (e.getKeyCode() == KeyEvent.VK_W) {//上
            myTank.moveUp();//改变坦克的坐标(将改变坐标封装到父类的moveUp()方法中)
            myTank.setDirection(0);//改变坦克的方向(将direction作为所有坦克的属性放到父类中)
        } else if (e.getKeyCode() == KeyEvent.VK_D) {//右
            myTank.moveRight();
            myTank.setDirection(1);
        } else if (e.getKeyCode() == KeyEvent.VK_S) {//下
            myTank.moveDown();
            myTank.setDirection(2);
        } else if (e.getKeyCode() == KeyEvent.VK_A) {//左
            myTank.moveLeft();
            myTank.setDirection(3);
        }
        //按下J键时,我方坦克发射子弹
        if (e.getKeyCode() == KeyEvent.VK_J) {
            //- 出现的问题:
            //-- 按一下J,只会调用一次keyPressed(),那面板只会重绘一次
            //-- 因此按一下J,只能看到一个不会动小球
            //-- 解决方法:让面板每隔100ms,自动重绘=>用多线程(面板实现Runnable接口)

            //- 实现:当子弹消亡时,才会发射新子弹
            //-- 方法:当子弹线程为空 或 子弹消亡时,调用shootEnemyTank()
            //- 实现:当我方坦克被击中时,不能再发射子弹
            //-- 方法:增加一个条件: myTank.isLive==true 当我方坦克存在时,才创建子弹
            if (myTank.isLive && myTank.shoot == null || !(myTank.shoot.isLive)) {//发射单颗(销毁了才会发第二颗)
                myTank.shootEnemyTank();
            }
//            myTank.shootEnemyTank();//发射多颗子弹:无需判断
        }
        //重绘
        this.repaint();
    }

    @Override
    public void keyReleased(KeyEvent e) {

    }

    @Override
    public void keyTyped(KeyEvent e) {

    }

    //每隔100ms,重绘面板,使子弹移动
    // - 要while不停的循环这些内容,不然线程只执行一次就退出了
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //- 判断是否击中敌人坦克
            hitEnemyTank();
//            hitEnemyTank();//多颗子弹
            // - 判断敌方坦克是否击中我方坦克
            hitMyTank();
            this.repaint();
        }
    }
}

TankGame窗口类

package com.wpz.tankgame2_2;

import javax.swing.*;

/**
 * @author 王胖子
 * @version 1.0
 * 窗口类
 * 在构造器中:设置窗口信息
 */
public class TankGame extends JFrame {
    private MyPanel mp = null;//定义面板

    public static void main(String[] args) {
        new TankGame();
    }

    public TankGame() {
        this.mp = new MyPanel();
        //启动线程mp的线程:重绘面板
        Thread thread = new Thread(mp);
        thread.start();
        this.add(mp);
        this.setSize(1000, 750);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
        this.addKeyListener(mp);//为mp面板添加键盘监听器
    }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值