第18章:坦克大战2.2
总体内容(增加的功能)
1. 我方坦克发射一颗子弹
需求:我方坦克在发射的子弹消亡后,才能继续发射新子弹
思路
在按下J键时,判断我方坦克对象的子弹,是否已经消亡。
若已经消亡,才调用shootEnemyTank():这个方法的作用是创建+启动子弹线程
实现
Mypanel类:
在MyPanel类的keyPress()中修改,按下J键时的处理方式
//按下J键时,我方坦克发射子弹
if (e.getKeyCode() == KeyEvent.VK_J) {
//- 出现的问题:
//-- 按一下J,只会调用一次keyPressed(),那面板只会重绘一次
//-- 因此按一下J,只能看到一个不会动小球
//-- 解决方法:让面板每隔100ms,自动重绘=>用多线程(面板实现Runnable接口)
//- 实现:当子弹消亡时,才会发射新子弹
//-- 方法:当子弹线程为空 或 子弹消亡时,调用shootEnemyTank()
if (myTank.shoot==null||!(myTank.shoot.isLive)) {
myTank.shootEnemyTank();
}
}
2. 我方坦克发射多颗子弹
需求:我方坦克可以发射多颗子弹(在面板上最多5颗)
思路
- 在MyTank类中增加一个子弹集合:子弹集合存放我方坦克发射的多发子弹。每按一下J都会调用shootEnemyTank()来创建一个子弹线程加入子弹集合,并启动它
- 画出我方坦克发射的多发子弹:点击J,创建启动子弹线程后,要画出这颗子弹,那就在paint()中循环遍历shoots集合画出,如果存在就画出,不存在就从集合中移除
- 判断每颗子弹是否击中:循环遍历shoots集合中的每颗子弹,和每个敌方坦克做判断。=>两次遍历
实现
MyTank类和MyPanel类
- 在MyTank类中:①增加一个属性:shoots集合(存放多颗子弹) ②shootEnemyTank()中,把新创建的子弹 加入集合 ③shootEnemyTank()中,控制只能有5颗子弹
- 在MyPanel类中:①画子弹:循环子弹集合画出存活的子弹,不存活的子弹被移除 ②击中坦克:遍历子弹集合,遍历敌方坦克集合,判断每颗子弹是否击中敌方的某个坦克
package com.wpz.tankgame2_2;
import java.util.Vector;
/**
* @author 王胖子
* @version 1.0
* 我的坦克
* 1. 动机描述:用户按下J键,就发射子弹开始射击
* 2. 为什么要把创建线程写到MyTank类中:因为射击是MyTank专有的,敌人的坦克没有,
* 所以就在这个类中,①定义子弹发射的线程 ②创建线程 ③为线程中子弹的x,y,direction赋值 ④启动线程
*/
public class MyTank extends Tank {
// Shoot shoot;//子弹发射的线程=>一颗子弹
Vector<Shoot> shoots = new Vector<>();//存放多颗子弹的集合
public MyTank(int x, int y) {
super(x, y);
}
public void shootEnemyTank() {//射击
//控制只能有5颗子弹=>5颗时不再创建子弹线程并退出该方法
if (shoots.size() == 5) {
return;
}
//创建线程:根据坦克的位置和方向 来创建 子弹射击的线程
Shoot shoot = null;
// - 判断坦克的方向,来创建线程
if (getDirection() == 0) {//上
shoot = new Shoot(getX() + 20, getY(), 0);
} else if (getDirection() == 1) {//右
shoot = new Shoot(getX() + 60, getY() + 20, 1);
} else if (getDirection() == 2) {//下
shoot = new Shoot(getX() + 20, getY() + 60, 2);
} else if (getDirection() == 3) {//左
shoot = new Shoot(getX(), getY() + 20, 3);
}
shoots.add(shoot);//- 把子弹加入集合
//- 启动线程
shoot.start();
}
}
MyPanel中paint() 画子弹的代码修改
//画我方坦克的子弹=>如果子弹线程非空且子弹存在
// if (myTank.shoot != null && myTank.shoot.isLive) {//单颗子弹
// g.setColor(Color.white);
// g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);
// }
for (int i = 0; i < myTank.shoots.size(); i++) {//多颗子弹
//- 取出
Shoot shoot = myTank.shoots.get(i);
//- 如果存在就画出子弹,不存在就从集合中移除
if (shoot.isLive) {
g.setColor(Color.white);
g.fillOval(shoot.x, shoot.y, 2, 2);
} else {
myTank.shoots.remove(shoot);
}
}
MyPanel中 击中敌人坦克的代码修改
①新建一个方法hitEnemyTank()
public void hitEnemyTank() {//我方坦克发射多颗子弹:遍历每颗子弹判断是否击中
for (int i = 0; i < myTank.shoots.size(); i++) {
//取出子弹
Shoot shoot = myTank.shoots.get(i);
//判断该颗子弹是否击中坦克=>遍历坦克
for (int j = 0; j < enemyTanks.size(); j++) {
EnemyTank enemyTank = enemyTanks.get(j);
hitTank(shoot, enemyTank);
}
}
}
②在run()中调用该方法
//在run中写
hitEnemyTank();//多颗子弹
③按下J键时,不用判断子弹是否存活才发射=>KeyPress()中
//按下J键时,我方坦克发射子弹
if (e.getKeyCode() == KeyEvent.VK_J) {
//- 出现的问题:
//-- 按一下J,只会调用一次keyPressed(),那面板只会重绘一次
//-- 因此按一下J,只能看到一个不会动小球
//-- 解决方法:让面板每隔100ms,自动重绘=>用多线程(面板实现Runnable接口)
//- 实现:当子弹消亡时,才会发射新子弹
//-- 方法:当子弹线程为空 或 子弹消亡时,调用shootEnemyTank()
// if (myTank.shoot==null||!(myTank.shoot.isLive)) {//发射单颗(销毁了才会发第二颗)
// myTank.shootEnemyTank();
// }
myTank.shootEnemyTank();//发射多颗子弹:无需判断
}
效果图
3. 敌方移动发射
需求:让敌人坦克发射的子弹消亡后,才可以再发射子弹
扩展:敌人坦克可以发多颗子弹
实现
EnemyTank类
- EnemyTank类的run()方法中:添加判断,如果该敌人坦克仍存在且子弹集合为空,就创建+启动一个子弹并加入子弹集合
- 扩展:发射多颗子弹=>在run方法中判断,敌人坦克仍存在且子弹集合中的子弹个数小于5,就继续创建+启动子弹并加入子弹集合
- 想法:一开始想到在画子弹的地方:当子弹消失就移除,并再创建一个子弹,但是创建过程得根据坦克方向来确定子弹的位置,代码太多,且维护性差,所以就在EnemyTank类中书写了
package com.wpz.tankgame2_2;
import java.util.Vector;
/**
* @author 王胖子
* @version 1.0
* 敌人的坦克
* 1. 动机:让敌人坦克可以自由随机移动
* 2. 方法:把敌人坦克类当作线程使用,使得坦克可以根据run()方法的业务代码来运行
*/
public class EnemyTank extends Tank implements Runnable {
//千万注意要初始化这个Vector...不然会报空指针
Vector<Shoot> shoots = new Vector();//一个敌方坦克可以发射多颗子弹,所以这里用集合
boolean isLive = true;//敌人坦克是否存在
public EnemyTank(int x, int y) {
super(x, y);
//每创建一个敌方坦克都分配一个子弹线程并启动=>所以把(创建+启动)线程 写到初始化敌人坦克里面写
// 不能在这个构造器中创建线程,因为初始化EnemyTank时,只给x,y赋值了,没给direction赋值,
// 而初始化子弹线程时,需要EnemyTank的方向,如果在这个类中创建线程,会报空指针异常
//也就是下面这两句:
//EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);
//shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());
}
@Override
public void run() {
while (true) {
//如果该敌人坦克仍存活着且子弹集合为空,就创建+启动一个新子弹线程并加入子弹集合
//如果想让坦克发多颗子弹(最多5颗):if (isLive && shoots.size() < 5)=>不够5颗的话 就继续创建子弹加入子弹集合
if (isLive && shoots.size() == 0) {
Shoot s = null;
switch (getDirection()) {//根据坦克方向定子弹位置 来创建子弹
case 0://上
s = new Shoot(getX() + 20, getY(), 0);
break;
case 1://右
s = new Shoot(getX() + 60, getY() + 20, 1);
break;
case 2://下
s = new Shoot(getX() + 20, getY() + 60, 2);
break;
case 3://左
s = new Shoot(getX(), getY() + 20, 3);
break;
}
new Thread(s).start();//启动
shoots.add(s);//加入
}
/*
写多线程时要考虑三个问题:①处理什么业务逻辑 ②什么时候被创建+启动 ③什么时候结束
敌人坦克这个线程中:①业务:敌人坦克朝当前方向走+改变方向
②结束:敌人坦克消失时,线程结束
③创建+启动:敌人坦克被初始化完成后就可以使用这个线程了
*/
//思路:先向坦克的当前方向走30步=>然后转变方向
//- 向当前方向移动30步
switch (getDirection()) {
case 0://上
for (int i = 0; i < 30; i++) {
moveUp();//使用父类中移动的方法
try {
Thread.sleep(50);//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case 1://右
for (int i = 0; i < 30; i++) {
moveRight();
try {
Thread.sleep(50);//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case 2://下
for (int i = 0; i < 30; i++) {
moveDown();
try {
Thread.sleep(50);//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case 3://左
for (int i = 0; i < 30; i++) {
moveLeft();
try {
Thread.sleep(50);//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
}
//- 转变方向(方向为0-3随机)
setDirection((int) (Math.random() * 4));
//- 注意!写并发程序,一定要考虑清楚,该线程什么时候结束
//-- 当坦克不再存在时,这个线程就结束了
if (!(isLive)) {
break;//退出线程
}
}
}
}
4. 我方被击中爆炸
需求:当敌人坦克击中我方坦克时,我方坦克消失,并出现爆炸效果。
想法
- 在MyPanel的run()中,判断敌人坦克是否击中我方坦克
- 所以另外写一个方法hitMyTank():方法内遍历每个敌人坦克+遍历每个敌人坦克的每颗子弹,调用hitTank()方法进行判断
- 但是发现一个问题:
hitTank(s,myTank);
出现问题,因为hitTank()的参数是EnemyTankpublic void hitTank(Shoot s, EnemyTank enemyTank)
,所以把EnemyTank改成他们的父类Tank
- 但是又有一个错误:Tank不能访问isLive,所以给Tank中增加一个
isLive
属性,默认为true,再把敌人坦克类中的isLive删除 - 画我方坦克:在我方坦克存在时画
- 我方坦克发射子弹:增加条件,在我方坦克存在时才能创建子弹线程
- 在MyPanel的run()中,调用hitMyTank()
- 可以把打敌人坦克的代码放到hitEnemyTank()方法中,然后在run()中调用
实现
上面的想法写的意思差不多了,这里不再阐述
在MyPanel类中:
① 增加hitMyTank()
public void hitMyTank(){
for (int i = 0; i < enemyTanks.size(); i++) {//判断每个坦克的每颗子弹 是否击中我方坦克
EnemyTank enemyTank = enemyTanks.get(i);//取出敌人坦克
for (int j = 0; j < enemyTank.shoots.size(); j++) {
Shoot s = enemyTank.shoots.get(j);//取出敌人子弹
//如果我方坦克存在 且 该颗子弹存在 就判断子弹是否集中我方坦克
if (myTank.isLive&&s.isLive){
hitTank(s,myTank);
}
}
}
}
②修改hitTank
public void hitTank(Shoot s, Tank tank) {
// - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向
switch (tank.getDirection()) {
case 0://坦克向上
case 2://坦克向下
if (s.x > tank.getX() && s.x < tank.getX() + 40 &&
s.y > tank.getY() && s.y < tank.getY() + 60) {
s.isLive = false;//子弹消失
tank.isLive = false;//敌人坦克消失
enemyTanks.remove(tank);//从集合中移除被击中的坦克
//击中时,创建bomb对象,放入bombs集合中
Bomb bomb = new Bomb(tank.getX(), tank.getY());
bombs.add(bomb);
}
case 1://坦克向右
case 3://坦克向左
if (s.x > tank.getX() && s.x < tank.getX() + 60 &&
s.y > tank.getY() && s.y < tank.getY() + 40) {
s.isLive = false;//子弹消失
tank.isLive = false;//敌人坦克消失
enemyTanks.remove(tank);//从集合中移除被击中的坦克
//击中时,创建bomb对象,放入bombs集合中
Bomb bomb = new Bomb(tank.getX(), tank.getY());
bombs.add(bomb);
}
}
}
③在paint()方法中,画我方坦克时增加判断
if (myTank!=null&&myTank.isLive) {
drawTank(myTank.getX(), myTank.getY(), g, myTank.getDirection(), 0);//画出我的坦克
}
④ run()中调用
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//- 判断是否击中敌人坦克
hitEnemyTank();
// hitEnemyTank();//多颗子弹
// - 判断敌方坦克是否击中我方坦克
hitMyTank();
this.repaint();
}
}
⑤在Keypress()的J键处理中:
if (myTank.isLive&&myTank.shoot==null||!(myTank.shoot.isLive)) {//发射单颗(销毁了才会发第二颗)
myTank.shootEnemyTank();
}
在Tank类中:
⑥增加属性
boolean isLive = true;
版本2.2的全部代码
Tank类
package com.wpz.tankgame2_2;
/**
* @author 王胖子
* @version 1.0
* 因为该游戏会有很多坦克
* 所以先抽象成一个父类
*/
public class Tank {
private int x;//坦克横坐标
private int y;//坦克纵坐标
//将坦克的方向写到父类中 这样任意坦克在不同方法中 都可以设置移动方向
private int direction;//坦克的方向(0:上,1:右,2:下,3:左)-->默认是0
private int speed = 2;//移动速度
boolean isLive = true;
public Tank(int x, int y) {
this.x = x;
this.y = y;
}
public int getDirection() {
return direction;
}
public void setDirection(int direction) {
this.direction = direction;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getSpeed() {
return speed;
}
public void setSpeed(int speed) {
this.speed = speed;
}
//将坦克移动时的坐标改变封装到 父类的方法中
//直接改变坐标的x/y值(取代myTank.setY(myTank.getY()-2);)
//先死后活:开始时坐标是加减固定值-->可以扩展成变化的--引入变量-->speed(可以在初始化坦克对象时赋值)
//- 让坦克(我方+敌方)都在规定的范围内(窗口内)移动
//-- 思路:先判断一下坦克当前的坐标是否超出边界,若没有超,就让坦克移动
public void moveUp() {
//判断坦克是否超出边界:若没超就移动
if (getY() > 0) {
y -= speed;
}
}
public void moveRight() {
if (getX() + 60 < 1000) {//本来是<1000的,如果坦克会超出一点边界就改成980
x += speed;
}
}
public void moveDown() {
if (getY() + 60 < 750) {//本来是750,如果会超改为710,把窗口放大就不会超了
y += speed;
}
}
public void moveLeft() {
if (getX() > 0) {
x -= speed;
}
}
}
MyTank类
package com.wpz.tankgame2_2;
import java.util.Vector;
/**
* @author 王胖子
* @version 1.0
* 我的坦克
* 1. 动机描述:用户按下J键,就发射子弹开始射击
* 2. 为什么要把创建线程写到MyTank类中:因为射击是MyTank专有的,敌人的坦克没有,
* 所以就在这个类中,①定义子弹发射的线程 ②创建线程 ③为线程中子弹的x,y,direction赋值 ④启动线程
*/
public class MyTank extends Tank {
Shoot shoot;//子弹发射的线程=>一颗子弹
// Vector<Shoot> shoots = new Vector<>();//存放多颗子弹的集合
public MyTank(int x, int y) {
super(x, y);
}
public void shootEnemyTank() {//射击
//控制只能有5颗子弹=>5颗时不再创建子弹线程并退出该方法
// if (shoots.size() == 5) {
// return;
// }
//创建线程:根据坦克的位置和方向 来创建 子弹射击的线程
// Shoot shoot = null;
// - 判断坦克的方向,来创建线程
if (getDirection() == 0) {//上
shoot = new Shoot(getX() + 20, getY(), 0);
} else if (getDirection() == 1) {//右
shoot = new Shoot(getX() + 60, getY() + 20, 1);
} else if (getDirection() == 2) {//下
shoot = new Shoot(getX() + 20, getY() + 60, 2);
} else if (getDirection() == 3) {//左
shoot = new Shoot(getX(), getY() + 20, 3);
}
// shoots.add(shoot);//- 把子弹加入集合
//- 启动线程
shoot.start();
}
}
EnemyTank类
package com.wpz.tankgame2_2;
import java.util.Vector;
/**
* @author 王胖子
* @version 1.0
* 敌人的坦克
* 1. 动机:让敌人坦克可以自由随机移动
* 2. 方法:把敌人坦克类当作线程使用,使得坦克可以根据run()方法的业务代码来运行
*/
public class EnemyTank extends Tank implements Runnable {
//千万注意要初始化这个Vector...不然会报空指针
Vector<Shoot> shoots = new Vector();//一个敌方坦克可以发射多颗子弹,所以这里用集合
// boolean isLive = true;//敌人坦克是否存在
public EnemyTank(int x, int y) {
super(x, y);
//每创建一个敌方坦克都分配一个子弹线程并启动=>所以把(创建+启动)线程 写到初始化敌人坦克里面写
// 不能在这个构造器中创建线程,因为初始化EnemyTank时,只给x,y赋值了,没给direction赋值,
// 而初始化子弹线程时,需要EnemyTank的方向,如果在这个类中创建线程,会报空指针异常
//也就是下面这两句:
//EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);
//shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());
}
@Override
public void run() {
while (true) {
//如果该敌人坦克仍存活着且子弹集合为空,就创建+启动一个新子弹线程并加入子弹集合
//如果想让坦克发多颗子弹(最多5颗):if (isLive && shoots.size() < 5)=>不够5颗的话 就继续创建子弹加入子弹集合
if (isLive && shoots.size() == 0) {
Shoot s = null;
switch (getDirection()) {//根据坦克方向定子弹位置 来创建子弹
case 0://上
s = new Shoot(getX() + 20, getY(), 0);
break;
case 1://右
s = new Shoot(getX() + 60, getY() + 20, 1);
break;
case 2://下
s = new Shoot(getX() + 20, getY() + 60, 2);
break;
case 3://左
s = new Shoot(getX(), getY() + 20, 3);
break;
}
new Thread(s).start();//启动
shoots.add(s);//加入
}
/*
写多线程时要考虑三个问题:①处理什么业务逻辑 ②什么时候被创建+启动 ③什么时候结束
敌人坦克这个线程中:①业务:敌人坦克朝当前方向走+改变方向
②结束:敌人坦克消失时,线程结束
③创建+启动:敌人坦克被初始化完成后就可以使用这个线程了
*/
//思路:先向坦克的当前方向走30步=>然后转变方向
//- 向当前方向移动30步
switch (getDirection()) {
case 0://上
for (int i = 0; i < 30; i++) {
moveUp();//使用父类中移动的方法
try {
Thread.sleep(50);//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case 1://右
for (int i = 0; i < 30; i++) {
moveRight();
try {
Thread.sleep(50);//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case 2://下
for (int i = 0; i < 30; i++) {
moveDown();
try {
Thread.sleep(50);//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case 3://左
for (int i = 0; i < 30; i++) {
moveLeft();
try {
Thread.sleep(50);//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
}
//- 转变方向(方向为0-3随机)
setDirection((int) (Math.random() * 4));
//- 注意!写并发程序,一定要考虑清楚,该线程什么时候结束
//-- 当坦克不再存在时,这个线程就结束了
if (!(isLive)) {
break;//退出线程
}
}
}
}
Bomb类(爆炸效果)
package com.wpz.tankgame2_2;
/**
* @author 王胖子
* @version 1.0
* 实现子弹爆炸效果的类=>炸弹类(爆炸类)
* 1. 动机:当击中坦克时,出现爆炸效果
* 2. 解决方法的概括:在被击中的坦克 的位置上逐个画出三张爆炸效果的图
* 3. 具体实现:写一个爆炸类:一个爆炸类可以控制三张图片的显示
* ①控制图片位置:根据击中坦克的位置确定爆炸效果图片的位置
* ②控制图片的出现时间:根据不同的爆炸时期,画出不同的爆炸效果=>爆炸效果总时长为9,时长分三段显示三张图片
* ③控制画完的图片消失:如果显示时间减为0,就把这个爆炸类对象从bombs集合中移除
* 综上该类需要的属性:①图片显示的位置xy ②控制图片逐个显示+显示的时间:life
* ③显示时间为0时,就让图片都不再绘制且把这个类从集合中移除:isLive
*/
public class Bomb {
int x, y;//爆炸效果图片的坐标
int life = 9;//爆炸效果的生命周期
boolean isLive = true;//是否存在(生命周期是否结束(<=0时结束))
public Bomb(int x, int y) {
this.x = x;
this.y = y;
}
//减少生命值,配合出现图片的爆炸效果,
//- 当生命值减少为0时,爆炸效果显示完毕,控制让炸弹不再显示isLive=false(在paint()中控制)
public void lifeDown() {
if (life > 0) {
life--;
} else {
isLive = false;//
}
}
}
Shoot类(子弹类)
package com.wpz.tankgame2_2;
/**
* @author 王胖子
* @version 1.0
* 控制子弹发射的线程
* 1. 动机描述:如果用户按下J键,则启动该线程开始发射子弹,若子弹碰壁则结束线程
* 2. 启动线程时得明确:①子弹在哪(根据坦克的位置) ②子弹往哪打(根据坦克的方向)
* ③什么时候绘制子弹(根据子弹是否存在=>线程创建子弹就存在了,一直到碰壁/敌人坦克时销毁)
* ④子弹发射的速度
* 3. 根据以上四条,这个线程中需要的属性有:横纵坐标,子弹方向,子弹是否存在,子弹发射的速度
*/
public class Shoot extends Thread {
int x;//子弹的横坐标
int y;//子弹的纵坐标
int direction;//子弹的方向
int speed = 4;//子弹发射的速度
boolean isLive = true;//子弹是否存在=>线程创建后子弹就存在,所以默认为true
//构造器:需要这三个属性是因为 这几个需要根据我方坦克的x,y和direction来定
public Shoot(int x, int y, int direction) {
this.x = x;
this.y = y;
this.direction = direction;
}
@Override
public void run() {
//该线程的任务是:
// ①控制子弹的移动(根据子弹的方向去移动)
// ②控制子弹是否存在
while (true) {
try {
Thread.sleep(50);//休眠一下,不然子弹一下子就打到墙上了
} catch (InterruptedException e) {
e.printStackTrace();
}
// - 根据方向改变x,y坐标
if (direction == 0) {//上
y -= speed;
} else if (direction == 1) {//右
x += speed;
} else if (direction == 2) {//下
y += speed;
} else if (direction == 3) {//左
x -= speed;
}
// System.out.println("x = " + x + " y = " + y);//测试
//- 如果碰到墙壁,则线程结束(break),子弹消失(置false,面板中不绘制子弹)
//- 解决子弹打到坦克时,线程还未退出的问题:判断的条件增加isLive
//把正确条件取反 就得到了它反面的条件(正确条件为:没碰到墙壁且子弹存在)
if (!(x >= 0 && x <= 1000 && y >= 0 && y <= 750 && isLive)) {
isLive = false;//子弹消失
System.out.println(Thread.currentThread().getName()+"线程结束");
break;//线程结束
}
}
}
}
MyPanel类(面板类)
package com.wpz.tankgame2_2;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;
/**
* @author 王胖子
* @version 1.0
* 坦克大战的绘图区域
* 1. 在构造器中:初始化坦克(我方+敌方)
* 2. 在paint()中:为该面板绘图
* 3. 在keyPressed()中:对键盘按键进行处理(移动+发射子弹+重绘面板)
*/
//为了让面板不停重绘子弹,让面板实现Runnable接口,当作一个线程使用(在run()中写重绘)
public class MyPanel extends JPanel implements KeyListener, Runnable {
MyTank myTank = null;//定义一个自己的坦克
Vector<EnemyTank> enemyTanks = new Vector<>();//定义敌人的坦克,放到Vector中
int enemyTankSize = 3;//敌人坦克的数量
//定义炸弹,放到Vector中=>炸弹爆炸不属于坦克属性,属于面板的属性
//- 当子弹击中坦克时,加入一个Bomb对象到bombs:被击中的每个坦克都有一个Bomb对象,加入集合中统一管理
Vector<Bomb> bombs = new Vector<>();
//定义三张炸弹爆炸的图片,用于显示炸弹爆炸效果
Image image1 = null;
Image image2 = null;
Image image3 = null;
public MyPanel() {
myTank = new MyTank(500, 100);//初始化自己的坦克
myTank.setSpeed(5);//设置坦克移动的速度
//初始化敌人的坦克(注意:使用循环来添加。因为敌人坦克数量多,不要一个一个add)
for (int i = 0; i < enemyTankSize; i++) {
// - 创建一个敌人的坦克
EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);
// - 设置方向
enemyTank.setDirection(2);
// - 创建敌方坦克时,就为这个坦克(创建+启动)一个子弹线程,并把这个子弹线程添加到自己的子弹集合中
Shoot shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());
shoot.start();
enemyTank.shoots.add(shoot);
// - 启动敌人坦克线程,让坦克动起来
new Thread(enemyTank).start();
// - 加入
enemyTanks.add(enemyTank);
}
//初始化三个爆炸的图片对象
image1 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_1.gif"));
image2 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_2.gif"));
image3 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_3.gif"));
}
@Override
public void paint(Graphics g) {
super.paint(g);
//绘图区域:填充矩形,默认是黑色
g.fillRect(0, 0, 1000, 750);
//画出自己的坦克->封装到画坦克的方法中
// - 对direction做了修改,把它放到了tank父类中(使用get()方法访问)
if (myTank != null && myTank.isLive) {
drawTank(myTank.getX(), myTank.getY(), g, myTank.getDirection(), 0);//画出我的坦克
}
//画敌人的坦克和子弹->遍历集合
for (int i = 0; i < enemyTanks.size(); i++) {
// - 取出坦克
EnemyTank enemyTank = enemyTanks.get(i);
// - 判断坦克是否存在,存在时才画这个坦克和它的子弹
if (enemyTank.isLive) {
drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirection(), 1);
//画敌方坦克的子弹 => 画出敌方坦克时,也画出每个坦克的(多颗)子弹(遍历每个坦克的子弹集合)
for (int j = 0; j < enemyTank.shoots.size(); j++) {
// - 取出子弹
Shoot shoot = enemyTank.shoots.get(j);
if (shoot.isLive) {// - 如果子弹存在就画
g.setColor(Color.white);
g.fillOval(shoot.x, shoot.y, 2, 2);
} else {
// - 如果子弹不存在,就把该子弹从Vector中移除
// -- 按照每个敌方坦克等到一颗子弹销毁后,才能发射另一颗的解决方法:
// 因为子弹不存活时,就会被移出集合,即子弹销毁后,集合是空的,所以就在EnemyTank中
// 判断集合是否为空,若为空,就创建新子弹线程并加入集合=>注意要判断当前坦克的方向来创建坦克
enemyTank.shoots.remove(j);
}
}
}
}
//画我方坦克的子弹=>如果子弹线程非空且子弹存在
if (myTank.shoot != null && myTank.shoot.isLive) {//单颗子弹
g.setColor(Color.white);
g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);
}
// for (int i = 0; i < myTank.shoots.size(); i++) {//多颗子弹
// //- 取出
// Shoot shoot = myTank.shoots.get(i);
// //- 如果存在就画出子弹,不存在就从集合中移除
// if (shoot.isLive) {
// g.setColor(Color.white);
// g.fillOval(shoot.x, shoot.y, 2, 2);
// } else {
// myTank.shoots.remove(shoot);
// }
// }
//画子弹爆炸效果的图片
//- 遍历bombs集合,画出每个(不用判断是否为空,因为size=0时,循环0次)
for (int i = 0; i < bombs.size(); i++) {
//- 取出一个爆炸类(一个爆炸类可以控制三张图片的显示)
try {
Thread.sleep(50);//休眠50ms,不然执行速度太快,第一个坦克爆炸没有效果
} catch (InterruptedException e) {
e.printStackTrace();
}
Bomb bomb = bombs.get(i);
//- 因为run()中不断重绘,所以不停执行这个for循环,life是一直减少的
if (bomb.life > 6) {//- 不同的爆炸时期,画出不同的爆炸效果
//- bomb对象在hitTank()中初始化过了
g.drawImage(image1, bomb.x, bomb.y, 60, 60, this);
} else if (bomb.life > 3) {
g.drawImage(image2, bomb.x, bomb.y, 60, 60, this);
} else {
g.drawImage(image3, bomb.x, bomb.y, 60, 60, this);
}
bomb.lifeDown();//- 让图片显示的生命值减少
//- 在lifeDown()中设置了如果life=0,则isLive=false
//-- 当life=0时,爆炸效果显示完毕,那就把该bomb对象从集合中移除,下次就不会再遍历到他了
if (!(bomb.isLive)) {
bombs.remove(bomb);
}
}
}
/**
* 该项目有两种类型坦克:①我方②敌方 -> 不同类型的坦克,颜色不同
* 该项目有四种移动方向:不同移动方向使用画笔绘制的坦克是不同的
*
* @param x 坦克左上角x坐标
* @param y 坦克左上角y坐标
* @param g 画笔
* @param direction 坦克的移动方向(上下左右)
* @param type 坦克的类型(敌方/我方)
*/
public void drawTank(int x, int y, Graphics g, int direction, int type) {
//两种类型的坦克①我方②敌方 -> 不同类型的坦克,颜色不同
switch (type) {
case 0://我方
g.setColor(Color.CYAN);
break;
case 1://敌方
g.setColor(Color.YELLOW);
break;
}
//四种移动方向:不同移动方向使用画笔绘制的坦克是不同的
//direction:0:向上,1:向右,2:向下,3:向左
switch (direction) {
case 0://向上
g.fill3DRect(x, y, 10, 60, false);//画坦克左边轱辘
g.fill3DRect(x + 30, y, 10, 60, false);//画坦克右边轱辘
g.fill3DRect(x + 10, y + 10, 20, 40, false);//画坦克的身体
g.fillOval(x + 10, y + 20, 20, 20);//画坦克的圆盖
g.drawLine(x + 20, y, x + 20, y + 30);//画坦克的炮筒
break;
case 1://向右
g.fill3DRect(x, y, 60, 10, false);//画坦克上边轱辘
g.fill3DRect(x, y + 30, 60, 10, false);//画坦克下边轱辘
g.fill3DRect(x + 10, y + 10, 40, 20, false);//画坦克的身体
g.fillOval(x + 20, y + 10, 20, 20);//画坦克的圆盖
g.drawLine(x + 30, y + 20, x + 60, y + 20);//画坦克的炮筒
break;
case 2://向下
g.fill3DRect(x, y, 10, 60, false);//画坦克左边轱辘
g.fill3DRect(x + 30, y, 10, 60, false);//画坦克右边轱辘
g.fill3DRect(x + 10, y + 10, 20, 40, false);//画坦克的身体
g.fillOval(x + 10, y + 20, 20, 20);//画坦克的圆盖
g.drawLine(x + 20, y + 30, x + 20, y + 60);//画坦克的炮筒
break;
case 3://向左
g.fill3DRect(x, y, 60, 10, false);//画坦克左边轱辘
g.fill3DRect(x, y + 30, 60, 10, false);//画坦克右边轱辘
g.fill3DRect(x + 10, y + 10, 40, 20, false);//画坦克的身体
g.fillOval(x + 20, y + 10, 20, 20);//画坦克的圆盖
g.drawLine(x + 30, y + 20, x, y + 20);//画坦克的炮筒
break;
}
}
//判断是否击中,若击中则坦克消失(这里按判断我方击中敌方)
// - 什么时候判断我方坦克是否击中敌方坦克?=> 在MyPanel的run()中,每隔100ms判断一次。
// -- 开始时想到当按下J时判断,但是这样的只会判断一次,而且里面的内容(包括:子弹的位置和坦克的位置都只获取了一次),那这是永远判断不成功的
//把EnemyTank改成Tank,使得MyTank也能使用这个方法,给Tank类写一个isLive属性(因为tank访问不到子类特有的属性)
public void hitTank(Shoot s, Tank tank) {
// - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向
switch (tank.getDirection()) {
case 0://坦克向上
case 2://坦克向下
if (s.x > tank.getX() && s.x < tank.getX() + 40 &&
s.y > tank.getY() && s.y < tank.getY() + 60) {
s.isLive = false;//子弹消失
tank.isLive = false;//敌人坦克消失
enemyTanks.remove(tank);//从集合中移除被击中的坦克
//击中时,创建bomb对象,放入bombs集合中
Bomb bomb = new Bomb(tank.getX(), tank.getY());
bombs.add(bomb);
}
case 1://坦克向右
case 3://坦克向左
if (s.x > tank.getX() && s.x < tank.getX() + 60 &&
s.y > tank.getY() && s.y < tank.getY() + 40) {
s.isLive = false;//子弹消失
tank.isLive = false;//敌人坦克消失
enemyTanks.remove(tank);//从集合中移除被击中的坦克
//击中时,创建bomb对象,放入bombs集合中
Bomb bomb = new Bomb(tank.getX(), tank.getY());
bombs.add(bomb);
}
}
}
public void hitEnemyTank() {
//我方坦克发射多颗子弹:遍历每颗子弹判断是否击中
// for (int i = 0; i < myTank.shoots.size(); i++) {
// //取出子弹
// Shoot shoot = myTank.shoots.get(i);
// //判断该颗子弹是否击中坦克=>遍历坦克
// for (int j = 0; j < enemyTanks.size(); j++) {
// EnemyTank enemyTank = enemyTanks.get(j);
// hitTank(shoot, enemyTank);
// }
// }
//单颗
//-- 如果我方发射线程不为空且子弹存在,再判断是否击中
// 在调用run()之前,myTank已经进行了初始化,所以myTank不为空,但是shoot线程是在按下J键时创建的,
// 程序刚开始不按J键时,shoot为空,所以不写myTank.shoot != null这个条件,会报空指针异常
//- 遇到一个问题:子弹打到坦克时,子弹的isLive=false,所以子弹不再绘制,但是子弹的xy还在继续变化
// 这是因为在isLive=false时,发射线程并没有退出=>前面设置的是 子弹碰到墙壁后退出线程,所以要为退出线程增加一个条件
if (myTank.shoot != null && myTank.shoot.isLive) {//单颗子弹
// -- 遍历敌方坦克,判断击中哪个
for (int i = 0; i < enemyTanks.size(); i++) {
EnemyTank enemyTank = enemyTanks.get(i);
hitTank(myTank.shoot, enemyTank);
}
}
}
public void hitMyTank() {
for (int i = 0; i < enemyTanks.size(); i++) {//判断每个坦克的每颗子弹 是否击中我方坦克
EnemyTank enemyTank = enemyTanks.get(i);//取出敌人坦克
for (int j = 0; j < enemyTank.shoots.size(); j++) {
Shoot s = enemyTank.shoots.get(j);//取出敌人子弹
//如果我方坦克存在 且 该颗子弹存在 就判断子弹是否集中我方坦克
if (myTank.isLive && s.isLive) {
hitTank(s, myTank);
}
}
}
}
//事件处理方法(对按键进行监听)
@Override
public void keyPressed(KeyEvent e) {
//判断事件(当按下WDSA键时进行处理)
if (e.getKeyCode() == KeyEvent.VK_W) {//上
myTank.moveUp();//改变坦克的坐标(将改变坐标封装到父类的moveUp()方法中)
myTank.setDirection(0);//改变坦克的方向(将direction作为所有坦克的属性放到父类中)
} else if (e.getKeyCode() == KeyEvent.VK_D) {//右
myTank.moveRight();
myTank.setDirection(1);
} else if (e.getKeyCode() == KeyEvent.VK_S) {//下
myTank.moveDown();
myTank.setDirection(2);
} else if (e.getKeyCode() == KeyEvent.VK_A) {//左
myTank.moveLeft();
myTank.setDirection(3);
}
//按下J键时,我方坦克发射子弹
if (e.getKeyCode() == KeyEvent.VK_J) {
//- 出现的问题:
//-- 按一下J,只会调用一次keyPressed(),那面板只会重绘一次
//-- 因此按一下J,只能看到一个不会动小球
//-- 解决方法:让面板每隔100ms,自动重绘=>用多线程(面板实现Runnable接口)
//- 实现:当子弹消亡时,才会发射新子弹
//-- 方法:当子弹线程为空 或 子弹消亡时,调用shootEnemyTank()
//- 实现:当我方坦克被击中时,不能再发射子弹
//-- 方法:增加一个条件: myTank.isLive==true 当我方坦克存在时,才创建子弹
if (myTank.isLive && myTank.shoot == null || !(myTank.shoot.isLive)) {//发射单颗(销毁了才会发第二颗)
myTank.shootEnemyTank();
}
// myTank.shootEnemyTank();//发射多颗子弹:无需判断
}
//重绘
this.repaint();
}
@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}
//每隔100ms,重绘面板,使子弹移动
// - 要while不停的循环这些内容,不然线程只执行一次就退出了
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//- 判断是否击中敌人坦克
hitEnemyTank();
// hitEnemyTank();//多颗子弹
// - 判断敌方坦克是否击中我方坦克
hitMyTank();
this.repaint();
}
}
}
TankGame窗口类
package com.wpz.tankgame2_2;
import javax.swing.*;
/**
* @author 王胖子
* @version 1.0
* 窗口类
* 在构造器中:设置窗口信息
*/
public class TankGame extends JFrame {
private MyPanel mp = null;//定义面板
public static void main(String[] args) {
new TankGame();
}
public TankGame() {
this.mp = new MyPanel();
//启动线程mp的线程:重绘面板
Thread thread = new Thread(mp);
thread.start();
this.add(mp);
this.setSize(1000, 750);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
this.addKeyListener(mp);//为mp面板添加键盘监听器
}
}