Skip to content

S06-05 IO流-项目:坦克大战

[TOC]

阶段一:绘制游戏地图和坦克

在动手敲代码之前,我们先来明确 Java 的绘图坐标体系。这和我们数学里的坐标轴有点不一样:

  • 坐标原点 (0, 0) 位于屏幕的左上角,以像素为单位。
  • x 轴向延伸,y 轴向延伸。

Java 帮我们把画画的组件封装好了。我们需要用到 JFrame(游戏大窗口)和 JPanel(我们的画布),以及 Graphics(这相当于我们的画笔)。

现在,请在你的 IDE 中创建一个项目 TankGame,然后创建以下三个核心类:

坦克的基类 Tank

这是所有坦克的祖先,目前它只需要记录坦克的 (x, y) 坐标。

java
package com.yourname.tankgame;

/**
 * 坦克的父类
 */
public class Tank {
  private int x; // 坦克的横坐标
  private int y; // 坦克的纵坐标

  public Tank(int x, int y) {
    this.x = x;
    this.y = y;
  }

  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; }
}

玩家的坦克 Hero

继承自 Tank,代表你亲自操控的英雄。

java
package com.yourname.tankgame;

/**
 * 我方坦克
 */
public class Hero extends Tank {
  public Hero(int x, int y) {
    super(x, y);
  }
}

游戏核心画布与主窗口 TankGame

这里我们把画布(MyPanel)和窗口合并到一个文件里,方便你运行。我们在里面手绘一辆精美的像素坦克!

java
package com.yourname.tankgame;

import javax.swing.*;
import java.awt.*;

/**
 * 坦克大战游戏主窗口
 */
public class TankGame extends JFrame {

  private MyPanel mp = null;

  public static void main(String[] args) {
    new TankGame();
  }

  public TankGame() {
    mp = new MyPanel();
    this.add(mp); // 把画布塞进窗口
    this.setSize(1000, 750); // 设置游戏窗口大小
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 点击关闭彻底退出程序
    this.setVisible(true); // 让窗口可见
  }
}

/**
 * 游戏绘图区域(画布)
 */
class MyPanel extends JPanel {
  Hero hero = null;

  public MyPanel() {
    hero = new Hero(100, 100); // 在坐标 (100, 100) 处初始化我方坦克
  }

  @Override
  public void paint(Graphics g) {
    super.paint(g);
    // 1. 铺上黑色的战场背景
    g.fillRect(0, 0, 1000, 750); //

    // 2. 绘制我方坦克(目前默认方向向上:0)
    drawTank(hero.getX(), hero.getY(), g, 0, 1); //
  }

  /**
   * 封装的绘制坦克方法
   * @param x 坦克左上角 x 坐标
   * @param y 坦克左上角 y 坐标
   * @param g 画笔
   * @param direct 坦克方向(0-上,1-右,2-下,3-左)
   * @param type 坦克类型(0-敌人,1-我方)
   */
  public void drawTank(int x, int y, Graphics g, int direct, int type) {
    // 根据类型设置颜色
    switch (type) {
      case 0: // 敌人
        g.setColor(Color.cyan); //
        break;
      case 1: // 我方
        g.setColor(Color.yellow); //
        break;
    }

    // 根据方向绘制坦克形状(目前只处理向上 0)
    switch (direct) {
      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 + 30, x + 20, y);  // 直线炮筒
        break;
      default:
        System.out.println("其他方向等我们下一关再来解锁!"); //
    }
  }
}

阶段二:多坦克渲染与键盘控制

请将你的项目代码按照以下逻辑进行升级:

升级基类 Tank

我们在基类中增加了 方向 (direct)速度 (speed) 属性,并封装了四个方向的移动方法。

java
package com.yourname.tankgame;

/**
 * 坦克父类(增强移动和方向控制)
 */
public class Tank {
  private int x; // 坦克的横坐标
  private int y; // 坦克的纵坐标
  private int direct = 0; // 坦克方向:0-上,1-右,2-下,3-左
  private int speed = 4;  // 坦克移动速度

  public Tank(int x, int y) {
    this.x = x;
    this.y = y;
  }

