0. 新增功能介绍

  • 修改背景为黑色
  • 坦克可以根据WASD旋转方向,子弹的朝向也发生变化
  • 添加墙体类wall,坦克和子弹的墙体的碰撞检测,子弹碰到墙体会消失,坦克碰到墙体会顶牛
  • 自定义地图
  • 添加敌方坦克类,敌方坦克移动策略和射击策略(敌方坦克子弹和墙体的碰撞检测还没有实现)
  • 导入了经典游戏的图片素材

坦克游戏(v2.0)代码地址:x-Achan/TankGame at v2.0https://github.com/x-Achan/TankGame/tree/v2.0

1. 枚举类 enum Direction

public enum Direction {
    UP , DOWN , LEFT , RIGHT;
}

        创建 enum Direction枚举类:UP、DOWN、LEFT、RIGHT,所有的坦克和子弹对象都只有四个移动方向,将四个方向放进Direction中

        为坦克和子弹类都加上一个方向Direction dir

protected Direction dir;

        protected 保护的枚举类Direction,在子弹类中调用 owner.dir程序不会出错,但是建议用getter方法owner.getDir()

        对于敌军坦克EnemyTank类,不同于玩家坦克通过键盘控制移动,移动是随机的,出生时或发生碰撞时,都需要随机选择一个方向(即改变speedx和speedy的值),需要一个改变方向的函数changeDirtection()

        枚举的一个核心用法:Direction.values(),返回Direction[](一个包含所有枚举常量的数组,顺序与声明一致)

// 改变方向:随机枚举一个方向,不同的方向有不同的speedx和speedy用来控制移动方向
public void changeDirection(){
    Direction[] dirs = Direction.values(); // 返回所有枚举常量的数组
    Direction newDir = dirs[random.nextInt(dirs.length)];  //从数组中取出一个变量

    this.dir = newDir;
    if(dir == Direction.UP){
        speedx = 0;
        speedy = -GameConfig.ENEMY_SPEED;
    }else if(dir == Direction.DOWN){
        speedx = 0;
        speedy = GameConfig.ENEMY_SPEED;
    }else if(dir == Direction.LEFT){
        speedx = -GameConfig.ENEMY_SPEED;
        speedy = 0;
    }else if(dir == Direction.RIGHT){
        speedx = GameConfig.ENEMY_SPEED;
        speedy = 0;
    }
}

2. 通过墙体类 Wall 创建地图

        墙体不会移动,定义简单,整个地图的大小为800*600,坦克和墙体为20*20(地图的格子大小为:40*30,4:3的比例)

        创建一个String的二维数组Map:

  • 1 的位置表示墙体
  • 0 的位置表示空地
  • * 的位置表示敌军坦克
 public static final String[] MAP_1 = {
            "1111111111111111111111111111111111111111", // 0 (顶部加固边界)
            "1111111111111111111111111111111111111111", // 1 (三个敌军固定出生点)
            "10*0000000000000000*00000000000000000*01", // 2
            "1011100111001110000000011100111001110001", // 3
            "1011100111001110011110011100111001110001", // 4
            "1011100111001110011110011100111001110001", // 5
            "1011100111001110011110011100111001110001", // 6
            "1000000*000000000000000000000*0000000001", // 7 (横向通道)
            "1000000000000000000000000000000000000001", // 8
            "1011111100011111100001111110001111110001", // 9 (长条掩体)
            "1011111100011111100001111110001111110001", // 10
            "1000000000000000001100000000000000000001", // 11
            "1011100111111000001100000011111100111001", // 12
            "1011100111111000000000000011111100111001", // 13
            "10111000000*0000011110000000000000111001", // 14
            "1011100000000000011110000000000000111001", // 15
            "1011100111111000000000000011111100111001", // 16
            "1011100111111000001100000011111100111001", // 17
            "100000000000000000110000000000*000000001", // 18
            "1011111100011111100001111110001111110001", // 19
            "1011111100011111100001111110001111110001", // 20
            "1000000000000000000000000000000000000001", // 21 (横向通道)
            "1000000000000000*00000000000000000000001", // 22
            "1011100111001110011110011100111001110001", // 23
            "1011100111001110011110011100111001110001", // 24
            "1011100111001110000000011100111001110001", // 25
            "1000000000000000000000000000000000000001", // 26 (基地外围)
            "1000000000000000000000000000000000000001", // 27 (老家顶部护甲)
            "1000000000000000000000000000000000000001", // 28 (老家侧边护甲)
            "1111111111111111111111111111111111111111"  // 29 (底部加固边界)
    };

