栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

第18章:坦克大战2.1
  • 总体内容(增加的功能)
  • 1. 敌方坦克发射子弹
    • 思路
    • 实现
  • 2. 敌方坦克击中就消失
    • 实现
  • 3. 击中的爆炸效果
    • 实现
  • 4. 敌方坦克自由移动
    • 思路
  • 5. 控制坦克移动范围
    • 实现
  • 版本2.1的全部代码
    • Tank类
    • MyTank类
    • EnemyTank类
    • MyPanel类(面板类)
    • Shoot类(子弹类)
    • Bomb类(爆炸效果类)
    • TankGame类(窗口类)


总体内容(增加的功能)
  • 2.1版本包括
    ①敌方坦克发射子弹
    ②敌方坦克击中就消失
    ③击中的爆炸效果
    ④敌方坦克自由移动
    ⑤控制坦克移动范围

1. 敌方坦克发射子弹

需求: 让敌人的坦克也能发射子弹(可以有多颗)

思路

实现

EnemyTank类和MyPanel类

  1. EnemyTank类中:①定义子弹线程的集合(因为允许一个敌方坦克发射多颗子弹) ②明白为什么不能在构造器中创建子弹线程(代码的注释里写了原因)
  2. MyPanel类类中:①修改:初始化敌方坦克后,创建+启动这个坦克的子弹线程,并把这个线程添加到自己的子弹集合中 ②修改:在画敌方坦克时,从每个坦克子弹集合中取出每颗子弹并绘制子弹
package com.wpz.tankgame;

import java.util.Vector;


public class EnemyTank extends Tank {
    //千万注意要初始化这个Vector...不然会报空指针
    Vector shoots = new Vector();//一个敌方坦克可以发射多颗子弹,所以这里用集合

    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());
    }
}

package com.wpz.tankgame;

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


//为了让面板不停重绘子弹,让面板实现Runnable接口,当作一个线程使用(在run()中写重绘)
public class MyPanel extends JPanel implements KeyListener, Runnable {
    MyTank myTank = null;//定义一个自己的坦克
    Vector enemyTanks = new Vector<>();//定义敌人的坦克,放到Vector中
    int enemyTankSize = 3;//敌人坦克的数量

    public MyPanel() {
        myTank = new MyTank(100, 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);
            // - 加入
            enemyTanks.add(enemyTank);
        }
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0, 0, 1000, 750);//绘图区域:填充矩形,默认是黑色
        //画出自己的坦克->封装到画坦克的方法中
        // - 对direction做了修改,把它放到了tank父类中(使用get()方法访问)
        drawTank(myTank.getX(), myTank.getY(), g, myTank.getDirection(), 0);//画出我的坦克
        //画敌人的坦克->遍历集合
        for (int i = 0; i < enemyTanks.size(); i++) {
            // - 取出坦克
            EnemyTank enemyTank = enemyTanks.get(i);
            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.shoots.remove(j);
                }
            }
        }
        //画我方坦克的子弹=>如果子弹线程非空且子弹存在
        if (myTank.shoot != null && myTank.shoot.isLive) {
            g.setColor(Color.white);
            g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);
        }
    }

    
    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;
        }
    }

    //事件处理方法(对按键进行监听)
    @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接口)
            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();
            }
            this.repaint();
        }
    }
}

输出效果:


2. 敌方坦克击中就消失

需求:当我方坦克击中敌方坦克时,敌方坦克就消失,如果能做出爆炸效果更好

实现

MyPanel类,EnemyTank类,Shoot类

  1. MyPanel类中:
    ① 增加一个方法hitTank(Shoot s,EnemyTank enemyTank),判断子弹是否击中坦克。因为坦克的方向不同,坦克所在的范围也不同,所以根据坦克的方向来判断=>子弹的x是否进入坦克的范围内,若进入,则将子弹isLive置false,坦克的isLive置false。这里没有直接遍历敌人坦克,没有直接获取我方坦克的子弹,而使用传参的方式:这样的处理使得这个方法更加灵活,使得方法还可以用于敌人坦克打我方坦克。这里没有将坦克从集合中移除而是用了isLive的方式:也是为了上面的原因,我方坦克不能使用集合方式来移除
    ② 修改:画敌方坦克时,增加判断。只有坦克存在的时候才画敌人坦克和敌人子弹。
    ③ 增加:Q:在哪里调用hitTank() 呢 A:在MyPanel的run()中,遍历坦克集合,判断我方坦克是否击中。不能在按下J键时调用,原因在注释里写了
  2. EnemyTank类中:增加属性IsLive,表示坦克是否存在
  3. Shoot类中:修改线程结束的条件为=>碰到墙壁或击中坦克。解决了击中坦克时,子弹消失(不再绘制子弹s.isLive=false),但是线程还没结束的问题。