  // 四个方向的坐标改变方法
  public void moveUp() { y -= speed; }
  public void moveRight() { x += speed; }
  public void moveDown() { y += speed; }
  public void moveLeft() { x -= speed; }

  // Getter 和 Setter
  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 getDirect() { return direct; }
  public void setDirect(int direct) { this.direct = direct; }
  public int getSpeed() { return speed; }
  public void setSpeed(int speed) { this.speed = speed; }
}

新增敌人坦克类 EnemyTank

单独创建敌人坦克类,继承自 Tank,方便以后扩展敌人特有的 AI 逻辑。

java
package com.yourname.tankgame;

/**
 * 敌人坦克类
 */
public class EnemyTank extends Tank {
  public EnemyTank(int x, int y) {
    super(x, y);
  }
}

升级主程序与画布 TankGame

这是本次升级的核心。画布类 MyPanel 将实现 KeyListener 接口,用以监听键盘按键。同时,我们使用线程安全的 Vector 容器来统一管理敌人坦克。

java
package com.yourname.tankgame;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;

/**
 * 坦克大战 2.0 版
 */
public class TankGame extends JFrame {

  private MyPanel mp = null;

  public static void main(String[] args) {
    new TankGame();
  }

  public TankGame() {
    mp = new MyPanel();
    this.add(mp);
    this.setSize(1000, 750);

    // 【重要】让窗口监听键盘事件
    this.addKeyListener(mp);

    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setVisible(true);
  }
}

/**
 * 游戏绘图区域(实现 KeyListener 接口)
 */
class MyPanel extends JPanel implements KeyListener {
  Hero hero = null;
  // 使用 Vector 存储敌人坦克,确保多线程安全
  Vector<EnemyTank> enemyTanks = new Vector<>();
  int enemyTankSize = 3; // 敌人坦克数量

  public MyPanel() {
    // 初始化我方坦克
    hero = new Hero(500, 500);

    // 初始化敌人坦克
    for (int i = 0; i < enemyTankSize; i++) {
      // 让他们横向排开
      EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 100);
      enemyTank.setDirect(2); // 默认方向向下
      enemyTanks.add(enemyTank);
    }
  }

  @Override
  public void paint(Graphics g) {
    super.paint(g);
    // 铺上黑色背景
    g.fillRect(0, 0, 1000, 750);

    // 1. 绘制我方坦克(黄色)
    drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 1);

    // 2. 遍历 Vector 绘制所有敌人坦克(青色)
    for (int i = 0; i < enemyTanks.size(); i++) {
      EnemyTank enemyTank = enemyTanks.get(i);
      drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirect(), 0);
    }
  }

  /**
   * 增强版绘制坦克方法(支持多方向)
   */
  public void drawTank(int x, int y, Graphics g, int direct, int type) {
    switch (type) {
      case 0: g.setColor(Color.cyan); break;   // 敌人坦克
      case 1: g.setColor(Color.yellow); break; // 我方坦克
    }

    // 根据不同方向,绘制精细的坦克组件坐标
    switch (direct) {
      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 + 30, x + 20, y);  // 炮筒
        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;
      default:
        System.out.println("未知方向");
    }
  }

  // 键盘按下事件监听
  @Override
  public void keyPressed(KeyEvent e) {
    if (e.getKeyCode() == KeyEvent.VK_W) {       // 向上移动
      hero.setDirect(0);
      hero.moveUp();
    } else if (e.getKeyCode() == KeyEvent.VK_D) { // 向右移动
      hero.setDirect(1);
      hero.moveRight();
    } else if (e.getKeyCode() == KeyEvent.VK_S) { // 向下移动
      hero.setDirect(2);
      hero.moveDown();
    } else if (e.getKeyCode() == KeyEvent.VK_A) { // 向左移动
      hero.setDirect(3);
      hero.moveLeft();
    }

    // 【关键】每次改变坐标后,必须手动重绘面板,否则画面静止不变!
    this.repaint();
  }

  @Override
  public void keyTyped(KeyEvent e) {}
  @Override
  public void keyReleased(KeyEvent e) {}
}