遍历这个二维数组,扫描到1,就计算坐标,并将墙体加入到墙体列表中,同样扫描到坦克同理

for (int row = 0; row < GameConfig.MAP_1.length; row++) {
    String line = GameConfig.MAP_1[row];
    for (int col = 0; col < line.length(); col++) {
        if(line.charAt(col) == '1'){

            //计算墙体生成的位置
            int wallX = col * GameConfig.WALL_SIZE;
            int wallY = row * GameConfig.WALL_SIZE;
            Wall w = new Wall(wallX,wallY);

            //将墙体加入到墙体列表中
            gameUI.wallList.add(w);

            //绘制敌军坦克
        }else if(line.charAt(col) == '*'){

            int tankX = col * GameConfig.TANK_SIZE;
            int tankY = row * GameConfig.TANK_SIZE;

            EnemyTank enemyTank = new EnemyTank(tankX,tankY,Direction.DOWN);
            gameUI.enemyTankList.add(enemyTank);
        }
    }
}

3. 碰撞检测

3.1 坦克和墙体

顶牛:当坦克进入墙体时,立刻将坦克恢复到安全合法的位置

3.1.1 玩家坦克

  在每个游戏元素GameElement 的大类中,添加一个函数:getRect() 返回矩形函数

    public Rectangle getRect(){
        return new Rectangle(x,y,width,height);
    }

        Rectangle 表示一个矩形区域,由左上角坐标(x,y)和宽度和高度决定,提供的方法:intersects()相交、contains()包含、union(合并)等

        用intersects()来判断是否碰撞(两个矩形是否相交)

 遍历所有墙体,通过墙体和玩家坦克矩形相交检测,判断是否发生碰撞

gameUI.wallList.get(i).getRect().intersects(gameUI.playerTank.getRect())

如果碰撞,则顶牛

gameUI.playerTank.setX(gameUI.playerTank.getX() - gameUI.playerTank.getSpeedx());
gameUI.playerTank.setY(gameUI.playerTank.getY() - gameUI.playerTank.getSpeedy());

完整检测过程:

// 遍历所有墙体wall,判断是否和坦克发生碰撞
for (int i = 0; i < gameUI.wallList.size(); i++) {
    // intersects() 方法检测两个矩形是否重叠
    if(gameUI.wallList.get(i).getRect().intersects(gameUI.playerTank.getRect())){

        System.out.println("发生碰撞了");
        //顶牛:碰撞了,将刚刚加上的速度减回去(退回到碰撞前的坐标)
        gameUI.playerTank.setX(gameUI.playerTank.getX() - gameUI.playerTank.getSpeedx());
        gameUI.playerTank.setY(gameUI.playerTank.getY() - gameUI.playerTank.getSpeedy());
    }
}

3.1.2 敌军坦克

        敌军坦克首先是自主移动的,同样遍历每个墙体元素,遍历每个敌军坦克,判断是否有碰撞发生,如果发生碰撞,先实现顶牛,再调用EnemyTank的changeDirection()方法,随机改变方向

        如果不先顶牛会怎么样? 问题:碰撞穿模

        在敌军坦克和墙体的碰撞检测中,如果检测到碰撞,无顶牛,就直接让坦克转向,代码如下

for (int i = 0; i < gameUI.wallList.size(); i++) {
    for (int j = 0; j < gameUI.enemyTankList.size(); j++){
        EnemyTank et = gameUI.enemyTankList.get(j);
        if(gameUI.wallList.get(i).getRect().intersects(et.getRect())){
            System.out.println("敌军坦克撞墙");
            et.changeDirection();
        }
    }
}

        分析:坦克进入墙体后执行转向,但是转向并不会改变坦克的位置坐标(坦克还在墙体里),转向只是修改了speedx和speedy的值,没有改变x和y的值,所以坦克根据新的方向执行移动move(),坦克有可能还是卡在墙体里,下一次碰撞检测发现坦克还是在墙体里,如此循环,只要坦克有一帧在墙里,每一帧都会强制改变方向,从而造成屏幕效果:坦克冲进墙里,像触电一样疯狂改变方向,彻底卡在墙里出不来了

        在改变方向前,先顶牛,先让坦克回到墙体外,安全的执行改变方向,不会卡墙

