项目代码
https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter18/src/com/yinhai/tankgame1_3
〇、要求
增加功能
1.让敌人的坦克也能够发射子弹(可以有多颗子弹)
2.当我方坦克击中敌人坦克时,敌人的坦克就消失,如果能做出爆炸效果更好.
3.让敌人的坦克也可以自由随机的上下左右移动
4.控制我方的坦克和敌人的坦克在规定的范围移动
一、敌人坦克也能发射子弹
思路
1.敌人的坦克也使用Vector来保存它的子弹,因为多个敌人有多个子弹
2.调用设计方法,就给该坦克初始化一个Shot对象,同时启动Shot
3.在绘制敌人坦克时,需要Enemy坦克,如果子弹消亡,记得回收该子弹
1.新建Enemy类
这一段代码类似于Hero类,有shotBullet方法,该方法创建了子弹对象,和1.1版本的功能一样,启动shot线程,这个类也创建了enemyBullets用于存放敌人射出的子弹对象
public class Enemy extends Tank {Vector<Bullet> enemyBullets = new Vector<>();private int type = 1;public Enemy(int x, int y,double speed) {super(x, y,speed);setDirect(2);}public int getTYPE() {return type;}public Bullet shotBullet(){Bullet bullet = null;switch (getDirect()){case 0:bullet = new Bullet(this.getX() + 18,this.getY() - 10,50,getDirect());break;case 1:bullet = new Bullet(this.getX() + 60,this.getY() +18,50,getDirect());break;case 2:bullet = new Bullet(this.getX() + 18,this.getY() +60,50,getDirect());break;case 3:bullet = new Bullet(this.getX() - 10,this.getY()+18,50,getDirect());break;}enemyBullets.add(bullet);Bullet.Shot shot = bullet.new Shot();Thread thread = new Thread(shot);thread.start();return bullet;}
}
2.MyPanel类的paint方法
该方法改进,将1.2的绘画子弹方法进行封装,paintBullet方法,其本质还是1.2版本的思路,循环遍历列表,消亡我就添加到消亡列表,然后remove子弹列表里的所有消亡列表,最后清空消亡列表,我们的Enemy保存为Vector类,记得取出后再调用特有属性
public void paint(Graphics g) {super.paint(g);paintBullet(hero.heroBullets, g);for (int i = 0;i < enemies.size();i++){Enemy enemy = enemies.get(i);enemy.shotBullet();paintBullet(enemy.enemyBullets, g);}}public void paintBullet(Vector<Bullet> bullets,Graphics g){Vector<Bullet> unliveBullets = new Vector<>();bullets.removeAll(unliveBullets);for (int i = 0; i < bullets.size(); i++) {Bullet bullet = bullets.get(i);if(!bullet.isLive()){unliveBullets.add(bullet);}if(bullet != null && bullet.isLive()){drawBullet(g,bullet,hero.getTYPE());}}unliveBullets.clear();}
效果
最后调用shotBullet即可发射子弹,将调用方法写在画板的paint方法里,效果如下
二、击中敌人坦克时消失
思路
1.应当编写一个判断方法,判断是否击中
2.如果击中,敌人坦克消亡应当有一个属性值,将其置为false,子弹也需要置为false
3.什么时候判断,应当在一个线程的循环里进行重复的判断
4.应当再paint方法内停止绘画已经消亡的坦克,并且溢出列表内的坦克
1.判断是否击中
1)在画板中判断是否击中,写两个方法纯粹是塞到一块太难看了,一个方法hitEnemyTank是负责判断子弹的范围,另外一个hitIf是循环取出子弹和循环取出敌人对象塞到hitEnemyTank方法里,如果击中,将新增的isLive置为false;
public static void hitEnemyTank(Bullet b, Enemy enemy) {switch (enemy.getDirect()) {case 0:case 2:if (b.getX() > enemy.getX() && b.getX() < enemy.getX() + 40&& b.getY() > enemy.getY() && b.getY() < enemy.getY() + 60) {b.setLive(false);enemy.setLive(false);}break;case 1:case 3:if (b.getX() > enemy.getX() && b.getX() < enemy.getX() + 60&& b.getY() > enemy.getY() && b.getY() < enemy.getY() + 40) {b.setLive(false);enemy.setLive(false);}break;}}public static void hitIf(Hero hero,Vector<Enemy> enemies){for (int i = 0; i < hero.heroBullets.size(); i++) {if(hero.heroBullets.get(i) == null){continue;}Bullet bullet = hero.heroBullets.get(i);for (int j = 0; j < enemies.size() ; j++) {Enemy enemy = enemies.get(j);hitEnemyTank(bullet,enemy);}}}
2.在画板线程里调用方法
3.如何让坦克消失
在paint方法内设置门槛,循环取出列表内的敌人,如果为空就继续跳到for开头(因为我们可能已经移除过一次中间的元素,如果不判断会抛出异常)。不为空,获取该元素,并查看是否还存活,如果不存活remove该元素,然后继续循环,最后绘出坦克,注意这里为什么要使用i--,因为不使用i--会跳过一个敌人
remove会自动前移数组,如果不i--,会导致这次线程不绘画本应该存在的下一个坦克,下一个坦克会在下一次线程中继续被绘出来,所以会闪一下(来自GPT的帮助)
@Overridepublic void paint(Graphics g) {super.paint(g);for (int i = 0; i < enemies.size(); i++) {if (enemies.get(i) == null) {continue;}Enemy enemy = enemies.get(i);if(!enemy.isLive()){enemies.remove(enemy);i--;//为什么需要i-- 是因为在处理敌人数组时,如果你使用 remove 方法来删除一个元素,它会将数组中的元素往前移动填补被删除元素的位置,这样数组中不会存在 null 元素。continue;}drawTank(enemy.getX(), enemy.getY(), g, enemy.getDirect(), enemy.getTYPE());}}
4.记得将Bullet线程以通知的方式结束
中间量为isLive
效果
二(加强)、爆炸效果
思路
使用绘图里的输出图片完成
坦克只在被击中的时候死亡,所以当一个坦克死亡的时候把坦克的位置用这三张图片替代,然后如果不做成一个像子弹一样的类的话很难保证不堵塞,因为图片太快了需要休眠让图片依次走,单独写一个炸弹类,类内定义一个Life,每执行一次线程就life--,相当于执行完爆炸效果需要9个线程的时间
1.定义Bomb类
该类写了一个life,用于执行坦克的图片的消亡过程
public class Bomb {private int x;private int y;private int life = 9;private boolean isLive = true;public int getLife() {return life;}public int getX() {return x;}public int getY() {return y;}public Bomb(int x, int y) {this.x = x;this.y = y;}public boolean isLive() {return isLive;}public void lifeDown(){if(life > 0){--life;}else{isLive = false;}}
}
2.添加Bomb对象
当我们击中坦克时,在该坦克处创建一个Bomb对象,该对象记录当前enemy的坐标。
public void hitEnemyTank(Bullet b, Enemy enemy) {switch (enemy.getDirect()) {case 0:case 2:if (b.getX() > enemy.getX() && b.getX() < enemy.getX() + 40&& b.getY() > enemy.getY() && b.getY() < enemy.getY() + 60) {b.setLive(false);enemy.setLive(false);bombs.add(new Bomb(enemy.getX(), enemy.getY()));System.out.println("子弹击中");}break;case 1:case 3:if (b.getX() > enemy.getX() && b.getX() < enemy.getX() + 60&& b.getY() > enemy.getY() && b.getY() < enemy.getY() + 40) {b.setLive(false);enemy.setLive(false);bombs.add(new Bomb(enemy.getX(), enemy.getY()));System.out.println("子弹击中");}break;}}
3.通过在paint方法内绘出炸弹效果
因为paint方法是在线程内被run方法反复执行,所以每调用一次bombEffect都会让bomb对象的life--,当处理完后移除该炸弹对象,注意如果只设置一个对象存放bomb会导致多个坦克的爆炸效果出现问题
public void paint(Graphics g) {super.paint(g);bombEffect(g);}public void bombEffect(Graphics g) {for (int i = 0; i < bombs.size(); i++) {Bomb bomb = bombs.get(i);if(bomb.getLife()>0){bomb.lifeDown();if (bomb.getLife() > 6) {g.drawImage(image, bomb.getX(), bomb.getY(), 60, 60, this);} else if (bomb.getLife() > 3) {g.drawImage(image1, bomb.getX(), bomb.getY(), 60, 60, this);} else {g.drawImage(image2, bomb.getX(), bomb.getY(), 60, 60, this);}}else {bombs.remove(bomb);}}}
效果
目前存在一个问题,就是第一个对象不会正常显示爆炸效果,考虑并行导致出现单线程里语句的干扰,找不到合理的解释。
三、敌人坦克随机移动
思路
敌人坦克可以自由移动,则需要将其设置为多线程(多个敌人同时移动), 其次在重写的run方法内实现randomMove方法,实现随机方向,加一个判断是否移动,然后再定义个值,判断是否转向
1.将enemy设置为多线程
设置为多线程后,重写run方法,在run里实现坦克的移动,记得在创建enemy对象的地方启动该线程
2.move方法
使用math.random的方式来随机移动,
public class Enemy extends Tank implements Runnable {Vector<Bullet> enemyBullets = new Vector<>();private int type = 1;private boolean isLive = true;private int count;public boolean isLive() {return isLive;}public void randomMove() {//先随机是否移动if ((int)(Math.random() * 4) == 3) {//判断是否可以移动,0-3,四分之3的概率可以移动return;}count++;//一个计数器,增加移动的次数switch (getDirect()) {//根据方向进行移动case 0:moveUp();break;case 1:moveRight();break;case 2:moveDown();break;case 3:moveLeft();break;}if (count >= (int) (Math.random() * 40)) {//当移动的次数大于某个值的时候,改变方向,0-39的范围setDirect((int) (Math.random() * 4));//随机给一个方向count = 0;//计数为0}}@Overridepublic void run() {while (isLive) {try {Thread.sleep(500);randomMove();} catch (InterruptedException e) {e.printStackTrace();}}}
}
效果
实现了坦克的随机移动
不过没有设置碰撞,和边界,坦克会瞎跑不见或者叠在其他坦克上
四、控制我方坦克和敌人的坦克在规定范围内移动
思路
创建一个静态的Map,用于表示当前地图的大小,然后在地图类内定义方法判断tank是否还在游戏游戏区域,该方法在tank的move内使用
1.定义map类,编写判断方法
在该map类初始化时赋值,然后写判断方法
注意,判断方法不能写成
if(tank.x < mapminX){return false;}
if(tank.x > mapmaxX){return false;}
if(tank.x < mapminY){return false;}
if(tank.x > mapmaxY){return false;}
return ture;
写成这样会导致方法调用在移动执行之前,但是每次判断都是false,导致执行不到移动方法,后果就是我们的tank被边界抓住了,无法移动,所以我们获取面向,如果是上,我们就只限制tank的y不能大于minY即可。为什么mapmaxX要减tank.speed,因为如果不减,如果本来的边界是1600 - 60 = 1540 ,判断完之后坦克是还能往右边走的,就会 变成 1540 + speed = 1560的位置才不能往前走,炮管会突出去。所以最好是加个speed。
public class Map {private static int mapMinX;private static int mapMaxX;private static int mapMinY;private static int mapMaxY;public static int getMapMinX() {return mapMinX;}public static int getMapMaxX() {return mapMaxX;}public static int getMapMinY() {return mapMinY;}public static int getMapMaxY() {return mapMaxY;}public Map(int mapMinX, int mapMinY, int mapMaxX, int mapMaxY) {this.mapMinX = mapMinX;this.mapMinY = mapMinY;this.mapMaxX = mapMaxX;this.mapMaxY = mapMaxY;}public static boolean scopeIf(Tank tank) {switch (tank.getDirect()) {case 0:if (tank.getY() < mapMinY +tank.getSpeed()) {return false;}break;case 1:if (tank.getX() > mapMaxX - 60 - tank.getSpeed()) {return false;}break;case 2:if (tank.getY() > mapMaxY - 60 - tank.getSpeed()) {return false;}break;case 3:if (tank.getX() < mapMinX + tank.getSpeed()) {return false;}break;}return true;}
}
2.在hero和enemy的移动方法内调用该方法
这样的好处就是不用动之前的代码,动来动去自己都忘了
public void heroMove() {if(!Map.scopeIf(this)){return;}{/*...根据面向执行移动*/}}public void randomMove() {//先随机是否移动if ((int) (Math.random() * 4) == 3) {return;}if(!Map.scopeIf(this)){setDirect((int) (Math.random() * 4));return;}{/*...根据面向执行移动*/}}
效果
现在都已经限制在这个黑色区域内了包括hero坦克