运行与测试说明

  1. 多坦克渲染:启动游戏后,除了中央的黄色我方坦克,你的屏幕上方还会整齐地排开 3 辆青色、炮口朝下的敌人坦克。

  2. 键盘控制:在游戏窗口处于激活状态时,按下键盘上的 W、A、S、D,你会发现你的黄色坦克会实时调整车身和炮口的方向,并在黑色的战场中流畅地四处移动。

阶段三:炮火连天,线程出击

这一步是整个坦克大战最精彩、也是技术含量的最高阶段之一!我们要将多线程(Multithreading)技术完美融入项目中。

在之前的版本里,只有你按下键盘,画面才会刷新。但现在,子弹要飞、敌人要动、碰撞要实时检测,这就需要战场拥有一个每时每刻都在自行运转的“主心骨”——游戏主线程

根据项目设计要求,本阶段我们要攻克以下核心思路:

  1. 子弹(Shot)成为独立线程:按下 J 键发射,子弹自主向前飞行,撞墙或击中目标后消亡。

  2. 我方发射限制:控制弹药数量,最多同时发射 5 颗子弹。

  3. 敌人坦克自主乱动并开火:敌人坦克实现 Runnable 接口,随机改变方向、随机开火(最多 3 颗子弹)。

  4. 碰撞检测:主线程每时每刻都在计算“子弹是否击中坦克”,击中则令其消亡。

请在项目中补充或修改以下四个核心类:

新增子弹类 Shot

子弹必须实现 Runnable 接口,拥有自己的生命周期。

java
package com.yourname.tankgame;

/**
 * 子弹类:独立线程,负责子弹的轨迹计算
 */
public class Shot implements Runnable {
  private int x; // 子弹横坐标
  private int y; // 子弹纵坐标
  private int direct; // 子弹方向
  private int speed = 6; // 子弹速度
  private boolean isLive = true; // 子弹是否存活

  public Shot(int x, int y, int direct) {
    this.x = x;
    this.y = y;
    this.direct = direct;
  }

  @Override
  public void run() {
    while (isLive) {
      // 休眠一下,防止子弹飞得太快变成“瞬移”
      try {
        Thread.sleep(50);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }

      // 根据方向改变坐标
      switch (direct) {
        case 0: y -= speed; break; // 上
        case 1: x += speed; break; // 右
        case 2: y += speed; break; // 下
        case 3: x -= speed; break; // 左
      }

      // 边界检测:子弹飞出地图则销毁
      if (!(x >= 0 && x <= 1000 && y >= 0 && y <= 750)) {
        isLive = false;
        break;
      }
    }
  }

  // Getter & Setter
  public int getX() { return x; }
  public int getY() { return y; }
  public boolean isLive() { return isLive; }
  public void setLive(boolean live) { isLive = live; }
}

升级我方坦克 Hero

允许我方坦克发射子弹,并用 Vector 存储,限制同屏最多 5 颗。

java
package com.yourname.tankgame;

import java.util.Vector;

public class Hero extends Tank {
  // 存储我方发射的子弹
  Vector<Shot> shots = new Vector<>();

  public Hero(int x, int y) {
    super(x, y);
  }

  /**
   * 发射子弹的方法
   */
  public void fireShot() {
    // 控制最多发射 5 颗子弹
    if (shots.size() >= 5) {
      return;
    }

    Shot shot = null;
    // 根据当前坦克的方向和位置,计算炮口坐标作为子弹的起点
    switch (getDirect()) {
      case 0: shot = new Shot(getX() + 20, getY(), 0); break;
      case 1: shot = new Shot(getX() + 60, getY() + 20, 1); break;
      case 2: shot = new Shot(getX() + 20, getY() + 60, 2); break;
      case 3: shot = new Shot(getX(), getY() + 20, 3); break;
    }

    shots.add(shot);
    // 【关键】启动子弹线程
    new Thread(shot).start();
  }
}

升级敌人坦克 EnemyTank

敌人坦克不仅会随机移动,还会自动朝着当前炮口方向开火(最多 3 颗)。为了防止乱跑出界,增加了移动边界限制。

java
package com.yourname.tankgame;

import java.util.Vector;

/**
 * 敌人坦克:实现 Runnable,能自主随机移动和开火
 */
public class EnemyTank extends Tank implements Runnable {
  Vector<Shot> shots = new Vector<>();
  private boolean isLive = true;