enemy.setX(enemy.getX() - enemy.getSpeedx());
enemy.setY(enemy.getY() - enemy.getSpeedy());

        关键就是:让改变方向时,确保坦克在一个安全的位置(在墙里改变方向,就会死循环)

开发经验:先解决空间重叠,再处理逻辑状态:第一优先级,永远是把对象退回到合法位置

完整过程:

for (int i = 0; i < gameUI.wallList.size(); i++) {
    for (int j = 0; j < gameUI.enemyTankList.size(); j++){
        EnemyTank et = gameUI.enemyTankList.get(j);
        if(gameUI.wallList.get(i).getRect().intersects(et.getRect())){
            System.out.println("敌军坦克撞墙");

            // 先让坦克回到安全位置,再改变方向
            et.setX(et.getX() - et.getSpeedx());
            et.setY(et.getY() - et.getSpeedy());

            et.changeDirection(); //改变方向
        }
    }
}

3.2 玩家子弹和墙体

        在GameUI中维护一个玩家子弹的列表 bulletList,遍历所有墙体和子弹列表,检测是否发生碰撞,如果碰撞,则让子弹死亡,isAlive = flase,

//循环判断每个墙体是否和子弹碰撞
for (int i = 0; i < gameUI.wallList.size(); i++) {
    for (int j = 0; j < gameUI.bulletsLsit.size(); j++) {
        Bullet bullet = gameUI.bulletsLsit.get(j);
        if(gameUI.wallList.get(i).getRect().intersects(bullet.getRect())){
            System.out.println("子弹和墙体发生了碰撞!");
            bullet.setAlive(false);
        }
    }
}

        在后续的子弹移除,会检测到死亡的子弹会被移除 bulletList

用子弹和墙体碰撞的过程为例,总结一下Java中的对象与引用

方案A:无意义的对象创建+内存浪费

new Bullet(),在堆内存中开辟一块新的内存空间,返回其引用(然后执行构造函数将基础的变量存进去)

bullet只是一个栈上的引用变量,存储对象的地址,

赋值操作 = 只是改变了引用指向,不复制对象 

刚才new的Bullet对象,失去了所有引用,成为了垃圾对象(无用内存)

        IDEA中,new Bullet(gameUI.playerTank)这段代码会变成灰色,是IEDA提醒这段代码是没必要的,但是实际执行时还会执行

for (int i = 0; i < gameUI.wallList.size(); i++) {
    for (int j = 0; j < gameUI.bulletsLsit.size(); j++) {
        Bullet bullet = new Bullet(gameUI.playerTank);
        bullet = gameUI.bulletsLsit.get(j);
        if(gameUI.wallList.get(i).getRect().intersects(bullet.getRect())){
            System.out.println("子弹和墙体发生了碰撞!");
            bullet.setAlive(false);
        }
    }
}

方案B(最优方案):高效引用复用

        

        创建一个新的引用 bullet,直接引用已有对象,没有额外内存分配,代码可读性高

        局部变量bullet在栈上,比访问gameUI.bulletList.get(i)寻址更快,提高访问速度

for (int i = 0; i < gameUI.wallList.size(); i++) {
    for (int j = 0; j < gameUI.bulletsLsit.size(); j++) {
        Bullet bullet = gameUI.bulletsLsit.get(j);
        if(gameUI.wallList.get(i).getRect().intersects(bullet.getRect())){
            System.out.println("子弹和墙体发生了碰撞!");
            bullet.setAlive(false);
        }
    }
}

