JAVA笔记(二十三):坦克大战【二】(线程---子弹发射 / 敌人坦克自发移动 / 坦克与子弹消失、Vector集合---多线程对象存储、图片导入与绘制)

文章介绍了如何使用Java编程实现坦克大战游戏的升级版本,包括坦克的移动、发射子弹、碰撞检测以及爆炸效果。每个坦克有自己的线程进行移动和发射子弹,当子弹击中敌方坦克时,坦克会消失并显示爆炸效果。同时,增加了敌方坦克的随机移动和发射多颗子弹的能力。
摘要由CSDN通过智能技术生成

线程 - 应用到坦克大战

一、坦克大战3.0版本

1、增强功能

   用户按下J键,我方坦克发射一颗子弹

2、思路

在这里插入图片描述

3、实现代码

Shot类
① 发射相当于一个子功能,且需要不断发射,所以当作一个线程 --> 实现Runnable
② 在run方法中是实现子弹移动–>需要子弹方向direct
③ 且需要不停地自动移动–>while(true)控制 -->在碰壁时退出循环
④ 必须休眠,不然会导致还没有重绘,子弹就碰壁销毁

//把发射作为一个线程
public class Shot implements Runnable{
    int x; //子弹x坐标
    int y; //子弹y坐标
    int direct = 0; //子弹方向
    int speed = 5; //子弹速度
    boolean isAlive = true; //子弹是否存活

    public Shot(int x,int y,int direct) {
        this.x = x;
        this.y = y;
        this.direct = direct;
    }
    @Override
    public void run() { //射击功能
        while(true) {
            //休眠 --> 不休眠会导致子弹在还没有重绘画布的时候就已经到达终点了(移动很快很快)
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            switch (direct) {
                case 0:
                    y -= speed;
                    break;
                case 1:
                    x -= speed;
                    break;
                case 2:
                    y += speed;
                    break;
                case 3:
                    x += speed;
                    break;
            }
            //测试:输出子弹坐标
            //System.out.println("子弹坐标x=" + x + " y=" + y);

            //当子弹移动到边界时会销毁(线程退出)
            if(!(x >= 0 && x <= 1000 && y >= 0 && y <= 750)){
                isAlive = false;
                break;
            }
        }
        //System.out.println("子弹线程退出");
    }
}

Hero类

① 发射功能由坦克调用–>写shotEnemy()方法–>创建Shot,并启动线程
② 创建对象需要输入坐标 --> 子弹坐标画图,由 和hero坐标的关系得到

public class Hero extends Tank {//自己的坦克
    //创建一个shot对象(发射是由坦克调用的)
    Shot shot = null;
    public Hero(int x, int y) {
        super(x, y);
    }

    //MyPanel按下J键时-->调用该方法
    public void shotEnemy() {
        //根据子弹方向创建shot对象
        switch (getDirect()){
            case 0:
                shot = new Shot(getX() + 19, getY(), 0);
                break;
            case 1:
                shot = new Shot(getX(), getY() + 19, 1);
                break;
            case 2:
                shot = new Shot(getX() + 19, getY() + 59, 2);
                break;
            case 3:
                shot = new Shot(getX() + 59, getY() + 19, 3);
                break;
        }
        //启动shot线程
        new Thread(shot).start();
    }
}

MyPanel类

① 按下J键就得产生一个子弹 --> 在keyPressed方法中,检测J键,调用hero.shotEnemy()
② 需要在画布上绘制子弹 --> paint方法中绘制,但需要检测shot对象是否为空、shot对象是否被销毁
④ 子弹移动需要不停地重绘,但由于子弹是自主移动,并不是按下一个键就移动一次,所以不能靠keyPressed中的repaint方法重绘 --> 让MyPanel自动重绘 --> 让MyPanel成为一个线程,实现每隔50ms就重绘一次的功能

public class MyPanel extends JPanel implements KeyListener,Runnable {
    //定义我方坦克
    Hero hero = null;
    //定义敌人坦克
    Vector<Enemy> enemys = new Vector<>(); //因为敌人多线程,所以用Vector
    int enemySize = 3; //敌人坦克个数
    //构造器
    public MyPanel() {
        //初始化我方坦克
        hero = new Hero(470,650);
        hero.setSpeed(3);
        //初始化敌方坦克
        for (int i = 0; i < enemySize; i++) {
            enemys.add(new Enemy((100 * (i+1)),0,2));
        }
    }

    //绘制画布、调用绘制坦克的方法drawTank()
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0,0,1000,750);//填充矩形,默认黑色

        //画出我方坦克(使用自己封装的方法)
        drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 0);          //我方tank1

        //画出敌人坦克
        for (int i = 0; i < enemySize; i++) {
            Enemy enemy = enemys.get(i);
            drawTank(enemy.getX(), enemy.getY(),g,enemy.getDirect(),1);
        }

        //画出子弹(需要在按下J键-->创建了子弹对象并且子弹对象还存活才能画)
        if(hero.shot != null && hero.shot.isAlive){
            //g.fill3DRect(hero.shot.x, hero.shot.y , 2,2,false);
            g.setColor(Color.cyan);
            g.drawOval(hero.shot.x, hero.shot.y , 2,2 );
        }
    }

    /**
     *
     * @param x 坦克的左上角x坐标
     * @param y 坦克的左上角y坐标
     * @param g 画笔
     * @param direct 坦克方向(上下左右)
     * @param type 坦克类型(自己、敌人。。。)
     */
    //编写方法,画出tank
    public void drawTank(int x, int y, Graphics g, int direct, int type) {

        switch (type) {
            case 0://我们的坦克
                g.setColor(Color.cyan);
                break;
            case 1://敌人的坦克
                g.setColor(Color.yellow);
                break;
        }

        //根据tank方向,来绘制对应形状的tank(朝哪边走,头就朝向哪边)
        //0、1、2、3:分别代表向上、向下、向左、向右
        switch (direct) {
            case 0://向上
                g.fill3DRect(x,y,10,60,false);//画出tank左边的轮子
                g.fill3DRect(x+30,y,10,60,false);//画出tank右边的轮子
                g.fill3DRect(x+10, y+10, 20, 40, false); //画出tank中间的长方形炮体
                g.fillOval(x+10, y+20, 20, 20); //画出tank中间的圆台
                g.drawLine(x+19, y+30, x+19,y);
                g.drawLine(x+20, y+30, x+20,y);
                g.drawLine(x+21, y+30, x+21,y);
                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 + 19, x, y + 19);//画出炮筒
                g.drawLine(x + 30, y + 20, x, y + 20);//画出炮筒
                g.drawLine(x + 30, y + 21, x, y + 21);//画出炮筒
                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 + 19, y + 30, x + 19, y + 60);//画出炮筒
                g.drawLine(x + 20, y + 30, x + 20, y + 60);//变粗
                g.drawLine(x + 21, y + 30, x + 21, 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 + 19, x + 60, y + 19);//画出炮筒
                g.drawLine(x + 30, y + 20, x + 60, y + 20);//变粗
                g.drawLine(x + 30, y + 21, x + 60, y + 21);//变粗
                break;


            default:
                System.out.println("暂时没有处理");

        }
    }

    @Override
    public void keyTyped(KeyEvent e) {

    }

    @Override
    public void keyPressed(KeyEvent e) {
        if(e.getKeyCode() == KeyEvent.VK_W) { //向上
            //改变我方tank1方向
            hero.setDirect(0);
            hero.moveUp();
        } else if(e.getKeyCode() == KeyEvent.VK_A){//向左
            hero.setDirect(1);
            hero.moveLeft();
        } else if(e.getKeyCode() == KeyEvent.VK_S){//向下
            hero.setDirect(2);
            hero.moveDown();
        }  else if(e.getKeyCode() == KeyEvent.VK_D){//向右
            hero.setDirect(3);
            hero.moveRight();
        }

        if(e.getKeyCode() == KeyEvent.VK_J) {
            hero.shotEnemy();
        }
        //让面板重绘
        this.repaint();
    }

    @Override
    public void keyReleased(KeyEvent e) {

    }

    //为了让MyPanel不停地重绘,需要将MyPanel变成一个线程,每隔一段时间就自己重绘
    @Override
    public void run() {
        while(true) {
            //每隔50ms就重新paint一次
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.repaint();
        }
    }
}

