总体内容
- 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面板添加键盘监听器
}
}
输出效果: