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


总体内容

  • 2.0版本包括:实现我方坦克发射子弹的过程
  • 本文中项目完成包括:
    ①增加一个子弹发射的线程类
    ②在我方坦克类中创建+启动发射线程的对象
    ③监听"J"键+绘制子弹+子弹连续显示

项目:我方发射子弹

需求:当用户按下J键,我方坦克就发射一颗子弹

思路

在这里插入图片描述


实现步骤

1. 控制子弹发射的线程

shoot类
实现Thread类,成为一个线程类
② 分析包括哪些属性
构造器对哪些属性进行初始化
run() 方法中:根据子弹方向对子弹坐标进行更改且当子弹碰到墙壁时,线程结束
⑤ 具体实现的分析 看代码里面的注解吧

package com.wpz.tankgame;

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

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

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


2. 创建+启动线程的方法

MyTank类
① 要明白为什么要把创建+启动线程的方法写到MyTank类中=>用户控制射击是我方坦克专有的
定义线程类的对象=>关联线程类,就可以通过我方坦克访问到子弹线程的信息了
④ 写一个射击的方法,功能包括:创建+启动线程
创建线程时:要通过我方坦克的方向和位置 来确定 子弹的方向和位置
⑥ 具体实现的分析 看代码里面的注解吧

package com.wpz.tankgame;

/**
 * @author 王胖子
 * @version 1.0
 * 我的坦克
 * 1. 动机描述:用户按下J键,就发射子弹开始射击
 * 2. 为什么要把创建线程写到MyTank类中:因为射击是MyTank专有的,敌人的坦克没有,
 * 所以就在这个类中,①定义子弹发射的线程 ②创建线程 ③为线程中子弹的x,y,direction赋值 ④启动线程
 */
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();
    }
}


3. 监听"J"+绘制子弹+子弹连续显示

MyPanel类,TankGame类
监听"J":在keyPressed()方法中修改,如果按下J键,就调用我方坦克发射子弹的方法
绘制子弹: 在paint()中修改,如果发射子弹的线程非空并且子弹存在,就绘制子弹,注意一定要验证线程是否为空,如果只判断子弹是否存在,那会报空指针异常
子弹连续显示: MyPanel类实现Runnable接口,在run()中每隔100秒就重绘面板。因为按一次J,KeyPressed()被调用一次,里面的重绘也只调用一次,那么虽然子弹的坐标一直在动,但是面板上不会显示子弹连续变化,所以想到用多线程来解决
④ 在TankGame的构造器中,启动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;

/**
 * @author 王胖子
 * @version 1.0
 * 坦克大战的绘图区域
 * 1. 在构造器中:初始化坦克(我方+敌方)
 * 2. 在paint()中:为该面板绘图
 * 3. 在keyPressed()中:对键盘按键进行处理(移动+发射子弹+重绘面板)
 */
//为了让面板不停重绘子弹,让面板实现Runnable接口,当作一个线程使用(在run()中写重绘)
public class MyPanel extends JPanel implements KeyListener, Runnable {
    MyTank myTank = null;//定义一个自己的坦克
    Vector<EnemyTank> enemyTanks = new Vector<>();//定义敌人的坦克,放到Vector中
    int enemyTankSize = 3;//敌人坦克的数量

    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);
            //加入
            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);
        }
        //画我方坦克的子弹
        if (myTank.shoot != null && myTank.shoot.isLive) {
            g.setColor(Color.white);
            g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);
        }
    }

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

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

package com.wpz.tankgame;

import javax.swing.*;

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

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

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

输出效果:
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值