TankGame03类

① MyPanel成为了一个线程,需要由创建该线程的类进行启动 --> 在TankGame03中,new MyPanel的后面启动mp线程

public class TankGame03 extends JFrame {
    //定义MyPanel
    MyPanel mp = null;
    public static void main(String[] args) {
        TankGame03 tankGame01 = new TankGame03();
    }

    public TankGame03() {
        mp = new MyPanel(); //画板,就是游戏区域
        new Thread(mp).start(); //启动mp线程,不停重绘
        this.add(mp);
        this.setSize(1000,750);
        this.addKeyListener(mp);//让JFrame 监听 mp事件
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }
}

Enemy类和Tank类没有修改

Enemy类

public class Enemy extends Tank {

    public Enemy(int x, int y,int direct) {
        super(x, y);
        super.setDirect(direct);
    }
}

Tank类

public class Tank {
    private int x;
    private int y;
    private int direct = 0 ; //坦克方向 0上 1左 2下 3右,初始化为向上
    private int speed = 3;   //坦克速度,初始化为3

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

    //上下左右移动
    public void moveUp() {
        y -= speed;
    }
    public void moveRight() {
        x += speed;
    }
    public void moveDown() {
        y += speed;
    }
    public void moveLeft() {
        x -= speed;
    }


    public int getSpeed() {
        return speed;
    }

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

    public int getDirect() {
        return direct;
    }

    public void setDirect(int direct) {
        this.direct = direct;
    }

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

二、坦克大战4.0版本

1、增强功能

① 让敌人的坦克也能够发射子弹(可以有多颗子弹)

② 当我方坦克击中敌人坦克时候,敌人的坦克就消失,如果能做出爆炸效果更好;

③ 让敌人的坦克也可以自由随机的上下左右移动;

④ 控制我方的坦克和敌人的坦克在规定的范围移动。

2、思路及实现代码

在这里插入图片描述

1)敌人自动发子弹

Enemy类

① 敌人自带多个子弹 —> Enemy类中存放一个Shot的Vector集合

public class Enemy extends Tank {
    Vector<Shot> shots = new Vector<>();

    public Enemy(int x, int y,int direct) {
        super(x, y);
        super.setDirect(direct);
    }
}

MyPanel类

① 子弹自动发射 —> 需要随着敌人坦克的创建而初始化 —> 敌人子弹初始化 —> 创建Shot线程 —> 存进shots集合中 —> 启动线程
② 子弹画出来 —> 子弹是和敌人坦克绑定的,有敌人坦克才有子弹 —> paint中的画敌人坦克代码块里画敌人子弹 —> 遍历shots集合

public class MyPanel extends JPanel implements KeyListener,Runnable {
    //定义我方坦克
    Hero hero = null;
    //定义敌人坦克
    Vector<Enemy> enemys = new Vector<>(); //因为敌人多线程,所以用Vector
    int enemySize = 3; //敌人坦克个数
    //构造器
    public MyPanel() {
        //初始化我方坦克
        hero = new Hero(470,650);
        hero.setSpeed(3);
        //初始化敌方坦克和子弹
        for (int i = 0; i < enemySize; i++) {
            //初始化敌方坦克
            Enemy enemy = new Enemy((100 * (i+1)),0,2);
            enemys.add(enemy);

            //初始化敌方子弹
            Shot shot = new Shot(enemy.getX() + 19,enemy.getY() + 59,enemy.getDirect());
            //添加到shots集合
            enemy.shots.add(shot);
            //自动启动shot线程
            new Thread(shot).start();
        }
    }

    //绘制画布、调用绘制坦克的方法drawTank()
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0,0,1000,750);//填充矩形,默认黑色

        //画出我方坦克(使用自己封装的方法)
        drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 0);          //我方tank1

        //画出敌人坦克
        for (int i = 0; i < enemySize; i++) {
            Enemy enemy = enemys.get(i);
            drawTank(enemy.getX(), enemy.getY(),g,enemy.getDirect(),1);

            //画出敌人子弹
            for (int j = 0; j < enemy.shots.size(); j++) {
                Shot shot = enemy.shots.get(j);
                if(shot.isAlive) {
                    g.drawOval(shot.x, shot.y,2,2);
                } else {
                    enemy.shots.remove(shot);
                }
            }
        }

        //画出子弹(需要在按下J键-->创建了子弹对象并且子弹对象还存活才能画)
        if(hero.shot != null && hero.shot.isAlive){
            //g.fill3DRect(hero.shot.x, hero.shot.y , 2,2,false);
            g.setColor(Color.cyan);
            g.drawOval(hero.shot.x, hero.shot.y , 2,2 );
        }
    }

    //...省略一些没有变化的代码
 
    //为了让MyPanel不停地重绘,需要将MyPanel变成一个线程,每隔一段时间就自己重绘
    @Override
    public void run() {
        while(true) {
            //每隔50ms就重新paint一次
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.repaint();
        }
    }
}

2)我方子弹打到敌人后敌人消失

在这里插入图片描述

Enemy类

public class Enemy extends Tank {
    boolean isAlive = true;
    Vector<Shot> shots = new Vector<>();

    public Enemy(int x, int y,int direct) {
        super(x, y);
        super.setDirect(direct);
    }
}

MyPanel类

public class MyPanel extends JPanel implements KeyListener,Runnable {
	//声明和初始化...

    //绘制画布、调用绘制坦克的方法drawTank()
    @Override
    public void paint(Graphics g) {
		//....其他paint

        //画出敌人坦克
        for (int i = 0; i < enemys.size(); i++) {
            Enemy enemy = enemys.get(i);
            if(enemy.isAlive){
                drawTank(enemy.getX(), enemy.getY(),g,enemy.getDirect(),1);

                //画出敌人子弹
                for (int j = 0; j < enemy.shots.size(); j++) {

                    Shot shot = enemy.shots.get(j);
                    if(shot.isAlive) {
                        g.drawOval(shot.x, shot.y,2,2);
                    } else {
                        enemy.shots.remove(shot);
                    }
                }
            }
//            else { //在这里移除enemy会导致整个画布跳动(立马重绘一次,残留的坦克就会闪烁),
//            //所以在enemy的isAlive变化的敌方将它移除
//                enemys.remove(enemy);
//            }

        }

    }


    public void hitEnemy(Shot shot , Enemy enemy) {
        switch (enemy.getDirect()) {
            case 0:
            case 2: //上下
                if(shot.x >= enemy.getX() && shot.x <= enemy.getX() + 40
                        && shot.y >= enemy.getY() && shot.y <= enemy.getY() + 60) {
                    enemy.isAlive = false;
                    shot.isAlive = false;
                    enemys.remove(enemy);
                }
                break;
            case 1:
            case 3: //1和3代表左和右,是同一种情况
                if(shot.x >= enemy.getX() && shot.x <= enemy.getX() + 60
                        && shot.y >= enemy.getY() && shot.y <= enemy.getY() + 40) {
                    enemy.isAlive = false;
                    shot.isAlive = false;
                    enemys.remove(enemy);
                }
                break;
        }
    }

 
    //为了让MyPanel不停地重绘,需要将MyPanel变成一个线程,每隔一段时间就自己重绘
    @Override
    public void run() {
        while(true) {
            //每隔50ms就重新paint一次
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.repaint();

            if(hero.shot != null && hero.shot.isAlive){
                for (int i = 0; i < enemys.size(); i++) {
                    hitEnemy(hero.shot, enemys.get(i));
                }
            }
        }
    }
}