package com.wpz.tankgame;

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


//为了让面板不停重绘子弹,让面板实现Runnable接口,当作一个线程使用(在run()中写重绘)
public class MyPanel extends JPanel implements KeyListener, Runnable {
    MyTank myTank = null;//定义一个自己的坦克
    Vector enemyTanks = new Vector<>();//定义敌人的坦克,放到Vector中
    int enemyTankSize = 3;//敌人坦克的数量

    public MyPanel() {
        myTank = new MyTank(100, 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);
            // - 加入
            enemyTanks.add(enemyTank);
        }
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0, 0, 1000, 750);//绘图区域:填充矩形,默认是黑色
        //画出自己的坦克->封装到画坦克的方法中
        // - 对direction做了修改,把它放到了tank父类中(使用get()方法访问)
        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.shoots.remove(j);
                    }
                }
            }
        }
        //画我方坦克的子弹=>如果子弹线程非空且子弹存在
        if (myTank.shoot != null && myTank.shoot.isLive) {
            g.setColor(Color.white);
            g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);
        }
    }

    
    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时判断,但是这样的只会判断一次,而且里面的内容(包括:子弹的位置和坦克的位置都只获取了一次),那这是永远判断不成功的
    public void hitTank(Shoot s, EnemyTank enemyTank) {
        // - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向
        switch (enemyTank.getDirection()) {
            case 0://坦克向上
            case 2://坦克向下
                if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 40 &&
                        s.y > enemyTank.getY() && s.y < enemyTank.getY() + 60) {
                    s.isLive = false;//子弹消失
                    enemyTank.isLive = false;//敌人坦克消失
                }
            case 1://坦克向右
            case 3://坦克向左
                if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 60 &&
                        s.y > enemyTank.getY() && s.y < enemyTank.getY() + 40) {
                    s.isLive = false;//子弹消失
                    enemyTank.isLive = false;//敌人坦克消失
                }
        }
    }

    //事件处理方法(对按键进行监听)
    @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接口)
            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();
            }
            //- 判断是否击中敌人坦克
            //-- 如果我方发射线程不为空且子弹存在,再判断是否击中
            // 在调用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);
                }
            }
            this.repaint();
        }
    }
}

package com.wpz.tankgame;

import java.util.Vector;


public class EnemyTank extends Tank {
    //千万注意要初始化这个Vector...不然会报空指针
    Vector 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());
    }
}

package com.wpz.tankgame;


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("线程结束");
                break;//线程结束
            }
        }
    }
}


3. 击中的爆炸效果 实现

增加Bomb类,修改MyPanel类

  1. 准备三张图片放到out/production/项目下
  2. 新建Bomb爆炸类:①图片显示的位置xy ②控制图片逐个显示+显示的时间:life ③显示时间为0时,就让图片都不再绘制且把这个类从集合中移除:isLive=>具体的解释看代码注释
  3. MyPanel类中:①增加bombs集合(存放被击中坦克的爆炸效果),三张图片 ②在构造器中初始化这三张图片 ③在hitTank()中修改:如果击中坦克,则初始化一个Bomb对象放到bombs集合中 ④在paint()中:画出爆炸效果的三张图片=>注意只需要循环遍历bombs集合,不需要循环减少效果的生命值,因为run()中一直在重绘。⑤为了解决击中已死亡的坦克还能出现爆炸效果的问题:在hitTank()中,将已死亡坦克从集合中移除。
package com.wpz.tankgame;


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;//
        }
    }
}

package com.wpz.tankgame;

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


