项目代码
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的帮助)
@Override
public 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
}
}
@Override
public 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坦克