  public EnemyTank(int x, int y) {
    super(x, y);
  }

  @Override
  public void run() {
    while (isLive) {
      // 如果子弹消亡了且同屏少于3颗,就补充子弹
      if (isLive && shots.size() < 3) {
        Shot s = null;
        switch (getDirect()) {
          case 0: s = new Shot(getX() + 20, getY(), 0); break;
          case 1: s = new Shot(getX() + 60, getY() + 20, 1); break;
          case 2: s = new Shot(getX() + 20, getY() + 60, 2); break;
          case 3: s = new Shot(getX(), getY() + 20, 3); break;
        }
        shots.add(s);
        new Thread(s).start();
      }

      // 根据当前方向,让坦克朝该方向走一段距离(比如30步)
      for (int i = 0; i < 30; i++) {
        try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); }

        // 带有边界限制的移动
        switch (getDirect()) {
          case 0: if (getY() > 0) moveUp(); break;
          case 1: if (getX() + 60 < 1000) moveRight(); break;
          case 2: if (getY() + 60 < 750) moveDown(); break;
          case 3: if (getX() > 0) moveLeft(); break;
        }
      }

      // 随机改变方向(0-3)
      setDirect((int) (Math.random() * 4));
    }
  }

  public boolean isLive() { return isLive; }
  public void setLive(boolean live) { isLive = live; }
}

升级画布与重构主程序 TankGame

让画布 MyPanel 实现 Runnable 接口作为游戏的主循环(每 100ms 刷新一次战场),并在此处理所有的子弹清除与碰撞检测。

java
package com.yourname.tankgame;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;

/**
 * 坦克大战 0.4 线程版
 */
public class TankGame extends JFrame {
  private MyPanel mp = null;

  public static void main(String[] args) {
    new TankGame();
  }

  public TankGame() {
    mp = new MyPanel();
    this.add(mp);
    this.setSize(1000, 750);
    this.addKeyListener(mp);

    // 【核心改变】启动画布(主游戏循环)线程
    new Thread(mp).start();

    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setVisible(true);
  }
}

/**
 * 游戏画布:作为主线程,负责刷新和碰撞检测
 */
class MyPanel extends JPanel implements KeyListener, Runnable {
  Hero hero = null;
  Vector<EnemyTank> enemyTanks = new Vector<>();
  int enemyTankSize = 3;

