第18章:坦克大战2.1

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

总体内容(增加的功能)

  • 2.1版本包括
    ①敌方坦克发射子弹
    ②敌方坦克击中就消失
    ③击中的爆炸效果
    ④敌方坦克自由移动
    ⑤控制坦克移动范围

1. 敌方坦克发射子弹

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

思路

实现

EnemyTank类和MyPanel类

  1. EnemyTank类中:①定义子弹线程的集合(因为允许一个敌方坦克发射多颗子弹) ②明白为什么不能在构造器中创建子弹线程(代码的注释里写了原因)
  2. MyPanel类类中:①修改:初始化敌方坦克后,创建+启动这个坦克的子弹线程,并把这个线程添加到自己的子弹集合中 ②修改:在画敌方坦克时,从每个坦克子弹集合中取出每颗子弹并绘制子弹
package com.wpz.tankgame;import java.util.Vector;/*** @author 王胖子* @version 1.0* 敌人的坦克*/
public class EnemyTank extends Tank {//千万注意要初始化这个Vector...不然会报空指针Vector<Shoot> shoots = new Vector();//一个敌方坦克可以发射多颗子弹,所以这里用集合public EnemyTank(int x, int y) {super(x, y);//每创建一个敌方坦克都分配一个子弹线程并启动=>把(创建+启动)线程 写到初始化敌人坦克后// 不能在这个构造器中创建线程,因为初始化EnemyTank时,只给x,y赋值了,没给direction赋值,// 而初始化子弹线程时,需要EnemyTank的方向,如果在这个类中创建线程,会报空指针异常//也就是下面这两句://EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);//shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());}
}
package com.wpz.tankgame;import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;/*** @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);// - 创建敌方坦克时,就为这个坦克(创建+启动)一个子弹线程,并把这个子弹线程添加到自己的子弹集合中Shoot shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());shoot.start();enemyTank.shoots.add(shoot);// - 加入enemyTanks.add(enemyTank);}}@Overridepublic void paint(Graphics g) {super.paint(g);g.fillRect(0, 0, 1000, 750);//绘图区域:填充矩形,默认是黑色//画出自己的坦克->封装到画坦克的方法中// - 对direction做了修改,把它放到了tank父类中(使用get()方法访问)drawTank(myTank.getX(), myTank.getY(), g, myTank.getDirection(), 0);//画出我的坦克//画敌人的坦克->遍历集合for (int i = 0; i < enemyTanks.size(); i++) {// - 取出坦克EnemyTank enemyTank = enemyTanks.get(i);drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirection(), 1);//画敌方坦克的子弹 => 画出敌方坦克时,也画出每个坦克的(多颗)子弹(遍历每隔坦克的子弹集合)for (int j = 0; j < enemyTank.shoots.size(); j++) {// - 取出子弹Shoot shoot = enemyTank.shoots.get(j);if (shoot.isLive) {g.setColor(Color.white);g.fillOval(shoot.x, shoot.y, 2, 2);} else {// - 如果子弹不存在,就把该子弹从Vector中移除enemyTank.shoots.remove(j);}}}//画我方坦克的子弹=>如果子弹线程非空且子弹存在if (myTank.shoot != null && myTank.shoot.isLive) {g.setColor(Color.white);g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);}}/*** 该项目有两种类型坦克:①我方②敌方 -> 不同类型的坦克,颜色不同* 该项目有四种移动方向:不同移动方向使用画笔绘制的坦克是不同的** @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;}}//事件处理方法(对按键进行监听)@Overridepublic 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();}@Overridepublic void keyReleased(KeyEvent e) {}@Overridepublic void keyTyped(KeyEvent e) {}//每隔100ms,重绘面板,使子弹移动// - 要while不停的循环这些内容,不然线程只执行一次就退出了@Overridepublic void run() {while (true) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}this.repaint();}}
}

输出效果:


2. 敌方坦克击中就消失

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

实现

MyPanel类,EnemyTank类,Shoot类

  1. MyPanel类中:
    ① 增加一个方法hitTank(Shoot s,EnemyTank enemyTank),判断子弹是否击中坦克。因为坦克的方向不同,坦克所在的范围也不同,所以根据坦克的方向来判断=>子弹的x是否进入坦克的范围内,若进入,则将子弹isLive置false,坦克的isLive置false。这里没有直接遍历敌人坦克,没有直接获取我方坦克的子弹,而使用传参的方式:这样的处理使得这个方法更加灵活,使得方法还可以用于敌人坦克打我方坦克。这里没有将坦克从集合中移除而是用了isLive的方式:也是为了上面的原因,我方坦克不能使用集合方式来移除
    ② 修改:画敌方坦克时,增加判断。只有坦克存在的时候才画敌人坦克和敌人子弹。
    ③ 增加:Q:在哪里调用hitTank() 呢 A:在MyPanel的run()中,遍历坦克集合,判断我方坦克是否击中。不能在按下J键时调用,原因在注释里写了
  2. EnemyTank类中:增加属性IsLive,表示坦克是否存在
  3. Shoot类中:修改线程结束的条件为=>碰到墙壁或击中坦克。解决了击中坦克时,子弹消失(不再绘制子弹s.isLive=false),但是线程还没结束的问题。
package com.wpz.tankgame;import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;/*** @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);// - 创建敌方坦克时,就为这个坦克(创建+启动)一个子弹线程,并把这个子弹线程添加到自己的子弹集合中Shoot shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());shoot.start();enemyTank.shoots.add(shoot);// - 加入enemyTanks.add(enemyTank);}}@Overridepublic void paint(Graphics g) {super.paint(g);g.fillRect(0, 0, 1000, 750);//绘图区域:填充矩形,默认是黑色//画出自己的坦克->封装到画坦克的方法中// - 对direction做了修改,把它放到了tank父类中(使用get()方法访问)drawTank(myTank.getX(), myTank.getY(), g, myTank.getDirection(), 0);//画出我的坦克//画敌人的坦克->遍历集合for (int i = 0; i < enemyTanks.size(); i++) {// - 取出坦克EnemyTank enemyTank = enemyTanks.get(i);// - 判断坦克是否存在,存在时才画这个坦克和它的子弹if (enemyTank.isLive) {drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirection(), 1);//画敌方坦克的子弹 => 画出敌方坦克时,也画出每个坦克的(多颗)子弹(遍历每隔坦克的子弹集合)for (int j = 0; j < enemyTank.shoots.size(); j++) {// - 取出子弹Shoot shoot = enemyTank.shoots.get(j);if (shoot.isLive) {// - 如果子弹存在就画g.setColor(Color.white);g.fillOval(shoot.x, shoot.y, 2, 2);} else {// - 如果子弹不存在,就把该子弹从Vector中移除enemyTank.shoots.remove(j);}}}}//画我方坦克的子弹=>如果子弹线程非空且子弹存在if (myTank.shoot != null && myTank.shoot.isLive) {g.setColor(Color.white);g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);}}/*** 该项目有两种类型坦克:①我方②敌方 -> 不同类型的坦克,颜色不同* 该项目有四种移动方向:不同移动方向使用画笔绘制的坦克是不同的** @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时判断,但是这样的只会判断一次,而且里面的内容(包括:子弹的位置和坦克的位置都只获取了一次),那这是永远判断不成功的public void hitTank(Shoot s, EnemyTank enemyTank) {// - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向switch (enemyTank.getDirection()) {case 0://坦克向上case 2://坦克向下if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 40 &&s.y > enemyTank.getY() && s.y < enemyTank.getY() + 60) {s.isLive = false;//子弹消失enemyTank.isLive = false;//敌人坦克消失}case 1://坦克向右case 3://坦克向左if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 60 &&s.y > enemyTank.getY() && s.y < enemyTank.getY() + 40) {s.isLive = false;//子弹消失enemyTank.isLive = false;//敌人坦克消失}}}//事件处理方法(对按键进行监听)@Overridepublic 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();}@Overridepublic void keyReleased(KeyEvent e) {}@Overridepublic void keyTyped(KeyEvent e) {}//每隔100ms,重绘面板,使子弹移动// - 要while不停的循环这些内容,不然线程只执行一次就退出了@Overridepublic void run() {while (true) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}//- 判断是否击中敌人坦克//-- 如果我方发射线程不为空且子弹存在,再判断是否击中// 在调用run()之前,myTank已经进行了初始化,所以myTank不为空,但是shoot线程是在按下J键时创建的,// 程序刚开始不按J键时,shoot为空,所以不写myTank.shoot != null这个条件,会报空指针异常//- 遇到一个问题:子弹打到坦克时,子弹的isLive=false,所以子弹不再绘制,但是子弹的xy还在继续变化// 这是因为在isLive=false时,发射线程并没有退出=>前面设置的是 子弹碰到墙壁后退出线程,所以要为退出线程增加一个条件if (myTank.shoot != null && myTank.shoot.isLive) {// -- 遍历敌方坦克,判断击中哪个for (int i = 0; i < enemyTanks.size(); i++) {EnemyTank enemyTank = enemyTanks.get(i);hitTank(myTank.shoot, enemyTank);}}this.repaint();}}
}
package com.wpz.tankgame;import java.util.Vector;/*** @author 王胖子* @version 1.0* 敌人的坦克*/
public class EnemyTank extends Tank {//千万注意要初始化这个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());}
}
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;}@Overridepublic void run() {//该线程的任务是:// ①控制子弹的移动(根据子弹的方向去移动)// ②控制子弹是否存在while (true) {try {Thread.sleep(50);//休眠一下,不然子弹一下子就打到墙上了} catch (InterruptedException e) {e.printStackTrace();}// - 根据方向改变x,y坐标if (direction == 0) {//上y -= speed;} else if (direction == 1) {//右x += speed;} else if (direction == 2) {//下y += speed;} else if (direction == 3) {//左x -= speed;}System.out.println("x = " + x + " y = " + y);//测试//- 如果碰到墙壁,则线程结束(break),子弹消失(置false,面板中不绘制子弹)//- 解决子弹打到坦克时,线程还未退出的问题:判断的条件增加isLive//把正确条件取反 就得到了它反面的条件(正确条件为:没碰到墙壁且子弹存在)if (!(x >= 0 && x <= 1000 && y >= 0 && y <= 750 && isLive)) {isLive = false;//子弹消失System.out.println("线程结束");break;//线程结束}}}
}