方案C:复用方法调用,可读性差

        相比方案B,访问 gameUI.wallList.get(i) 比访问 bullet 更慢,代码冗长,易出错,违反了DRY原则(Don't Repeat Yourself)

for (int i = 0; i < gameUI.wallList.size(); i++) {
    for (int j = 0; j < gameUI.bulletsLsit.size(); j++) {
        if(gameUI.wallList.get(i).getRect().intersects(gameUI.bulletsLsit.get(j).getRect())){
            System.out.println("子弹和墙体发生了碰撞!");
            gameUI.bulletsLsit.get(j).setAlive(false);
        }
    }
}

4. 敌军坦克 EnemyTank 类

4.1 改变方向changeDirection方法

前面第1节已经介绍,在敌军坦克的碰撞到墙体时,需要随机转向

坦克和子弹不会斜着走,代表speedx和speedy两个方向,同一时间有一个为0

    // 改变方向:随机枚举一个方向,不同的方向有不同的speedx和speedy用来控制移动方向
    public void changeDirection(){
        Direction[] dirs = Direction.values(); // 返回所有枚举常量的数组
        Direction newDir = dirs[random.nextInt(dirs.length)];  //从数组中取出一个变量

        this.dir = newDir;
        if(dir == Direction.UP){
            speedx = 0;
            speedy = -GameConfig.ENEMY_SPEED;
        }else if(dir == Direction.DOWN){
            speedx = 0;
            speedy = GameConfig.ENEMY_SPEED;
        }else if(dir == Direction.LEFT){
            speedx = -GameConfig.ENEMY_SPEED;
            speedy = 0;
        }else if(dir == Direction.RIGHT){
            speedx = GameConfig.ENEMY_SPEED;
            speedy = 0;
        }
    }

4.2 子弹射击 fire()方法

每一个敌军坦克都有自己的 bulletList子弹列表

public ArrayList<Bullet> bulletList = new ArrayList<>();

fire()会添创建一个子弹,添加到子弹列表中

public void fire(){
        Bullet bullet = new Bullet(this);
        bulletList.add(bullet);
}

在绘制敌军坦克时,会有2%的概率调用坦克的fire()

用random.nextInt(100) < 2 控制概率

for (int i = 0; i < gameUI.enemyTankList.size(); i++) {
    //取出每一个坦克的子弹列表,然后移动和绘制子弹
    //每移动一步,只有2%的概率开火
    if(random.nextInt(100) < 2){
        gameUI.enemyTankList.get(i).fire();
    }
    gameUI.enemyTankList.get(i).move();
    gameUI.enemyTankList.get(i).draw(bg);
}

绘制敌军坦克的子弹,依旧遍历

// 绘制敌军坦克的子弹
for (int i = 0; i < gameUI.enemyTankList.size(); i++) {
    //取出每一个坦克的子弹列表,然后移动和绘制子弹
    for (int j = 0; j < gameUI.enemyTankList.get(i).bulletList.size(); j++) {
        Bullet b = gameUI.enemyTankList.get(i).bulletList.get(j);
        b.move();
        b.draw(bg);
    }
}

5. 子弹类的构造函数

子弹的发射位置、移动轨迹和方向都是在发射的一刻就决定了

所以在构造函数中:

  • 发射位置:x和y的初值
  • 移动轨迹和方向:dir、speedx,speedy

这些值需要在构造函数中直接赋值,其中dir就是owner发射子弹那一刻的方向

speedx和speedy是要根据dir来决定值,只有四种方向

public Bullet(Tank owner){
    super();
    this.owner = owner;
 //在创建的那一刻,就已经将owner的坐标值取了出来

    this.width = GameConfig.BULLET_SIZE;
    this.height = GameConfig.BULLET_SIZE;

    //获取 owner的初始方向,将初始方向保存
    this.dir = owner.getDir();


    //子弹的 移动方向 和 发射位置 在出生的时候就已经决定了  (在创建的那一刻,就已经将owner的坐标值取了出来)
    if(dir == Direction.UP){
        this.x = owner.getX();
        this.y = owner.getY() - this.height;
        this.speedx = 0;
        this.speedy = -GameConfig.BULLET_SPEED;
    }else if(dir == Direction.DOWN){
        this.x = owner.getX();
        this.y = owner.getY() + this.height;
        this.speedx = 0;
        this.speedy = GameConfig.BULLET_SPEED;
    }else if(dir == Direction.LEFT){
        this.x = owner.getX() - this.width;
        this.y = owner.getY();
        this.speedx = -GameConfig.BULLET_SPEED;
        this.speedy = 0;
    }else if(dir == Direction.RIGHT){
        this.x = owner.getX() + this.width;
        this.y = owner.getY();
        this.speedx = GameConfig.BULLET_SPEED;
        this.speedy = 0;
    }
}

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