3)敌人被击中产生爆炸效果

在这里插入图片描述

Bomb类

public class Bomb {
    int x;
    int y;
    int life = 9;
    boolean isAlive = true;

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

    public void lifeDown(){
        if(life <= 0) {
            isAlive = false;
        } else {
            life--;
        }
    }
}

MyPanel类

public class MyPanel extends JPanel implements KeyListener,Runnable {
	//...
	//定义爆炸
    Vector<Bomb> bombs = new Vector<>();
    Image image1 = null;
    Image image2 = null;
    Image image3 = null;
	//构造器
    public MyPanel() {
		//...
        //初始化爆炸图片
        image1 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/baozha1.png"));
        image2 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/baozha2.png"));
        image3 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/baozha3.png"));
    }
	
	    //绘制画布、调用绘制坦克的方法drawTank()
    @Override
    public void paint(Graphics g) {
    	//...
        //画爆炸
        for (int i = 0; i < bombs.size(); i++) {
            //不休眠第一个会爆炸不出来,取图片太慢了应该
            try {
                Thread.sleep(55);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Bomb bomb = bombs.get(i);
            if(bomb.isAlive) {
                if(bomb.life > 6) {
                    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 if(bomb.life > 0){
                    g.drawImage(image3,bomb.x,bomb.y,60,60,this);
                }
                bomb.lifeDown();
                if(bomb.life == 0) {
                    bombs.remove(bomb);
                }
            }
        }
    }

	public void hitEnemy(Shot shot , Enemy enemy) {
        switch (enemy.getDirect()) {
            case 0:
            case 2: //上下
                if(shot.x >= enemy.getX() && shot.x <= enemy.getX() + 40
                        && shot.y >= enemy.getY() && shot.y <= enemy.getY() + 60) {
                    enemy.isAlive = false;
                    shot.isAlive = false;
                    bombs.add(new Bomb(enemy.getX(), enemy.getY())); //要放在enemy移除前面,不然enemy会被回收
                    enemys.remove(enemy);
                }
                break;
            case 1:
            case 3: //1和3代表左和右,是同一种情况
                if(shot.x >= enemy.getX() && shot.x <= enemy.getX() + 60
                        && shot.y >= enemy.getY() && shot.y <= enemy.getY() + 40) {
                    enemy.isAlive = false;
                    shot.isAlive = false;
                    bombs.add(new Bomb(enemy.getX(), enemy.getY()));
                    enemys.remove(enemy);
                }
                break;
        }
    }
    //...
}

4)让敌人的坦克可以随机自由地上下移动

在这里插入图片描述
Enemy类

public class Enemy extends Tank implements Runnable{
    boolean isAlive = true;
    Vector<Shot> shots = new Vector<>();

    public Enemy(int x, int y,int direct) {
        super(x, y);
        super.setDirect(direct);
    }