//为了让面板不停重绘子弹,让面板实现Runnable接口,当作一个线程使用(在run()中写重绘)
public class MyPanel extends JPanel implements KeyListener, Runnable {
    MyTank myTank = null;//定义一个自己的坦克
    Vector enemyTanks = new Vector<>();//定义敌人的坦克,放到Vector中
    int enemyTankSize = 3;//敌人坦克的数量
    //定义炸弹,放到Vector中=>炸弹爆炸不属于坦克属性,属于面板的属性
    //- 当子弹击中坦克时,加入一个Bomb对象到bombs:被击中的每个坦克都有一个Bomb对象,加入集合中统一管理
    Vector bombs = new Vector<>();
    //定义三张炸弹爆炸的图片,用于显示炸弹爆炸效果
    Image image1 = null;
    Image image2 = null;
    Image image3 = null;

    public MyPanel() {
        myTank = new MyTank(100, 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);
            // - 加入
            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()方法访问)
        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.shoots.remove(j);
                    }
                }
            }
        }
        //画我方坦克的子弹=>如果子弹线程非空且子弹存在
        if (myTank.shoot != null && myTank.shoot.isLive) {
            g.setColor(Color.white);
            g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);
        }
        //画子弹爆炸效果的图片
        //- 遍历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);
            }
        }
    }

    
    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时判断,但是这样的只会判断一次,而且里面的内容(包括:子弹的位置和坦克的位置都只获取了一次),那这是永远判断不成功的
    public void hitTank(Shoot s, EnemyTank enemyTank) {
        // - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向
        switch (enemyTank.getDirection()) {
            case 0://坦克向上
            case 2://坦克向下
                if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 40 &&
                        s.y > enemyTank.getY() && s.y < enemyTank.getY() + 60) {
                    s.isLive = false;//子弹消失
                    enemyTank.isLive = false;//敌人坦克消失
                    enemyTanks.remove(enemyTank);//从集合中移除被击中的坦克
                    //击中时,创建bomb对象,放入bombs集合中
                    Bomb bomb = new Bomb(enemyTank.getX(), enemyTank.getY());
                    bombs.add(bomb);
                }
            case 1://坦克向右
            case 3://坦克向左
                if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 60 &&
                        s.y > enemyTank.getY() && s.y < enemyTank.getY() + 40) {
                    s.isLive = false;//子弹消失
                    enemyTank.isLive = false;//敌人坦克消失
                    enemyTanks.remove(enemyTank);//从集合中移除被击中的坦克
                    //击中时,创建bomb对象,放入bombs集合中
                    Bomb bomb = new Bomb(enemyTank.getX(), enemyTank.getY());
                    bombs.add(bomb);
                }
        }
    }

    //事件处理方法(对按键进行监听)
    @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接口)
            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();
            }
            //- 判断是否击中敌人坦克
            //-- 如果我方发射线程不为空且子弹存在,再判断是否击中
            // 在调用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);
                }
            }
            this.repaint();
        }
    }
}

输出效果:


4. 敌方坦克自由移动

需求:让敌方坦克也可以自由随机的上下左右移动

思路

体会线程的妙用!
思路补充一条:4. 在创建敌人坦克对象时,启动线程

### 实现

EnemyTank类和MyPanel类

  1. EnemyTank类:①实现Runnable接口=>敌人坦克类可以当作线程使用了 ②编写run()=>让坦克动起来
  2. MyPanel类中:初始化敌方坦克后,启动这个线程
package com.wpz.tankgame;

import java.util.Vector;


public class EnemyTank extends Tank implements Runnable {
    //千万注意要初始化这个Vector...不然会报空指针
    Vector 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) {
            
            //思路:先向坦克的当前方向走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;//退出线程
            }
        }
    }
}


输出效果:


5. 控制坦克移动范围

需求:控制我方的坦克和敌人的坦克在规定的范围内移动

实现

Tank类

  • 因为我方坦克和敌方坦克都必须在窗口内移动,所以把判断条件放到move的方法中
  • 每次移动时都判断坦克的坐标是否超出边界,若没有超,就可以继续移动
package com.wpz.tankgame;


public class Tank {
    private int x;//坦克横坐标
    private int y;//坦克纵坐标
    //将坦克的方向写到父类中 这样任意坦克在不同方法中 都可以设置移动方向
    private int direction;//坦克的方向(0:上,1:右,2:下,3:左)-->默认是0
    private int speed = 2;//移动速度

    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;
        }
    }
}


版本2.1的全部代码 Tank类
package com.wpz.tankgame;