3. 击中的爆炸效果

实现

增加Bomb类,修改MyPanel类

  1. 准备三张图片放到out/production/项目下
  2. 新建Bomb爆炸类:①图片显示的位置xy ②控制图片逐个显示+显示的时间:life ③显示时间为0时,就让图片都不再绘制且把这个类从集合中移除:isLive=>具体的解释看代码注释
  3. MyPanel类中:①增加bombs集合(存放被击中坦克的爆炸效果),三张图片 ②在构造器中初始化这三张图片 ③在hitTank()中修改:如果击中坦克,则初始化一个Bomb对象放到bombs集合中 ④在paint()中:画出爆炸效果的三张图片=>注意只需要循环遍历bombs集合,不需要循环减少效果的生命值,因为run()中一直在重绘。⑤为了解决击中已死亡的坦克还能出现爆炸效果的问题:在hitTank()中,将已死亡坦克从集合中移除
package com.wpz.tankgame;/*** @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;//}}
}
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;//敌人坦克的数量//定义炸弹,放到Vector中=>炸弹爆炸不属于坦克属性,属于面板的属性//- 当子弹击中坦克时,加入一个Bomb对象到bombs:被击中的每个坦克都有一个Bomb对象,加入集合中统一管理Vector<Bomb> bombs = new Vector<>();//定义三张炸弹爆炸的图片,用于显示炸弹爆炸效果Image image1 = null;Image image2 = null;Image image3 = null;public MyPanel() {myTank = new MyTank(100, 100);//初始化自己的坦克myTank.setSpeed(5);//设置坦克移动的速度//初始化敌人的坦克(注意:使用循环来添加。因为敌人坦克数量多,不要一个一个add)for (int i = 0; i < enemyTankSize; i++) {// - 创建一个敌人的坦克EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);// - 设置方向enemyTank.setDirection(2);// - 创建敌方坦克时,就为这个坦克(创建+启动)一个子弹线程,并把这个子弹线程添加到自己的子弹集合中Shoot shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());shoot.start();enemyTank.shoots.add(shoot);// - 加入enemyTanks.add(enemyTank);}//初始化三个爆炸的图片对象image1 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_1.gif"));image2 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_2.gif"));image3 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_3.gif"));}@Overridepublic void paint(Graphics g) {super.paint(g);g.fillRect(0, 0, 1000, 750);//绘图区域:填充矩形,默认是黑色//画出自己的坦克->封装到画坦克的方法中// - 对direction做了修改,把它放到了tank父类中(使用get()方法访问)drawTank(myTank.getX(), myTank.getY(), g, myTank.getDirection(), 0);//画出我的坦克//画敌人的坦克->遍历集合for (int i = 0; i < enemyTanks.size(); i++) {// - 取出坦克EnemyTank enemyTank = enemyTanks.get(i);// - 判断坦克是否存在,存在时才画这个坦克和它的子弹if (enemyTank.isLive) {drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirection(), 1);//画敌方坦克的子弹 => 画出敌方坦克时,也画出每个坦克的(多颗)子弹(遍历每隔坦克的子弹集合)for (int j = 0; j < enemyTank.shoots.size(); j++) {// - 取出子弹Shoot shoot = enemyTank.shoots.get(j);if (shoot.isLive) {// - 如果子弹存在就画g.setColor(Color.white);g.fillOval(shoot.x, shoot.y, 2, 2);} else {// - 如果子弹不存在,就把该子弹从Vector中移除enemyTank.shoots.remove(j);}}}}//画我方坦克的子弹=>如果子弹线程非空且子弹存在if (myTank.shoot != null && myTank.shoot.isLive) {g.setColor(Color.white);g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);}//画子弹爆炸效果的图片//- 遍历bombs集合,画出每个(不用判断是否为空,因为size=0时,循环0次)for (int i = 0; i < bombs.size(); i++) {//- 取出一个爆炸类(一个爆炸类可以控制三张图片的显示)try {Thread.sleep(50);//休眠50ms,不然执行速度太快,第一个坦克爆炸没有效果} catch (InterruptedException e) {e.printStackTrace();}Bomb bomb = bombs.get(i);//- 因为run()中不断重绘,所以不停执行这个for循环,life是一直减少的if (bomb.life > 6) {//- 不同的爆炸时期,画出不同的爆炸效果//- bomb对象在hitTank()中初始化过了g.drawImage(image1, bomb.x, bomb.y, 60, 60, this);} else if (bomb.life > 3) {g.drawImage(image2, bomb.x, bomb.y, 60, 60, this);} else {g.drawImage(image3, bomb.x, bomb.y, 60, 60, this);}bomb.lifeDown();//- 让图片显示的生命值减少//- 在lifeDown()中设置了如果life=0,则isLive=false//-- 当life=0时,爆炸效果显示完毕,那就把该bomb对象从集合中移除,下次就不会再遍历到他了if (!(bomb.isLive)) {bombs.remove(bomb);}}}/*** 该项目有两种类型坦克:①我方②敌方 -> 不同类型的坦克,颜色不同* 该项目有四种移动方向:不同移动方向使用画笔绘制的坦克是不同的** @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时判断,但是这样的只会判断一次,而且里面的内容(包括:子弹的位置和坦克的位置都只获取了一次),那这是永远判断不成功的public void hitTank(Shoot s, EnemyTank enemyTank) {// - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向switch (enemyTank.getDirection()) {case 0://坦克向上case 2://坦克向下if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 40 &&s.y > enemyTank.getY() && s.y < enemyTank.getY() + 60) {s.isLive = false;//子弹消失enemyTank.isLive = false;//敌人坦克消失enemyTanks.remove(enemyTank);//从集合中移除被击中的坦克//击中时,创建bomb对象,放入bombs集合中Bomb bomb = new Bomb(enemyTank.getX(), enemyTank.getY());bombs.add(bomb);}case 1://坦克向右case 3://坦克向左if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 60 &&s.y > enemyTank.getY() && s.y < enemyTank.getY() + 40) {s.isLive = false;//子弹消失enemyTank.isLive = false;//敌人坦克消失enemyTanks.remove(enemyTank);//从集合中移除被击中的坦克//击中时,创建bomb对象,放入bombs集合中Bomb bomb = new Bomb(enemyTank.getX(), enemyTank.getY());bombs.add(bomb);}}}//事件处理方法(对按键进行监听)@Overridepublic 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();}@Overridepublic void keyReleased(KeyEvent e) {}@Overridepublic void keyTyped(KeyEvent e) {}//每隔100ms,重绘面板,使子弹移动// - 要while不停的循环这些内容,不然线程只执行一次就退出了@Overridepublic void run() {while (true) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}//- 判断是否击中敌人坦克//-- 如果我方发射线程不为空且子弹存在,再判断是否击中// 在调用run()之前,myTank已经进行了初始化,所以myTank不为空,但是shoot线程是在按下J键时创建的,// 程序刚开始不按J键时,shoot为空,所以不写myTank.shoot != null这个条件,会报空指针异常//- 遇到一个问题:子弹打到坦克时,子弹的isLive=false,所以子弹不再绘制,但是子弹的xy还在继续变化// 这是因为在isLive=false时,发射线程并没有退出=>前面设置的是 子弹碰到墙壁后退出线程,所以要为退出线程增加一个条件if (myTank.shoot != null && myTank.shoot.isLive) {// -- 遍历敌方坦克,判断击中哪个for (int i = 0; i < enemyTanks.size(); i++) {EnemyTank enemyTank = enemyTanks.get(i);hitTank(myTank.shoot, enemyTank);}}this.repaint();}}
}

输出效果:


4. 敌方坦克自由移动

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

思路

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

### 实现

EnemyTank类和MyPanel类

  1. EnemyTank类:①实现Runnable接口=>敌人坦克类可以当作线程使用了 ②编写run()=>让坦克动起来
  2. MyPanel类中:初始化敌方坦克后,启动这个线程
package com.wpz.tankgame;import java.util.Vector;/*** @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());}@Overridepublic void run() {while (true) {/*写多线程时要考虑三个问题:①处理什么业务逻辑 ②什么时候被创建+启动 ③什么时候结束敌人坦克这个线程中:①业务:敌人坦克朝当前方向走+改变方向②结束:敌人坦克消失时,线程结束③创建+启动:敌人坦克被初始化完成后就可以使用这个线程了*///思路:先向坦克的当前方向走30步=>然后转变方向//- 向当前方向移动30步switch (getDirection()) {case 0://上for (int i = 0; i < 30; i++) {moveUp();try {Thread.sleep(50);//休眠} catch (InterruptedException e) {e.printStackTrace();}}break;case 1://右for (int i = 0; i < 30; i++) {moveRight();//使用父类中移动的方法try {Thread.sleep(50);//休眠} catch (InterruptedException e) {e.printStackTrace();}}break;case 2://下for (int i = 0; i < 30; i++) {moveDown();try {Thread.sleep(50);//休眠} catch (InterruptedException e) {e.printStackTrace();}}break;case 3://左for (int i = 0; i < 30; i++) {moveLeft();try {Thread.sleep(50);//休眠} catch (InterruptedException e) {e.printStackTrace();}}break;}//- 转变方向(方向为0-3随机)setDirection((int) (Math.random() * 4));//- 注意!写并发程序,一定要考虑清楚,该线程什么时候结束//-- 当坦克不再存在时,这个线程就结束了if (!(isLive)) {break;//退出线程}}}
}