    @Override
    public void run() {
        while(true) {
            switch (getDirect()) {
                case 0: //向上
                    for (int i = 0; i < 30; i++) { //移动30步再变化方向
                        moveUp();
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 1: //向左
                    for (int i = 0; i < 30; i++) { //移动30步再变化方向
                        moveLeft();
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 2: //向下
                    for (int i = 0; i < 30; i++) { //移动30步再变化方向
                        moveDown();
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 3: //向右
                    for (int i = 0; i < 30; i++) { //移动30步再变化方向
                        moveRight();
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
            }
            setDirect((int)(Math.random() * 4)); //方向随机设置
            if(!isAlive) { //坦克不存在,则退出坦克线程
                break;
            }
        }
    }
}

MyPanel类

public class MyPanel extends JPanel implements KeyListener,Runnable {
    //...

    //构造器
    public MyPanel() {
        //...
        
        //初始化敌方坦克和子弹
        for (int i = 0; i < enemySize; i++) {
            //初始化敌方坦克
            Enemy enemy = new Enemy((100 * (i+1)),0,2);
            enemys.add(enemy);

            //启动敌方线程
            new Thread(enemy).start();

            //...
        }
        //...
    }
    
    //...
}

5)控制我方坦克和敌人坦克在规定的范围内移动

在这里插入图片描述
Enemy类

public class Enemy extends Tank implements Runnable{
//...
    @Override
    public void run() {
        while(true) {
            switch (getDirect()) {
                case 0: //向上
                    for (int i = 0; i < 30; i++) { //移动30步再变化方向
                        if(getY() > 0){  //在画布范围内移动
                            moveUp();
                        }
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 1: //向左
                    for (int i = 0; i < 30; i++) { //移动30步再变化方向
                        if(getX() > 0) {
                            moveLeft();
                        }
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 2: //向下
                    for (int i = 0; i < 30; i++) { //移动30步再变化方向
                        if (getY() + 60 < 750) {
                            moveDown();
                        }
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 3: //向右
                    for (int i = 0; i < 30; i++) { //移动30步再变化方向
                        if(getX() + 60 < 1000) {
                            moveRight();
                        }
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
            }
            setDirect((int)(Math.random() * 4));
            if(!isAlive) { //坦克不存在,则退出坦克线程
                break;
            }
        }
    }
}

MyPanel类

@Override
    public void keyPressed(KeyEvent e) {
        if(e.getKeyCode() == KeyEvent.VK_W) { //向上
            //改变我方tank1方向
            hero.setDirect(0);
            if(hero.getY() > 0) { //控制移动范围
                hero.moveUp();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_A){//向左
            hero.setDirect(1);
            if(hero.getX() > 0){
                hero.moveLeft();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_S){//向下
            hero.setDirect(2);
            if(hero.getY() + 60 < 750) {
                hero.moveDown();
            }
        }  else if(e.getKeyCode() == KeyEvent.VK_D){//向右
            hero.setDirect(3);
            if(hero.getX() + 60 < 1000) {
                hero.moveRight();
            }
        }

        if(e.getKeyCode() == KeyEvent.VK_J) {
            hero.shotEnemy();
        }
        //让面板重绘
        this.repaint();
    }

6)4.0完整版

package com.rxli.tankgame5;

Tank类

public class Tank {
    private int x;
    private int y;
    private int direct = 0 ; //坦克方向 0上 1左 2下 3右,初始化为向上
    private int speed = 3;   //坦克速度,初始化为3

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

    //上下左右移动
    public void moveUp() {
        y -= speed;
    }
    public void moveRight() {
        x += speed;
    }
    public void moveDown() {
        y += speed;
    }
    public void moveLeft() {
        x -= speed;
    }


    public int getSpeed() {
        return speed;
    }

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

    public int getDirect() {
        return direct;
    }

    public void setDirect(int direct) {
        this.direct = direct;
    }

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

Hero类

public class Hero extends Tank {//自己的坦克
    boolean isAlive = true;
    //创建一个shot对象(发射是由坦克调用的)
    Shot shot = null;
    public Hero(int x, int y) {
        super(x, y);
    }

    //MyPanel按下J键时-->调用该方法
    public void shotEnemy() {
        //根据子弹方向创建shot对象
        switch (getDirect()){
            case 0:
                shot = new Shot(getX() + 19, getY(), 0);
                break;
            case 1:
                shot = new Shot(getX(), getY() + 19, 1);
                break;
            case 2:
                shot = new Shot(getX() + 19, getY() + 59, 2);
                break;
            case 3:
                shot = new Shot(getX() + 59, getY() + 19, 3);
                break;
        }
        //启动shot线程
        new Thread(shot).start();
    }
}

Enemy类

public class Enemy extends Tank implements Runnable{
    boolean isAlive = true;
    Vector<Shot> shots = new Vector<>();

    public Enemy(int x, int y,int direct) {
        super(x, y);
        super.setDirect(direct);
    }

    @Override
    public void run() {
        while(true) {
            switch (getDirect()) {
                case 0: //向上
                    for (int i = 0; i < 30; i++) { //移动30步再变化方向
                        if(getY() > 0){  //在画布范围内移动
                            moveUp();
                        }
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 1: //向左
                    for (int i = 0; i < 100; i++) { //移动100步再变化方向
                        if(getX() > 0) {
                            moveLeft();
                        }
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 2: //向下
                    for (int i = 0; i < 100; i++) { //移动100步再变化方向
                        if (getY() + 60 < 750) {
                            moveDown();
                        }
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 3: //向右
                    for (int i = 0; i < 100; i++) { //移动100步再变化方向
                        if(getX() + 60 < 1000) {
                            moveRight();
                        }
                        //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
            }
            setDirect((int)(Math.random() * 4));
            if(!isAlive) { //坦克不存在,则退出坦克线程
                break;
            }
        }
    }
}

Shot类

//把发射作为一个线程
public class Shot implements Runnable{
    int x; //子弹x坐标
    int y; //子弹y坐标
    int direct = 0; //子弹方向
    int speed = 5; //子弹速度
    boolean isAlive = true; //子弹是否存活

    public Shot(int x,int y,int direct) {
        this.x = x;
        this.y = y;
        this.direct = direct;
    }
    @Override
    public void run() { //射击功能
        while(true) {
            //休眠 --> 不休眠会导致子弹在还没有重绘画布的时候就已经到达终点了(移动很快很快)
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            switch (direct) {
                case 0:
                    y -= speed;
                    break;
                case 1:
                    x -= speed;
                    break;
                case 2:
                    y += speed;
                    break;
                case 3:
                    x += speed;
                    break;
            }
            //测试:输出子弹坐标
            //System.out.println("子弹坐标x=" + x + " y=" + y);

            //当子弹移动到边界时会销毁(线程退出)
            //当子弹碰到坦克就销毁 --> isAlive
            if(!(x >= 0 && x <= 1000 && y >= 0 && y <= 750 && isAlive)){
                isAlive = false;
                break;
            }
        }
        //System.out.println("子弹线程退出");
    }
}

Bomb类

public class Bomb {
    int x;
    int y;
    int life = 9;
    boolean isAlive = true;

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

    public void lifeDown(){
        if(life <= 0) {
            isAlive = false;
        } else {
            life--;
        }
    }
}

MyPanel类

public class MyPanel extends JPanel implements KeyListener,Runnable {
    //定义我方坦克
    Hero hero = null;
    //定义敌人坦克
    Vector<Enemy> enemys = new Vector<>(); //因为敌人多线程,所以用Vector
    //定义爆炸
    Vector<Bomb> bombs = new Vector<>();
    Image image1 = null;
    Image image2 = null;
    Image image3 = null;

    //敌人坦克个数
    int enemySize = 3;

    //构造器
    public MyPanel() {
        //初始化我方坦克
        hero = new Hero(100,100);
        hero.setSpeed(3);
        //初始化敌方坦克和子弹
        for (int i = 0; i < enemySize; i++) {
            //初始化敌方坦克
            Enemy enemy = new Enemy((100 * (i+1)),0,2);
            enemys.add(enemy);

            //启动敌方线程
            new Thread(enemy).start();

            //初始化敌方子弹
            Shot shot = new Shot(enemy.getX() + 19,enemy.getY() + 59,enemy.getDirect());
            //添加到shots集合
            enemy.shots.add(shot);
            //自动启动shot线程
            new Thread(shot).start();
        }
        //初始化爆炸图片
        image1 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/baozha1.png"));
        image2 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/baozha2.png"));
        image3 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/baozha3.png"));
    }

    //绘制画布、调用绘制坦克的方法drawTank()
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0,0,1000,750);//填充矩形,默认黑色

        //画出我方坦克(使用自己封装的方法)
        drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 0);          //我方tank1

        //画出子弹(需要在按下J键-->创建了子弹对象并且子弹对象还存活才能画)
        if(hero.shot != null && hero.shot.isAlive){
            g.setColor(Color.cyan);
            g.drawOval(hero.shot.x, hero.shot.y , 2,2 );
        }

        //画出敌人坦克
        for (int i = 0; i < enemys.size(); i++) {
            Enemy enemy = enemys.get(i);
            if(enemy.isAlive){
                drawTank(enemy.getX(), enemy.getY(),g,enemy.getDirect(),1);

                //画出敌人子弹
                for (int j = 0; j < enemy.shots.size(); j++) {

                    Shot shot = enemy.shots.get(j);
                    if(shot.isAlive) {
                        g.drawOval(shot.x, shot.y,2,2);
                    } else {
                        enemy.shots.remove(shot);
                    }
                }
            }
//            else { //在这里移除enemy会导致整个画布跳动(立马重绘一次,残留的坦克就会闪烁),
//            //所以在enemy的isAlive变化的敌方将它移除
//                enemys.remove(enemy);
//            }
        }

        //画爆炸
        for (int i = 0; i < bombs.size(); i++) {
            //不休眠第一个会爆炸不出来,取图片太慢了应该
            try {
                Thread.sleep(55);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("开始画bomb");
            Bomb bomb = bombs.get(i);
            if(bomb.isAlive) {
                if(bomb.life > 6) {
                    System.out.println("开始画最大的bomb");
                    g.drawImage(image1, bomb.x, bomb.y,60,60,this);
                } else if(bomb.life > 3) {
                    System.out.println("开始画中间的bomb");
                    g.drawImage(image2,bomb.x,bomb.y,60,60,this);
                } else if(bomb.life > 0){
                    System.out.println("开始画最小的bomb");
                    g.drawImage(image3,bomb.x,bomb.y,60,60,this);
                }
                bomb.lifeDown();
                if(bomb.life == 0) {
                    bombs.remove(bomb);
                }
            }
        }
    }

    /**
     *
     * @param x 坦克的左上角x坐标
     * @param y 坦克的左上角y坐标
     * @param g 画笔
     * @param direct 坦克方向(上下左右)
     * @param type 坦克类型(自己、敌人。。。)
     */
    //编写方法,画出tank
    public void drawTank(int x, int y, Graphics g, int direct, int type) {

        switch (type) {
            case 0://我们的坦克
                g.setColor(Color.cyan);
                break;
            case 1://敌人的坦克
                g.setColor(Color.yellow);
                break;
        }

        //根据tank方向,来绘制对应形状的tank(朝哪边走,头就朝向哪边)
        //0、1、2、3:分别代表向上、向下、向左、向右
        switch (direct) {
            case 0://向上
                g.fill3DRect(x,y,10,60,false);//画出tank左边的轮子
                g.fill3DRect(x+30,y,10,60,false);//画出tank右边的轮子
                g.fill3DRect(x+10, y+10, 20, 40, false); //画出tank中间的长方形炮体
                g.fillOval(x+10, y+20, 20, 20); //画出tank中间的圆台
                g.drawLine(x+19, y+30, x+19,y);
                g.drawLine(x+20, y+30, x+20,y);
                g.drawLine(x+21, y+30, x+21,y);
                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 + 19, x, y + 19);//画出炮筒
                g.drawLine(x + 30, y + 20, x, y + 20);//画出炮筒
                g.drawLine(x + 30, y + 21, x, y + 21);//画出炮筒
                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 + 19, y + 30, x + 19, y + 60);//画出炮筒
                g.drawLine(x + 20, y + 30, x + 20, y + 60);//变粗
                g.drawLine(x + 21, y + 30, x + 21, 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 + 19, x + 60, y + 19);//画出炮筒
                g.drawLine(x + 30, y + 20, x + 60, y + 20);//变粗
                g.drawLine(x + 30, y + 21, x + 60, y + 21);//变粗
                break;


            default:
                System.out.println("暂时没有处理");

        }
    }

    public void hitEnemy(Shot shot , Enemy enemy) {
        switch (enemy.getDirect()) {
            case 0:
            case 2: //上下
                if(shot.x >= enemy.getX() && shot.x <= enemy.getX() + 40
                        && shot.y >= enemy.getY() && shot.y <= enemy.getY() + 60) {
                    enemy.isAlive = false;
                    shot.isAlive = false;
                    bombs.add(new Bomb(enemy.getX(), enemy.getY())); //要放在enemy移除前面,不然enemy会被回收
                    enemys.remove(enemy);
                }
                break;
            case 1:
            case 3: //1和3代表左和右,是同一种情况
                if(shot.x >= enemy.getX() && shot.x <= enemy.getX() + 60
                        && shot.y >= enemy.getY() && shot.y <= enemy.getY() + 40) {
                    enemy.isAlive = false;
                    shot.isAlive = false;
                    bombs.add(new Bomb(enemy.getX(), enemy.getY()));
                    enemys.remove(enemy);
                }
                break;
        }
    }

    @Override
    public void keyTyped(KeyEvent e) {

    }

    @Override
    public void keyPressed(KeyEvent e) {
        if(e.getKeyCode() == KeyEvent.VK_W) { //向上
            //改变我方tank1方向
            hero.setDirect(0);
            if(hero.getY() > 0) { //控制移动范围
                hero.moveUp();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_A){//向左
            hero.setDirect(1);
            if(hero.getX() > 0){
                hero.moveLeft();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_S){//向下
            hero.setDirect(2);
            if(hero.getY() + 60 < 750) {
                hero.moveDown();
            }
        }  else if(e.getKeyCode() == KeyEvent.VK_D){//向右
            hero.setDirect(3);
            if(hero.getX() + 60 < 1000) {
                hero.moveRight();
            }
        }

        if(e.getKeyCode() == KeyEvent.VK_J) {
            hero.shotEnemy();
        }
        //让面板重绘
        this.repaint();
    }

    @Override
    public void keyReleased(KeyEvent e) {

    }

    //为了让MyPanel不停地重绘,需要将MyPanel变成一个线程,每隔一段时间就自己重绘
    @Override
    public void run() {
        while(true) {
            //每隔50ms就重新paint一次
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.repaint();

            if(hero.shot != null && hero.shot.isAlive){
                for (int i = 0; i < enemys.size(); i++) {
                    hitEnemy(hero.shot, enemys.get(i));
                }
            }
        }
    }
}

TankGame05类

package com.rxli.tankgame5;

import javax.swing.*;

public class TankGame05 extends JFrame {
    //定义MyPanel
    MyPanel mp = null;
    public static void main(String[] args) {
        TankGame05 tankGame01 = new TankGame05();
    }

    public TankGame05() {
        mp = new MyPanel(); //画板,就是游戏区域
        new Thread(mp).start(); //启动mp线程,不停重绘
        this.add(mp);
        this.setSize(1200,800); //Jrame框比MyPanel画板稍大一点
        this.addKeyListener(mp);//让JFrame 监听 mp事件
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }
}

三、坦克大战5.0版本

1、增加功能

在这里插入图片描述

2、思路及实现代码

1)我方坦克在发射的子弹消亡后,才能发射新的子弹

在这里插入图片描述
MyPanel类

    @Override
    public void keyPressed(KeyEvent e) {
        if(e.getKeyCode() == KeyEvent.VK_W) { //向上
            //改变我方tank1方向
            hero.setDirect(0);
            if(hero.getY() > 0) { //控制移动范围
                hero.moveUp();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_A){//向左
            hero.setDirect(1);
            if(hero.getX() > 0){
                hero.moveLeft();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_S){//向下
            hero.setDirect(2);
            if(hero.getY() + 60 < 750) {
                hero.moveDown();
            }
        }  else if(e.getKeyCode() == KeyEvent.VK_D){//向右
            hero.setDirect(3);
            if(hero.getX() + 60 < 1000) {
                hero.moveRight();
            }
        }

        if(e.getKeyCode() == KeyEvent.VK_J) {
            //发射一颗子弹
            if(hero.shot == null || !hero.shot.isAlive){
                //shotEnemy(): 创建hero.shot线程,并启动
                hero.shotEnemy();
            }
        }
        //让面板重绘
        this.repaint();
    }

2)我方发射多颗子弹,且画板上最多只有5颗子弹

在这里插入图片描述
Hero类

public class Hero extends Tank {//自己的坦克
    boolean isAlive = true;
    //一发子弹
    Shot shot = null;
    //多发子弹集合
    Vector<Shot> shots = new Vector<>();
    public Hero(int x, int y) {
        super(x, y);
    }

    //MyPanel按下J键时-->调用该方法
    public void shotEnemy() {

        //面板上最多有5个子弹
        if(shots.size() == 5) {
            return;
        }

        //根据子弹方向创建shot对象
        switch (getDirect()){
            case 0:
                shot = new Shot(getX() + 19, getY(), 0);
                break;
            case 1:
                shot = new Shot(getX(), getY() + 19, 1);
                break;
            case 2:
                shot = new Shot(getX() + 19, getY() + 59, 2);
                break;
            case 3:
                shot = new Shot(getX() + 59, getY() + 19, 3);
                break;
        }

        //将shot对象加入shots集合
        shots.add(shot);
        //启动shot线程
        new Thread(shot).start();
    }
}

MyPanel类

	@Override
    public void paint(Graphics g) {
        //画出我方子弹
        for (int i = 0; i < hero.shots.size(); i++) {
            Shot shot = hero.shots.get(i);
            //创建了子弹对象并且子弹对象还存活才能画
            if(shot.isAlive){
                g.setColor(Color.cyan);
                g.drawOval(shot.x, shot.y , 2,2 );
            } else {
                hero.shots.remove(shot);
            }
        }
    }
        
	/**
     * 遍历所有子弹,再遍历所有坦克,判断是否打中
     */
    public void hitEnemy() {
        for (int i = 0; i < hero.shots.size(); i++) {
            Shot shot = hero.shots.get(i);
            if(shot != null && shot.isAlive){
                for (int j = 0; j < enemys.size(); j++) {
                    singleHitEnemy(shot, enemys.get(j));
                }
            }
        }
    }
    /**
     * 传入一个子弹和一个坦克,判断这颗子弹是否打中坦克
     * @param shot 单科子弹
     * @param enemy 单个坦克
     */
    public void singleHitEnemy(Shot shot , Enemy enemy) {
        switch (enemy.getDirect()) {
            case 0:
            case 2: //上下
                if(shot.x >= enemy.getX() && shot.x <= enemy.getX() + 40
                        && shot.y >= enemy.getY() && shot.y <= enemy.getY() + 60) {
                    enemy.isAlive = false;
                    shot.isAlive = false;
                    bombs.add(new Bomb(enemy.getX(), enemy.getY())); //要放在enemy移除前面,不然enemy会被回收
                    enemys.remove(enemy);
                }
                break;
            case 1:
            case 3: //1和3代表左和右,是同一种情况
                if(shot.x >= enemy.getX() && shot.x <= enemy.getX() + 60
                        && shot.y >= enemy.getY() && shot.y <= enemy.getY() + 40) {
                    enemy.isAlive = false;
                    shot.isAlive = false;
                    bombs.add(new Bomb(enemy.getX(), enemy.getY()));
                    enemys.remove(enemy);
                }
                break;
        }
    }

	@Override
    public void keyPressed(KeyEvent e) {
        if(e.getKeyCode() == KeyEvent.VK_W) { //向上
            //改变我方tank1方向
            hero.setDirect(0);
            if(hero.getY() > 0) { //控制移动范围
                hero.moveUp();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_A){//向左
            hero.setDirect(1);
            if(hero.getX() > 0){
                hero.moveLeft();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_S){//向下
            hero.setDirect(2);
            if(hero.getY() + 60 < 750) {
                hero.moveDown();
            }
        }  else if(e.getKeyCode() == KeyEvent.VK_D){//向右
            hero.setDirect(3);
            if(hero.getX() + 60 < 1000) {
                hero.moveRight();
            }
        }

        if(e.getKeyCode() == KeyEvent.VK_J) {
//            //发射一颗子弹
//            if(hero.shot == null || !hero.shot.isAlive){
//                //创建hero.shot线程,并启动
//                hero.shotEnemy();
//            }
            hero.shotEnemy();
        }
        //让面板重绘
        this.repaint();
    }

    //为了让MyPanel不停地重绘,需要将MyPanel变成一个线程,每隔一段时间就自己重绘
    @Override
    public void run() {
        while(true) {
            //每隔50ms就重新paint一次
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.repaint();

            //判断画布上是否有其中一颗子弹打中其中一个坦克的情况
            hitEnemy();

        }
    }

3)每辆敌人坦克可以发射2颗子弹

在这里插入图片描述
Enemy类

public class Enemy extends Tank implements Runnable{
    boolean isAlive = true;
    Vector<Shot> shots = new Vector<>();
    Shot shot = null;

    public Enemy(int x, int y,int direct) {
        super(x, y);
        super.setDirect(direct);
    }

    public void shotHero() {
        //Enemy自动发射子弹
        if(shots.size() < 2){ //在画布上敌人最多只能有2个子弹
            //创建一个子弹
            switch (getDirect()){
                case 0:
                    shot = new Shot(getX() + 19, getY(), 0);
                    break;
                case 1:
                    shot = new Shot(getX(), getY() + 19, 1);
                    break;
                case 2:
                    shot = new Shot(getX() + 19, getY() + 59, 2);
                    break;
                case 3:
                    shot = new Shot(getX() + 59, getY() + 19, 3);
                    break;
            }
            //添加到shot集合中
            shots.add(shot);
            //启动子弹线程
            new Thread(shot).start();
        }
    }

    public void move() {
        switch (getDirect()) {
            case 0: //向上
                for (int i = 0; i < 30; i++) { //移动30步再变化方向
                    if(getY() > 0){  //在画布范围内移动
                        moveUp();
                    }
                    //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case 1: //向左
                for (int i = 0; i < 30; i++) { //移动30步再变化方向
                    if(getX() > 0) {
                        moveLeft();
                    }
                    //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case 2: //向下
                for (int i = 0; i < 30; i++) { //移动30步再变化方向
                    if (getY() + 60 < 750) {
                        moveDown();
                    }
                    //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case 3: //向右
                for (int i = 0; i < 30; i++) { //移动30步再变化方向
                    if(getX() + 60 < 1000) {
                        moveRight();
                    }
                    //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                break;
        }
        setDirect((int)(Math.random() * 4));
    }

    @Override
    public void run() {
        while(true) {
            if(!isAlive) { //坦克不存在,则退出坦克线程
                break;
            }

            //Enemy自动发射子弹 --> 由shots.size()控制敌人子弹数
            shotHero();

            //Enemy坦克自动移动 --> move包含休眠
            move();
        }
    }
}

MyPanel类

    //构造器
    public MyPanel() {
       
        //初始化敌方坦克
        for (int i = 0; i < enemySize; i++) {
            //初始化敌方坦克
            Enemy enemy = new Enemy((100 * (i+1)),0,2);
            enemys.add(enemy);
            //启动敌方线程
            new Thread(enemy).start();
        }
        
        
    //绘制画布、调用绘制坦克的方法drawTank()
    @Override
    public void paint(Graphics g) {
        //画出敌人坦克
        for (int i = 0; i < enemys.size(); i++) {
            Enemy enemy = enemys.get(i);
            //如果敌人存活
            if(enemy.isAlive){
                drawTank(enemy.getX(), enemy.getY(),g,enemy.getDirect(),1);

                //画出敌人子弹
                for (int j = 0; j < enemy.shots.size(); j++) {

                    Shot shot = enemy.shots.get(j);
                    if(shot.isAlive) {
                        g.drawOval(shot.x, shot.y,2,2);
                    } else {
                        enemy.shots.remove(shot);
                    }
                }
            }
            
        } 
        
	}

4)当敌人的坦克击中我方坦克时,我方坦克消失,并出现爆炸效果

在这里插入图片描述
MyPanel类

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0,0,1000,750);//填充矩形,默认黑色

        //画出我方坦克(使用自己封装的方法)
        if(hero != null && hero.isAlive){
            drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 0);          //我方tank1
        }
    }	
    
	/**
     * 遍历每一辆坦克,再遍历每一辆坦克的每一颗子弹,判断这些子弹是否打中hero
     */
    public void hitHero() {
        for (int i = 0; i < enemys.size(); i++) {
            Enemy enemy = enemys.get(i);
            for (int j = 0; j < enemy.shots.size(); j++) {
                Shot shot = enemy.shots.get(j);
                if(shot.isAlive && enemy.isAlive) {
                    singleHit(shot , hero);
                }
            }
        }
    }
    
	/**
     * 传入一个子弹和一个坦克,判断这颗子弹是否打中坦克
     * @param shot 单科子弹
     * @param tank 单个坦克
     */
    public void singleHit(Shot shot , Tank tank) {
        switch (tank.getDirect()) {
            case 0:
            case 2: //上下
                if(shot.x >= tank.getX() && shot.x <= tank.getX() + 40
                        && shot.y >= tank.getY() && shot.y <= tank.getY() + 60) {
                    tank.setAlive(false);   //属性没有动态绑定!不能用tank.isAlive = false; --> 这样只修改了Tank类的isAlive
                    //检验属性的绑定机制
                    System.out.println("被击中,tank.isAlive=" + tank.isAlive + "  hero.isAlive=" + hero.isAlive);
                    shot.isAlive = false;
                    bombs.add(new Bomb(tank.getX(), tank.getY())); //要放在tank移除前面,不然tank会被回收
                    if(tank instanceof Enemy) {  //被击中的是敌人,则移除敌人坦克
                        enemys.remove(tank);
                    }
                }
                break;
            case 1:
            case 3: //1和3代表左和右,是同一种情况
                if(shot.x >= tank.getX() && shot.x <= tank.getX() + 60
                        && shot.y >= tank.getY() && shot.y <= tank.getY() + 40) {
                    tank.setAlive(false);
                    shot.isAlive = false;
                    bombs.add(new Bomb(tank.getX(), tank.getY()));
                    if(tank instanceof Enemy) {  //被击中的是敌人,则移除敌人坦克
                        enemys.remove(tank);
                    }
                }
                break;
        }
    }
    
	@Override
    public void run() {
        while(true) {
            //每隔50ms就重新paint一次
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.repaint();

            //判断画布上是否有其中一颗子弹打中其中一个坦克的情况
            hitEnemy();

            //监测敌人是否打中我方
            hitHero();
        }
    }

5)5.0完整版

Tank类

//Tank类
public class Tank {
    private int x;
    private int y;
    private int direct = 0 ; //坦克方向 0上 1左 2下 3右,初始化为向上
    private int speed = 3;   //坦克速度,初始化为3
    boolean isAlive = true;
    public Tank(int x, int y) {
        this.x = x;
        this.y = y;
    }

    //上下左右移动
    public void moveUp() {
        y -= speed;
    }
    public void moveRight() {
        x += speed;
    }
    public void moveDown() {
        y += speed;
    }
    public void moveLeft() {
        x -= speed;
    }

    //设置isAlive
    public void setAlive(boolean isAlive) {
        this.isAlive = isAlive;
    }

    public int getSpeed() {
        return speed;
    }

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

    public int getDirect() {
        return direct;
    }

    public void setDirect(int direct) {
        this.direct = direct;
    }

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

Hero类

public class Hero extends Tank {//自己的坦克
    boolean isAlive = true; //是否存活
    Shot shot = null;//一发子弹
    Vector<Shot> shots = new Vector<>();//多发子弹集合


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

    //设置isAlive
    public void setAlive(boolean isAlive) {
        this.isAlive = isAlive;
    }

    //MyPanel按下J键时-->调用该方法
    public void shotEnemy() {

        //面板上最多有5个子弹
        if(shots.size() == 5) {
            return;
        }

        //根据子弹方向创建shot对象
        switch (getDirect()){
            case 0:
                shot = new Shot(getX() + 19, getY(), 0);
                break;
            case 1:
                shot = new Shot(getX(), getY() + 19, 1);
                break;
            case 2:
                shot = new Shot(getX() + 19, getY() + 59, 2);
                break;
            case 3:
                shot = new Shot(getX() + 59, getY() + 19, 3);
                break;
        }

        //将shot对象加入shots集合
        shots.add(shot);
        //启动shot线程
        new Thread(shot).start();
    }
}

Enemy类

public class Enemy extends Tank implements Runnable{
    boolean isAlive = true;
    Vector<Shot> shots = new Vector<>();
    Shot shot = null;

    public Enemy(int x, int y,int direct) {
        super(x, y);
        super.setDirect(direct);
    }

    //设置isAlive
    public void setAlive(boolean isAlive) {
        this.isAlive = isAlive;
    }

    public void shotHero() {
        //Enemy自动发射子弹
        if(shots.size() < 2){ //在画布上敌人最多只能有2个子弹
            //创建一个子弹
            switch (getDirect()){
                case 0:
                    shot = new Shot(getX() + 19, getY(), 0);
                    break;
                case 1:
                    shot = new Shot(getX(), getY() + 19, 1);
                    break;
                case 2:
                    shot = new Shot(getX() + 19, getY() + 59, 2);
                    break;
                case 3:
                    shot = new Shot(getX() + 59, getY() + 19, 3);
                    break;
            }
            //添加到shot集合中
            shots.add(shot);
            //启动子弹线程
            new Thread(shot).start();
        }
    }

    public void move() {
        switch (getDirect()) {
            case 0: //向上
                for (int i = 0; i < 30; i++) { //移动30步再变化方向
                    if(getY() > 0){  //在画布范围内移动
                        moveUp();
                    }
                    //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case 1: //向左
                for (int i = 0; i < 30; i++) { //移动30步再变化方向
                    if(getX() > 0) {
                        moveLeft();
                    }
                    //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case 2: //向下
                for (int i = 0; i < 30; i++) { //移动30步再变化方向
                    if (getY() + 60 < 750) {
                        moveDown();
                    }
                    //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case 3: //向右
                for (int i = 0; i < 30; i++) { //移动30步再变化方向
                    if(getX() + 60 < 1000) {
                        moveRight();
                    }
                    //休眠:每移动一次都休眠一下,让下一次移动能够显示在重绘后的画布上(画布每50ms重绘一次)
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                break;
        }
        setDirect((int)(Math.random() * 4));
    }

    @Override
    public void run() {
        while(true) {
            if(!isAlive) { //坦克不存在,则退出坦克线程
                break;
            }

            //Enemy自动发射子弹 --> 由shots.size()控制敌人子弹数
            shotHero();

            //Enemy坦克自动移动 --> move包含休眠
            move();
        }
    }
}

Shot类

package com.rxli.tankgame5;

//把发射作为一个线程
public class Shot implements Runnable{
    int x; //子弹x坐标
    int y; //子弹y坐标
    int direct = 0; //子弹方向
    int speed = 5; //子弹速度
    boolean isAlive = true; //子弹是否存活

    public Shot(int x,int y,int direct) {
        this.x = x;
        this.y = y;
        this.direct = direct;
    }
    @Override
    public void run() { //射击功能
        while(true) {
            //休眠 --> 不休眠会导致子弹在还没有重绘画布的时候就已经到达终点了(移动很快很快)
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            switch (direct) {
                case 0:
                    y -= speed;
                    break;
                case 1:
                    x -= speed;
                    break;
                case 2:
                    y += speed;
                    break;
                case 3:
                    x += speed;
                    break;
            }
            //测试:输出子弹坐标
            //System.out.println("子弹坐标x=" + x + " y=" + y);

            //当子弹移动到边界时会销毁(线程退出)
            //当子弹碰到坦克就销毁 --> isAlive
            if(!(x >= 0 && x <= 1000 && y >= 0 && y <= 750 && isAlive)){
                isAlive = false;
                break;
            }
        }
        //System.out.println("子弹线程退出");
    }
}

Bomb类

public class Bomb {
    int x;
    int y;
    int life = 9;
    boolean isAlive = true;

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

    public void lifeDown(){
        if(life <= 0) {
            isAlive = false;
        } else {
            life--;
        }
    }
}

MyPanel类

public class MyPanel extends JPanel implements KeyListener,Runnable {
    //定义我方坦克
    Hero hero = null;
    //定义敌人坦克
    Vector<Enemy> enemys = new Vector<>(); //因为敌人多线程,所以用Vector
    //定义爆炸
    Vector<Bomb> bombs = new Vector<>();
    Image image1 = null;
    Image image2 = null;
    Image image3 = null;

    //敌人坦克个数
    int enemySize = 3;

    //构造器
    public MyPanel() {
        //初始化我方坦克
        hero = new Hero(100,100);
        hero.setSpeed(3);
        //初始化敌方坦克
        for (int i = 0; i < enemySize; i++) {
            //初始化敌方坦克
            Enemy enemy = new Enemy((100 * (i+1)),0,2);
            enemys.add(enemy);
            //启动敌方线程
            new Thread(enemy).start();
        }
        //初始化爆炸图片
        image1 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/baozha1.png"));
        image2 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/baozha2.png"));
        image3 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/baozha3.png"));
    }

    //绘制画布、调用绘制坦克的方法drawTank()
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0,0,1000,750);//填充矩形,默认黑色

        //画出我方坦克(使用自己封装的方法)
        if(hero != null && hero.isAlive){
            drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 0);          //我方tank1
        }

        //画出我方子弹
        for (int i = 0; i < hero.shots.size(); i++) {
            Shot shot = hero.shots.get(i);
            //创建了子弹对象并且子弹对象还存活才能画
            if(shot.isAlive){
                g.setColor(Color.cyan);
                g.drawOval(shot.x, shot.y , 2,2 );
            } else {
                hero.shots.remove(shot);
            }
        }


        //画出敌人坦克
        for (int i = 0; i < enemys.size(); i++) {
            Enemy enemy = enemys.get(i);
            if(enemy.isAlive){
                drawTank(enemy.getX(), enemy.getY(),g,enemy.getDirect(),1);

                //画出敌人子弹
                for (int j = 0; j < enemy.shots.size(); j++) {

                    Shot shot = enemy.shots.get(j);
                    if(shot.isAlive) {
                        g.drawOval(shot.x, shot.y,2,2);
                    } else {
                        enemy.shots.remove(shot);
                    }
                }
            }
        }

        //画爆炸
        for (int i = 0; i < bombs.size(); i++) {
            //不休眠第一个会爆炸不出来,取图片太慢了应该
            try {
                Thread.sleep(55);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Bomb bomb = bombs.get(i);
            if(bomb.isAlive) {
                if(bomb.life > 6) {
                    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 if(bomb.life > 0){
                    g.drawImage(image3,bomb.x,bomb.y,60,60,this);
                }
                bomb.lifeDown();
                if(bomb.life == 0) {
                    bombs.remove(bomb);
                }
            }
        }


    }

    /**
     *
     * @param x 坦克的左上角x坐标
     * @param y 坦克的左上角y坐标
     * @param g 画笔
     * @param direct 坦克方向(上下左右)
     * @param type 坦克类型(自己、敌人。。。)
     */
    //编写方法,画出tank
    public void drawTank(int x, int y, Graphics g, int direct, int type) {

        switch (type) {
            case 0://我们的坦克
                g.setColor(Color.cyan);
                break;
            case 1://敌人的坦克
                g.setColor(Color.yellow);
                break;
        }

        //根据tank方向,来绘制对应形状的tank(朝哪边走,头就朝向哪边)
        //0、1、2、3:分别代表向上、向下、向左、向右
        switch (direct) {
            case 0://向上
                g.fill3DRect(x,y,10,60,false);//画出tank左边的轮子
                g.fill3DRect(x+30,y,10,60,false);//画出tank右边的轮子
                g.fill3DRect(x+10, y+10, 20, 40, false); //画出tank中间的长方形炮体
                g.fillOval(x+10, y+20, 20, 20); //画出tank中间的圆台
                g.drawLine(x+19, y+30, x+19,y);
                g.drawLine(x+20, y+30, x+20,y);
                g.drawLine(x+21, y+30, x+21,y);
                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 + 19, x, y + 19);//画出炮筒
                g.drawLine(x + 30, y + 20, x, y + 20);//画出炮筒
                g.drawLine(x + 30, y + 21, x, y + 21);//画出炮筒
                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 + 19, y + 30, x + 19, y + 60);//画出炮筒
                g.drawLine(x + 20, y + 30, x + 20, y + 60);//变粗
                g.drawLine(x + 21, y + 30, x + 21, 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 + 19, x + 60, y + 19);//画出炮筒
                g.drawLine(x + 30, y + 20, x + 60, y + 20);//变粗
                g.drawLine(x + 30, y + 21, x + 60, y + 21);//变粗
                break;


            default:
                System.out.println("暂时没有处理");

        }
    }

    /**
     * 遍历每一辆坦克,再遍历每一辆坦克的每一颗子弹,判断这些子弹是否打中hero
     */
    public void hitHero() {
        for (int i = 0; i < enemys.size(); i++) {
            Enemy enemy = enemys.get(i);
            for (int j = 0; j < enemy.shots.size(); j++) {
                Shot shot = enemy.shots.get(j);
                if(shot.isAlive && enemy.isAlive) {
                    singleHit(shot , hero);
                }
            }
        }
    }

    /**
     * 遍历所有子弹,再遍历所有坦克,判断是否打中
     */
    public void hitEnemy() {
        for (int i = 0; i < hero.shots.size(); i++) {
            Shot shot = hero.shots.get(i);
            if(shot != null && shot.isAlive){
                for (int j = 0; j < enemys.size(); j++) {
                    singleHit(shot, enemys.get(j));
                }
            }
        }
    }
    /**
     * 传入一个子弹和一个坦克,判断这颗子弹是否打中坦克
     * @param shot 单科子弹
     * @param tank 单个坦克
     */
    public void singleHit(Shot shot , Tank tank) {
        switch (tank.getDirect()) {
            case 0:
            case 2: //上下
                if(shot.x >= tank.getX() && shot.x <= tank.getX() + 40
                        && shot.y >= tank.getY() && shot.y <= tank.getY() + 60) {
                    tank.setAlive(false);   //属性没有动态绑定!不能用tank.isAlive = false; --> 这样只修改了Tank类的isAlive
                    //检验属性的绑定机制
                    System.out.println("被击中,tank.isAlive=" + tank.isAlive + "  hero.isAlive=" + hero.isAlive);
                    shot.isAlive = false;
                    bombs.add(new Bomb(tank.getX(), tank.getY())); //要放在tank移除前面,不然tank会被回收
                    if(tank instanceof Enemy) {  //被击中的是敌人,则移除敌人坦克
                        enemys.remove(tank);
                    }
                }
                break;
            case 1:
            case 3: //1和3代表左和右,是同一种情况
                if(shot.x >= tank.getX() && shot.x <= tank.getX() + 60
                        && shot.y >= tank.getY() && shot.y <= tank.getY() + 40) {
                    tank.setAlive(false);
                    shot.isAlive = false;
                    bombs.add(new Bomb(tank.getX(), tank.getY()));
                    if(tank instanceof Enemy) {  //被击中的是敌人,则移除敌人坦克
                        enemys.remove(tank);
                    }
                }
                break;
        }
    }

    @Override
    public void keyTyped(KeyEvent e) {

    }

    @Override
    public void keyPressed(KeyEvent e) {
        if(e.getKeyCode() == KeyEvent.VK_W) { //向上
            //改变我方tank1方向
            hero.setDirect(0);
            if(hero.getY() > 0) { //控制移动范围
                hero.moveUp();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_A){//向左
            hero.setDirect(1);
            if(hero.getX() > 0){
                hero.moveLeft();
            }
        } else if(e.getKeyCode() == KeyEvent.VK_S){//向下
            hero.setDirect(2);
            if(hero.getY() + 60 < 750) {
                hero.moveDown();
            }
        }  else if(e.getKeyCode() == KeyEvent.VK_D){//向右
            hero.setDirect(3);
            if(hero.getX() + 60 < 1000) {
                hero.moveRight();
            }
        }

        if(e.getKeyCode() == KeyEvent.VK_J) {
//            //发射一颗子弹
//            if(hero.shot == null || !hero.shot.isAlive){
//                //创建hero.shot线程,并启动
//                hero.shotEnemy();
//            }
            hero.shotEnemy();
        }
        //让面板重绘
        this.repaint();
    }

    @Override
    public void keyReleased(KeyEvent e) {

    }

    //为了让MyPanel不停地重绘,需要将MyPanel变成一个线程,每隔一段时间就自己重绘
    @Override
    public void run() {
        while(true) {
            //每隔50ms就重新paint一次
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.repaint();

            //判断画布上是否有其中一颗子弹打中其中一个坦克的情况
            hitEnemy();

            //监测敌人是否打中我方
            hitHero();
        }
    }
}

TankGame05类

public class TankGame05 extends JFrame {
    //定义MyPanel
    MyPanel mp = null;
    public static void main(String[] args) {
        TankGame05 tankGame01 = new TankGame05();
    }

    public TankGame05() {
        mp = new MyPanel(); //画板,就是游戏区域
        new Thread(mp).start(); //启动mp线程,不停重绘
        this.add(mp);
        this.setSize(1200,800); //Jrame框比MyPanel画板稍大一点
        this.addKeyListener(mp);//让JFrame 监听 mp事件
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值