  public MyPanel() {
    hero = new Hero(500, 500);
    for (int i = 0; i < enemyTankSize; i++) {
      EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 100);
      enemyTank.setDirect(2);
      enemyTanks.add(enemyTank);
      // 【核心改变】启动敌人坦克线程,让他们自己动起来!
      new Thread(enemyTank).start();
    }
  }

  @Override
  public void run() {
    // 主线程循环:每 100ms 重绘并检测碰撞
    while (true) {
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }

      // 检测我方子弹是否击中敌人
      hitEnemyTank();
      // 重绘画面
      this.repaint();
    }
  }

  /**
   * 检测我方所有子弹是否击中敌方坦克
   */
  public void hitEnemyTank() {
    for (int i = 0; i < hero.shots.size(); i++) {
      Shot shot = hero.shots.get(i);
      if (shot != null && shot.isLive()) { // 子弹还活着
        for (int j = 0; j < enemyTanks.size(); j++) {
          EnemyTank enemyTank = enemyTanks.get(j);
          if (enemyTank.isLive()) {
            // 调用碰撞算法
            checkHit(shot, enemyTank);
          }
        }
      }
    }
  }

  /**
   * 具体碰撞算法:判断子弹是否进入坦克的矩形范围
   */
  public void checkHit(Shot s, EnemyTank tank) {
    // 判定坦克的上下左右边界(注意方向不同,宽高互换)
    int width = (tank.getDirect() == 0 || tank.getDirect() == 2) ? 40 : 60;
    int height = (tank.getDirect() == 0 || tank.getDirect() == 2) ? 60 : 40;

    if (s.getX() > tank.getX() && s.getX() < tank.getX() + width
        && s.getY() > tank.getY() && s.getY() < tank.getY() + height) {
      s.setLive(false);    // 子弹消亡
      tank.setLive(false); // 坦克消亡
      enemyTanks.remove(tank); // 从容器中移除敌人
    }
  }

  @Override
  public void paint(Graphics g) {
    super.paint(g);
    g.fillRect(0, 0, 1000, 750); // 黑色战场

    // 1. 绘制我方坦克
    drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 1);

    // 2. 绘制我方发射的所有有效子弹
    for (int i = 0; i < hero.shots.size(); i++) {
      Shot shot = hero.shots.get(i);
      if (shot != null && shot.isLive()) {
        g.fillOval(shot.getX() - 2, shot.getY() - 2, 4, 4); // 画个小圆形当子弹
      } else {
        hero.shots.remove(shot); // 清理死掉的子弹
      }
    }

    // 3. 绘制敌方坦克和他们的子弹
    for (int i = 0; i < enemyTanks.size(); i++) {
      EnemyTank enemyTank = enemyTanks.get(i);
      if (enemyTank.isLive()) {
        drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirect(), 0);

        // 遍历绘制该敌人的子弹
        for (int j = 0; j < enemyTank.shots.size(); j++) {
          Shot shot = enemyTank.shots.get(j);
          if (shot != null && shot.isLive()) {
            g.setColor(Color.cyan);
            g.fillOval(shot.getX() - 2, shot.getY() - 2, 4, 4);
          } else {
            enemyTank.shots.remove(shot); // 清理死子弹
          }
        }
      }
    }
  }

  // 绘制坦克的 drawTank 方法保持与第二步一致即可...
  public void drawTank(int x, int y, Graphics g, int direct, int type) {
    switch (type) {
      case 0: g.setColor(Color.cyan); break;
      case 1: g.setColor(Color.yellow); break;
    }
    switch (direct) {
      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 + 30, x + 20, y);
        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;
    }
  }

  @Override
  public void keyPressed(KeyEvent e) {
    if (e.getKeyCode() == KeyEvent.VK_W) { hero.setDirect(0); hero.moveUp(); }
    else if (e.getKeyCode() == KeyEvent.VK_D) { hero.setDirect(1); hero.moveRight(); }
    else if (e.getKeyCode() == KeyEvent.VK_S) { hero.setDirect(2); hero.moveDown(); }
    else if (e.getKeyCode() == KeyEvent.VK_A) { hero.setDirect(3); hero.moveLeft(); }

    // 【新增】监听 J 键发射子弹
    if (e.getKeyCode() == KeyEvent.VK_J) {
      hero.fireShot();
    }
  }

  @Override
  public void keyTyped(KeyEvent e) {}
  @Override
  public void keyReleased(KeyEvent e) {}
}

这一阶段的运行效果

  1. 游戏一启动,敌人坦克就会开始四处乱晃,并且会自动朝你发射青色的小子弹

  2. 你可以操控黄色坦克躲避,同时按下 J 键对准敌人开火。当你发出的子弹撞上敌人坦克时,敌人坦克会立刻从地图上消失

  3. 如果连续狂按 J 键,屏幕上最多只会有 5 颗你打出的子弹,达到了控弹效果。

阶段四:存档与音乐

在这一步中,我们将解决两个核心痛点:一是让游戏具有“记忆力”(使用 I/O 流实现持久化存储,记录击毁数和坦克坐标);二是让游戏具有“沉浸感”(使用多线程播放背景音乐)。

核心功能设计思路

  1. 数据持久化(I/O 流)

    • 创建一个 Recorder 记录类。当玩家退出游戏时,使用 BufferedWriter 将玩家的击毁敌方坦克数、地图上剩余敌人坦克的 (x, y, direct) 写入到文本文件 myRecord.txt 中。
    • 游戏启动时,通过弹窗让玩家选择“新游戏”或“继续上局游戏”。如果选择继续,则使用 BufferedReader 读取文件,恢复上局的进度。
  2. 多线程音频播放

    • 创建一个专门用于播放音频的线程类(如 AePlayWave),利用 Java 的 AudioSystem 读取 .wav 格式的音频文件,实现游戏开始时自动循环播放 BGM。

新增记录器类 Recorder

这个类负责处理所有与文件 I/O 相关的存盘与读盘逻辑。