输出效果:


5. 控制坦克移动范围

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

实现

Tank类

  • 因为我方坦克和敌方坦克都必须在窗口内移动,所以把判断条件放到move的方法中
  • 每次移动时都判断坦克的坐标是否超出边界,若没有超,就可以继续移动
package com.wpz.tankgame;/*** @author 王胖子* @version 1.0* 因为该游戏会有很多坦克* 所以先抽象成一个父类*/
public class Tank {private int x;//坦克横坐标private int y;//坦克纵坐标//将坦克的方向写到父类中 这样任意坦克在不同方法中 都可以设置移动方向private int direction;//坦克的方向(0:上,1:右,2:下,3:左)-->默认是0private int speed = 2;//移动速度public Tank(int x, int y) {this.x = x;this.y = y;}public int getDirection() {return direction;}public void setDirection(int direction) {this.direction = direction;}public int getX() {return x;}public void setX(int x) {this.x = x;}public int getY() {return y;}public void setY(int y) {this.y = y;}public int getSpeed() {return speed;}public void setSpeed(int speed) {this.speed = speed;}//将坦克移动时的坐标改变封装到 父类的方法中//直接改变坐标的x/y值(取代myTank.setY(myTank.getY()-2);)//先死后活:开始时坐标是加减固定值-->可以扩展成变化的--引入变量-->speed(可以在初始化坦克对象时赋值)//- 让坦克(我方+敌方)都在规定的范围内(窗口内)移动//-- 思路:先判断一下坦克当前的坐标是否超出边界,若没有超,就让坦克移动public void moveUp() {//判断坦克是否超出边界:若没超就移动if (getY() > 0) {y -= speed;}}public void moveRight() {if (getX() + 60 < 1000) {//本来是<1000的,如果坦克会超出一点边界就改成980x += speed;}}public void moveDown() {if (getY() + 60 < 750) {//本来是750,如果会超改为710,把窗口放大就不会超了y += speed;}}public void moveLeft() {if (getX() > 0) {x -= speed;}}
}