public class Tank {
    private int x;//坦克横坐标
    private int y;//坦克纵坐标
    //将坦克的方向写到父类中 这样任意坦克在不同方法中 都可以设置移动方向
    private int direction;//坦克的方向(0:上,1:右,2:下,3:左)-->默认是0
    private int speed = 2;//移动速度

    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.tankgame;


public class MyTank extends Tank {
    Shoot shoot;//子弹发射的线程

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

    public void shootEnemyTank() {//射击
        //创建线程:根据坦克的位置和方向 来创建 子弹射击的线程
        // - 判断坦克的方向,来创建线程
        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);
        }
        //启动线程
        shoot.start();
    }
}

EnemyTank类
package com.wpz.tankgame;

import java.util.Vector;


public class EnemyTank extends Tank implements Runnable {
    //千万注意要初始化这个Vector...不然会报空指针
    Vector 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) {
            
            //思路:先向坦克的当前方向走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;//退出线程
            }
        }
    }
}

MyPanel类(面板类)
package com.wpz.tankgame;

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


//为了让面板不停重绘子弹,让面板实现Runnable接口,当作一个线程使用(在run()中写重绘)
public class MyPanel extends JPanel implements KeyListener, Runnable {
    MyTank myTank = null;//定义一个自己的坦克
    Vector enemyTanks = new Vector<>();//定义敌人的坦克,放到Vector中
    int enemyTankSize = 3;//敌人坦克的数量
    //定义炸弹,放到Vector中=>炸弹爆炸不属于坦克属性,属于面板的属性
    //- 当子弹击中坦克时,加入一个Bomb对象到bombs:被击中的每个坦克都有一个Bomb对象,加入集合中统一管理
    Vector bombs = new Vector<>();
    //定义三张炸弹爆炸的图片,用于显示炸弹爆炸效果
    Image image1 = null;
    Image image2 = null;
    Image image3 = null;

    public MyPanel() {
        myTank = new MyTank(100, 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()方法访问)
        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.shoots.remove(j);
                    }
                }
            }
        }
        //画我方坦克的子弹=>如果子弹线程非空且子弹存在
        if (myTank.shoot != null && myTank.shoot.isLive) {
            g.setColor(Color.white);
            g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);
        }
        //画子弹爆炸效果的图片
        //- 遍历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);
            }
        }
    }

    
    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时判断,但是这样的只会判断一次,而且里面的内容(包括:子弹的位置和坦克的位置都只获取了一次),那这是永远判断不成功的
    public void hitTank(Shoot s, EnemyTank enemyTank) {
        // - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向
        switch (enemyTank.getDirection()) {
            case 0://坦克向上
            case 2://坦克向下
                if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 40 &&
                        s.y > enemyTank.getY() && s.y < enemyTank.getY() + 60) {
                    s.isLive = false;//子弹消失
                    enemyTank.isLive = false;//敌人坦克消失
                    enemyTanks.remove(enemyTank);//从集合中移除被击中的坦克
                    //击中时,创建bomb对象,放入bombs集合中
                    Bomb bomb = new Bomb(enemyTank.getX(), enemyTank.getY());
                    bombs.add(bomb);
                }
            case 1://坦克向右
            case 3://坦克向左
                if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 60 &&
                        s.y > enemyTank.getY() && s.y < enemyTank.getY() + 40) {
                    s.isLive = false;//子弹消失
                    enemyTank.isLive = false;//敌人坦克消失
                    enemyTanks.remove(enemyTank);//从集合中移除被击中的坦克
                    //击中时,创建bomb对象,放入bombs集合中
                    Bomb bomb = new Bomb(enemyTank.getX(), enemyTank.getY());
                    bombs.add(bomb);
                }
        }
    }

    //事件处理方法(对按键进行监听)
    @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接口)
            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();
            }
            //- 判断是否击中敌人坦克
            //-- 如果我方发射线程不为空且子弹存在,再判断是否击中
            // 在调用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);
                }
            }
            this.repaint();
        }
    }
}

Shoot类(子弹类)
package com.wpz.tankgame;


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;//线程结束
            }
        }
    }
}

Bomb类(爆炸效果类)
package com.wpz.tankgame;


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;//
        }
    }
}

TankGame类(窗口类)
package com.wpz.tankgame;

import javax.swing.*;


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面板添加键盘监听器
    }
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/847955.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号