java
package com.yourname.tankgame;

import java.io.*;
import java.util.Vector;

/**
 * 记录器类:负责持久化游戏数据与状态
 */
public class Recorder {
  // 记录我方击毁敌方坦克总数
  private static int allEnemyTankNum = 0;

  // I/O 流定义
  private static BufferedWriter bw = null;
  private static BufferedReader br = null;
  private static String recordFile = "src/myRecord.txt"; // 存档文件路径

  // 指向画布中的敌人坦克 Vector,用于获取存盘时的坐标
  private static Vector<EnemyTank> enemyTanks = null;
  // 用于恢复数据的容器
  private static Vector<Node> nodes = new Vector<>();

  public static void setEnemyTanks(Vector<EnemyTank> enemyTanks) {
    Recorder.enemyTanks = enemyTanks;
  }

  public static int getAllEnemyTankNum() { return allEnemyTankNum; }
  public static void setAllEnemyTankNum(int allEnemyTankNum) { Recorder.allEnemyTankNum = allEnemyTankNum; }

  // 击毁敌人坦克时,计数加 1
  public static void addAllEnemyTankNum() { Recorder.allEnemyTankNum++; }

  /**
   * 恢复数据:读取 myRecord.txt 文件
   */
  public static Vector<Node> getNodesAndRecord() {
    try {
      br = new BufferedReader(new FileReader(recordFile));
      // 读取第一行:击毁总数
      allEnemyTankNum = Integer.parseInt(br.readLine());

      // 循环读取后续行:敌人坦克的坐标和方向
      String line = "";
      while ((line = br.readLine()) != null) {
        String[] xyd = line.split(" ");
        Node node = new Node(
            Integer.parseInt(xyd[0]),
            Integer.parseInt(xyd[1]),
            Integer.parseInt(xyd[2])
        );
        nodes.add(node);
      }
    } catch (IOException e) {
      System.out.println("未找到历史存档,请开启新游戏!");
    } finally {
      try {
        if (br != null) br.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    return nodes;
  }

  /**
   * 存盘退出:将游戏数据写入文件
   */
  public static void keepRecord() {
    try {
      bw = new BufferedWriter(new FileWriter(recordFile));
      // 1. 写入击毁数
      bw.write(allEnemyTankNum + "\r\n");

      // 2. 遍历当前存活的敌人坦克,保存坐标和方向
      if (enemyTanks != null) {
        for (int i = 0; i < enemyTanks.size(); i++) {
          EnemyTank enemyTank = enemyTanks.get(i);
          if (enemyTank.isLive()) {
            String record = enemyTank.getX() + " " + enemyTank.getY() + " " + enemyTank.getDirect();
            bw.write(record + "\r\n");
          }
        }
      }
      System.out.println("游戏数据保存成功!");
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (bw != null) bw.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

/**
 * 辅助内部类:专门用于临时存放读取出来的坦克信息
 */
class Node {
  private int x;
  private int y;
  private int direct;

  public Node(int x, int y, int direct) {
    this.x = x;
    this.y = y;
    this.direct = direct;
  }

  public int getX() { return x; }
  public int getY() { return y; }
  public int getDirect() { return direct; }
}

新增音频播放类 AePlayWave

这是一个标准的声音流播放器线程,支持传入外部的 .wav 文件并独立于主游戏运行。

java
package com.yourname.tankgame;

import javax.sound.sampled.*;
import java.io.File;
import java.io.IOException;

/**
 * 音乐播放线程
 */
public class AePlayWave extends Thread {
  private String filename;

  public AePlayWave(String wavfile) {
    filename = wavfile;
  }

  @Override
  public void run() {
    File soundFile = new File(filename);
    if (!soundFile.exists()) {
      System.err.println("Wave file not found: " + filename);
      return;
    }

    AudioInputStream audioInputStream = null;
    try {
      audioInputStream = AudioSystem.getAudioInputStream(soundFile);
    } catch (UnsupportedAudioFileException | IOException e) {
      e.printStackTrace();
      return;
    }

    AudioFormat format = audioInputStream.getFormat();
    SourceDataLine auline = null;
    DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);

    try {
      auline = (SourceDataLine) AudioSystem.getLine(info);
      auline.open(format);
    } catch (LineUnavailableException e) {
      e.printStackTrace();
      return;
    }

    auline.start();
    int nBytesRead = 0;
    byte[] abData = new byte[512];

    try {
      while (nBytesRead != -1) {
        nBytesRead = audioInputStream.read(abData, 0, abData.length);
        if (nBytesRead >= 0) {
          auline.write(abData, 0, nBytesRead);
        }
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      auline.drain();
      auline.close();
    }
  }
}

升级主入口 TankGame

在主窗口启动时,我们加入一个选项对话框让玩家选择模式。同时,通过监听窗口关闭事件,在玩家关闭窗口时触发自动存盘。

java
package com.yourname.tankgame;

import javax.swing.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Scanner;

/**
 * 最终整合版:加入存盘与音乐控制
 */
public class TankGame extends JFrame {
  private MyPanel mp = null;

  public static void main(String[] args) {
    new TankGame();
  }

  public TankGame() {
    // 1. 游戏启动时提示选择
    String key = JOptionPane.showInputDialog("请选择: 1. 新游戏  2. 继续上局游戏");
    if (key == null) key = "1"; // 默认开启新游戏

    mp = new MyPanel(key);
    this.add(mp);
    this.setSize(1000, 750);
    this.addKeyListener(mp);

    new Thread(mp).start(); // 启动主刷新线程

    // 2. 播放游戏背景音乐
    // 请确保在项目根目录或指定路径下放一个真实的 111.wav 音乐文件
    new AePlayWave("src/111.wav").start();

    // 3. 【核心】监听窗口关闭,关闭时进行自动存盘
    this.addWindowListener(new WindowAdapter() {
      @Override
      public void windowClosing(WindowEvent e) {
        Recorder.keepRecord(); // 执行存盘
        System.exit(0);
      }
    });

    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setVisible(true);
  }
}

升级游戏画布类 MyPanel

修改 MyPanel 的构造函数,让它根据玩家的选择来决定是生成全新的敌人,还是从 Recordernodes 列表中读取上局的坐标进行还原。

java
// 在 MyPanel 类中修改构造器:
public MyPanel(String key) {
  hero = new Hero(500, 500);

  // 将当前面板的敌人容器交给 Recorder 集中管理
  Recorder.setEnemyTanks(enemyTanks);

  if (key.equals("2")) { // 继续上局游戏
    Vector<Node> nodes = Recorder.getNodesAndRecord();
    for (int i = 0; i < nodes.size(); i++) {
      Node node = nodes.get(i);
      EnemyTank enemyTank = new EnemyTank(node.getX(), node.getY());
      enemyTank.setDirect(node.getDirect());
      enemyTanks.add(enemyTank);
      new Thread(enemyTank).start();
    }
  } else { // 开启全新游戏
    for (int i = 0; i < enemyTankSize; i++) {
      EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 100);
      enemyTank.setDirect(2);
      enemyTanks.add(enemyTank);
      new Thread(enemyTank).start();
    }
  }
}

// 别忘了:在你的 checkHit 击毁敌方坦克的逻辑中,成功击中后要加上一句话:
// Recorder.addAllEnemyTankNum(); // 战绩加 1

最终版本测试指南

  1. 音乐起航:准备一个 .wav 格式的音效文件,重命名为 111.wav 放在 src/ 目录下。启动游戏,激昂的战歌就会随之响起!

  2. 战绩与存盘:进入游戏后干掉几辆敌方坦克,然后直接点击窗口的 X 按钮关闭游戏。此时去查看你的 src/ 目录,你会发现自动生成了一个 myRecord.txt 文件,里面精确保存了你的击毁总数和战场残余敌人的坐标。

  3. 完美复活:再次启动游戏,在弹窗中输入 2。瞧!上一局没打完的敌人坦克会在它们原本断气/停驻的位置重新复活,战绩也得到了继承。

至此,一个功能完整、技术全面的 Java 坦克大战迭代项目 就彻底完工了!你从零搭建了坐标系、引入了键盘监听、攻克了多线程并发子弹、最终用 I/O 流锁住了数据。这绝对是你 Java 进阶之路上里程碑式的一步。