版本2.1的全部代码

Tank类

package com.wpz.tankgame;/*** @author 王胖子* @version 1.0* 因为该游戏会有很多坦克* 所以先抽象成一个父类*/
public class Tank {private int x;//坦克横坐标private int y;//坦克纵坐标//将坦克的方向写到父类中 这样任意坦克在不同方法中 都可以设置移动方向private int direction;//坦克的方向(0:上,1:右,2:下,3:左)-->默认是0private int speed = 2;//移动速度public Tank(int x, int y) {this.x = x;this.y = y;}public int getDirection() {return direction;}public void setDirection(int direction) {this.direction = direction;}public int getX() {return x;}public void setX(int x) {this.x = x;}public int getY() {return y;}public void setY(int y) {this.y = y;}public int getSpeed() {return speed;}public void setSpeed(int speed) {this.speed = speed;}//将坦克移动时的坐标改变封装到 父类的方法中//直接改变坐标的x/y值(取代myTank.setY(myTank.getY()-2);)//先死后活:开始时坐标是加减固定值-->可以扩展成变化的--引入变量-->speed(可以在初始化坦克对象时赋值)//- 让坦克(我方+敌方)都在规定的范围内(窗口内)移动//-- 思路:先判断一下坦克当前的坐标是否超出边界,若没有超,就让坦克移动public void moveUp() {//判断坦克是否超出边界:若没超就移动if (getY() > 0) {y -= speed;}}public void moveRight() {if (getX() + 60 < 1000) {//本来是<1000的,如果坦克会超出一点边界就改成980x += speed;}}public void moveDown() {if (getY() + 60 < 750) {//本来是750,如果会超改为710,把窗口放大就不会超了y += speed;}}public void moveLeft() {if (getX() > 0) {x -= speed;}}
}

MyTank类

package com.wpz.tankgame;/*** @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();}
}

EnemyTank类

package com.wpz.tankgame;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());}@Overridepublic void run() {while (true) {/*写多线程时要考虑三个问题:①处理什么业务逻辑 ②什么时候被创建+启动 ③什么时候结束敌人坦克这个线程中:①业务:敌人坦克朝当前方向走+改变方向②结束:敌人坦克消失时,线程结束③创建+启动:敌人坦克被初始化完成后就可以使用这个线程了*///思路:先向坦克的当前方向走30步=>然后转变方向//- 向当前方向移动30步switch (getDirection()) {case 0://上for (int i = 0; i < 30; i++) {moveUp();//使用父类中移动的方法try {Thread.sleep(50);//休眠} catch (InterruptedException e) {e.printStackTrace();}}break;case 1://右for (int i = 0; i < 30; i++) {moveRight();try {Thread.sleep(50);//休眠} catch (InterruptedException e) {e.printStackTrace();}}break;case 2://下for (int i = 0; i < 30; i++) {moveDown();try {Thread.sleep(50);//休眠} catch (InterruptedException e) {e.printStackTrace();}}break;case 3://左for (int i = 0; i < 30; i++) {moveLeft();try {Thread.sleep(50);//休眠} catch (InterruptedException e) {e.printStackTrace();}}break;}//- 转变方向(方向为0-3随机)setDirection((int)(Math.random()*4));//- 注意!写并发程序,一定要考虑清楚,该线程什么时候结束//-- 当坦克不再存在时,这个线程就结束了if (!(isLive)) {break;//退出线程}}}
}

MyPanel类(面板类)

package com.wpz.tankgame;import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;/*** @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(100, 100);//初始化自己的坦克myTank.setSpeed(5);//设置坦克移动的速度//初始化敌人的坦克(注意:使用循环来添加。因为敌人坦克数量多,不要一个一个add)for (int i = 0; i < enemyTankSize; i++) {// - 创建一个敌人的坦克EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);// - 设置方向enemyTank.setDirection(2);// - 创建敌方坦克时,就为这个坦克(创建+启动)一个子弹线程,并把这个子弹线程添加到自己的子弹集合中Shoot shoot = new Shoot(enemyTank.getX() + 20, enemyTank.getY() + 60, enemyTank.getDirection());shoot.start();enemyTank.shoots.add(shoot);// - 启动敌人坦克线程,让坦克动起来new Thread(enemyTank).start();// - 加入enemyTanks.add(enemyTank);}//初始化三个爆炸的图片对象image1 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_1.gif"));image2 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_2.gif"));image3 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_3.gif"));}@Overridepublic void paint(Graphics g) {super.paint(g);g.fillRect(0, 0, 1000, 750);//绘图区域:填充矩形,默认是黑色//画出自己的坦克->封装到画坦克的方法中// - 对direction做了修改,把它放到了tank父类中(使用get()方法访问)drawTank(myTank.getX(), myTank.getY(), g, myTank.getDirection(), 0);//画出我的坦克//画敌人的坦克->遍历集合for (int i = 0; i < enemyTanks.size(); i++) {// - 取出坦克EnemyTank enemyTank = enemyTanks.get(i);// - 判断坦克是否存在,存在时才画这个坦克和它的子弹if (enemyTank.isLive) {drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirection(), 1);//画敌方坦克的子弹 => 画出敌方坦克时,也画出每个坦克的(多颗)子弹(遍历每隔坦克的子弹集合)for (int j = 0; j < enemyTank.shoots.size(); j++) {// - 取出子弹Shoot shoot = enemyTank.shoots.get(j);if (shoot.isLive) {// - 如果子弹存在就画g.setColor(Color.white);g.fillOval(shoot.x, shoot.y, 2, 2);} else {// - 如果子弹不存在,就把该子弹从Vector中移除enemyTank.shoots.remove(j);}}}}//画我方坦克的子弹=>如果子弹线程非空且子弹存在if (myTank.shoot != null && myTank.shoot.isLive) {g.setColor(Color.white);g.fillOval(myTank.shoot.x, myTank.shoot.y, 2, 2);}//画子弹爆炸效果的图片//- 遍历bombs集合,画出每个(不用判断是否为空,因为size=0时,循环0次)for (int i = 0; i < bombs.size(); i++) {//- 取出一个爆炸类(一个爆炸类可以控制三张图片的显示)try {Thread.sleep(50);//休眠50ms,不然执行速度太快,第一个坦克爆炸没有效果} catch (InterruptedException e) {e.printStackTrace();}Bomb bomb = bombs.get(i);//- 因为run()中不断重绘,所以不停执行这个for循环,life是一直减少的if (bomb.life > 6) {//- 不同的爆炸时期,画出不同的爆炸效果//- bomb对象在hitTank()中初始化过了g.drawImage(image1, bomb.x, bomb.y, 60, 60, this);} else if (bomb.life > 3) {g.drawImage(image2, bomb.x, bomb.y, 60, 60, this);} else {g.drawImage(image3, bomb.x, bomb.y, 60, 60, this);}bomb.lifeDown();//- 让图片显示的生命值减少//- 在lifeDown()中设置了如果life=0,则isLive=false//-- 当life=0时,爆炸效果显示完毕,那就把该bomb对象从集合中移除,下次就不会再遍历到他了if (!(bomb.isLive)) {bombs.remove(bomb);}}}/*** 该项目有两种类型坦克:①我方②敌方 -> 不同类型的坦克,颜色不同* 该项目有四种移动方向:不同移动方向使用画笔绘制的坦克是不同的** @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时判断,但是这样的只会判断一次,而且里面的内容(包括:子弹的位置和坦克的位置都只获取了一次),那这是永远判断不成功的public void hitTank(Shoot s, EnemyTank enemyTank) {// - 判断子弹s 是否击中坦克=>因为方向不同,坦克所在的范围也不同,所以这里用switch穿透来判断敌人坦克方向switch (enemyTank.getDirection()) {case 0://坦克向上case 2://坦克向下if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 40 &&s.y > enemyTank.getY() && s.y < enemyTank.getY() + 60) {s.isLive = false;//子弹消失enemyTank.isLive = false;//敌人坦克消失enemyTanks.remove(enemyTank);//从集合中移除被击中的坦克//击中时,创建bomb对象,放入bombs集合中Bomb bomb = new Bomb(enemyTank.getX(), enemyTank.getY());bombs.add(bomb);}case 1://坦克向右case 3://坦克向左if (s.x > enemyTank.getX() && s.x < enemyTank.getX() + 60 &&s.y > enemyTank.getY() && s.y < enemyTank.getY() + 40) {s.isLive = false;//子弹消失enemyTank.isLive = false;//敌人坦克消失enemyTanks.remove(enemyTank);//从集合中移除被击中的坦克//击中时,创建bomb对象,放入bombs集合中Bomb bomb = new Bomb(enemyTank.getX(), enemyTank.getY());bombs.add(bomb);}}}//事件处理方法(对按键进行监听)@Overridepublic 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();}@Overridepublic void keyReleased(KeyEvent e) {}@Overridepublic void keyTyped(KeyEvent e) {}//每隔100ms,重绘面板,使子弹移动// - 要while不停的循环这些内容,不然线程只执行一次就退出了@Overridepublic void run() {while (true) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}//- 判断是否击中敌人坦克//-- 如果我方发射线程不为空且子弹存在,再判断是否击中// 在调用run()之前,myTank已经进行了初始化,所以myTank不为空,但是shoot线程是在按下J键时创建的,// 程序刚开始不按J键时,shoot为空,所以不写myTank.shoot != null这个条件,会报空指针异常//- 遇到一个问题:子弹打到坦克时,子弹的isLive=false,所以子弹不再绘制,但是子弹的xy还在继续变化// 这是因为在isLive=false时,发射线程并没有退出=>前面设置的是 子弹碰到墙壁后退出线程,所以要为退出线程增加一个条件if (myTank.shoot != null && myTank.shoot.isLive) {// -- 遍历敌方坦克,判断击中哪个for (int i = 0; i < enemyTanks.size(); i++) {EnemyTank enemyTank = enemyTanks.get(i);hitTank(myTank.shoot, enemyTank);}}this.repaint();}}
}

Shoot类(子弹类)

package com.wpz.tankgame;/*** @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;}@Overridepublic void run() {//该线程的任务是:// ①控制子弹的移动(根据子弹的方向去移动)// ②控制子弹是否存在while (true) {try {Thread.sleep(50);//休眠一下,不然子弹一下子就打到墙上了} catch (InterruptedException e) {e.printStackTrace();}// - 根据方向改变x,y坐标if (direction == 0) {//上y -= speed;} else if (direction == 1) {//右x += speed;} else if (direction == 2) {//下y += speed;} else if (direction == 3) {//左x -= speed;}
//            System.out.println("x = " + x + " y = " + y);//测试//- 如果碰到墙壁,则线程结束(break),子弹消失(置false,面板中不绘制子弹)//- 解决子弹打到坦克时,线程还未退出的问题:判断的条件增加isLive//把正确条件取反 就得到了它反面的条件(正确条件为:没碰到墙壁且子弹存在)if (!(x >= 0 && x <= 1000 && y >= 0 && y <= 750 && isLive)) {isLive = false;//子弹消失System.out.println(Thread.currentThread().getName()+"线程结束");break;//线程结束}}}
}

Bomb类(爆炸效果类)

package com.wpz.tankgame;/*** @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;//}}
}

TankGame类(窗口类)

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

java项目笔记 - 第18章:坦克大战2.1相关推荐

  1. JavaStudy7(18章-坦克大战2)—B站韩顺平

    JavaStudy7(18章-坦克大战2)-B站韩顺平 1.坦克大战 1.1线程-应用到坦克大战 1.1.1 坦克大战 0.3 代码演示: //为了监听 键盘事件, 实现 KeyListener pu ...

  2. Java 学习笔记:第一章 Java入门

    Java 学习笔记:第一章 Java入门 1.1 计算机语言发展史以及未来方向 1.2 常见编程语言介绍 C语言 C++ 语言 Java语言 PHP 语言 Object-C和Swift 语言 Java ...

  3. 《Java小游戏实现》:坦克大战(续四)

    <Java小游戏实现>:坦克大战(续四) 相关博文: <Java小游戏实现>:坦克大战http://blog.csdn.net/u010412719/article/detai ...

  4. 《Java小游戏实现》:坦克大战

    <Java小游戏实现>:坦克大战 前面写了一个简单的聊天小程序,今天开始就写一个坦克大战的游戏,算是对Java相关小知识点的一个应用. 这个游戏的完成,我们也是分步完成,逐步累加,一个一个 ...

  5. 用JAVA 做一个简易版的坦克大战(只实现基本功能)

    不太会写文章,只是为了记录自己做过的东西 文章目录 前言 一.大概思路 二.主要代码 1.Tank.java 2.Shot.java 3. Mypanel.java 4.Hero.java 5.Ene ...

  6. java游戏牛仔炮筒,《Java小游戏实现》:坦克大战(续2)

    <Java小游戏实现>:坦克大战(续2) 相关博文: 博文<Java小游戏实现>:坦克大战(续1)中已经实现到了坦克可以发射一颗子弹了.这篇博文在此基础上继续实现更多的功能. ...

  7. eclipse 导入项目_JAVA编程实战:坦克大战系列2-坦克如何在eclipse中编写

    游戏中寻找学习JAVA的乐趣之 坦克大战系列2-坦克如何在Eclipse中编写 前言 本篇主要对Robocode在eclipse中如何配置并编写. Eclipse中的配置 通过本身自带的编辑器去写代码 ...

  8. Unity学习笔记5:2D坦克大战

    之前看了很长一段时间的视频,但是基本没有手动操作过,究竟沉淀了多少还真难说得清楚. 所以直接看实例视频跟着做了. 视频地址:http://www.sikiedu.com/my/course/90 un ...

  9. GitHub 项目推荐:俄罗斯方块、坦克大战

    作 者:GitHubDaily 来 源:GitHubDaily 俄罗斯方块小游戏 搜索关键词:react-tetris 有个前端工程师在 GitHub 上用 React 搞了个俄罗斯方块小游戏,不仅好 ...

  10. Clean Code 代码整洁之道笔记(1-8 章)

    Clean Code Chapter 1-8 第一章 整洁代码 1. 为什么需要代码? 2. 混乱的代码 3. 什么是整洁代码 第二章 有意义的命名 1. 名副其实 2. 避免误导 3. 做有意义的区 ...

最新文章

  1. 怎么查找执行比较慢的sql语句
  2. 基于C#局域网语音聊天
  3. matlab设计长度为50的滤波器,实验5 基于Matlab的数字滤波器设计
  4. pandas如何统计所有列的空值,并转化为list?
  5. Let's Encrypt 发布 ACME v2,开始测试通配符证书
  6. 贪婪算法+小应用(调度问题)
  7. 在Windows Server 2008上安装和配置Web和FTP服务
  8. mysql 优化关键字_MySQL 优化之 EXPLAIN 关键字
  9. tab栏切换制作(原生js版本)
  10. 苹果pns推送和唤醒
  11. Android音视频——H.264帧码流(SODB、RBSP、EBSP)浅析
  12. 西门子触摸屏脚本程序_西门子触摸屏实例程序
  13. JAVA ECXCEL 考勤导入查询
  14. 大神之光照耀着我 - 我的成长之路 - 起点
  15. 使用 SysRq 查看系统信息
  16. C语言int型数组转化为字符串
  17. Ubuntu20.04安装英伟达驱动
  18. 肿瘤的分型、分级和分期
  19. [总结]常见漏洞的总结
  20. 总结及处理u盘安装Ubuntu18.04的坑

热门文章

  1. 记录:前端框架Bootstrap学习使用之组件——Collapse(折叠)
  2. 调制深度(modulation depth)是什么?
  3. 去YY欢聚时代的一次面试经历
  4. Unity使用BMFont制作字体
  5. console接口配置登录密码
  6. 自制STC12C5A60S2最小系统板
  7. 网络爬虫技术是什么,网络爬虫的基本工作流程是什么?
  8. java短信验证码_java实现发送短信验证码
  9. 每个人都是雕刻自己的艺术家,生活是你的背景
  10. JavaScript ES6介绍