6. 游戏引擎类:
6.1 完整源码展示:
import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.ArrayList; import java.util.HashSet; import java.util.Random; import java.util.Set;public class GamePanel extends JPanel implements KeyListener {//GamePanel类是游戏的核心控制器,负责管理游戏循环、输入处理和游戏状态更新。private TankA tankA;private TankB tankB;private final Set<Integer> pressedKeys = new HashSet<>();private final Timer gameTimer;private final Random ran = new Random();private final BattleMaps map;private final scorePanel sPanel;private final ArrayList<Bullet> bullets = new ArrayList<>();private boolean gameOver = false;private String winner = "";public GamePanel() {map = new BattleMaps();sPanel = new scorePanel();// 生成坦克A的合法位置tankA = generatePositionA(45, 35);tankB = generatePositionB(45, 35);// 生成坦克B的合法位置(且不与A重叠)// 初始化游戏定时器(每16ms≈60FPS)//使用游戏循环(Timer)来定期处理按键状态,更新坦克位置。gameTimer = new Timer(7, e -> {processInput();// 处理输入updateGame();// 更新游戏状态SwingUtilities.invokeLater(this::repaint);// 请求重绘});gameTimer.start();setFocusable(true);addKeyListener(this);}private TankA generatePositionA(int width, int height) {Random ran = new Random();Rectangle tempRect;int x, y;do {x = 120 + ran.nextInt(900);y = 60 + ran.nextInt(750);tempRect = new Rectangle(x, y, width, height);} while (map.isCollidingWithWall(tempRect)); // 确保不生成在墙上return new TankA(x, y); // 或 TankB}private TankB generatePositionB(int width, int height) {Random ran = new Random();Rectangle tempRect;int x, y;do {x = 120 + ran.nextInt(900);y = 60 + ran.nextInt(750);tempRect = new Rectangle(x, y, width, height);} while (map.isCollidingWithWall(tempRect)); // 确保不生成在墙上return new TankB(x, y); // 或 TankB}private void processInput() {// 游戏结束时忽略所有输入if (gameOver) return;// 处理坦克A// 每次循环先重置速度tankA.setSpeedX(0);tankA.setSpeedY(0);// 根据当前按下的键设置速度if (pressedKeys.contains(KeyEvent.VK_A)) {tankA.setDirection(0);tankA.setSpeedX(-4);}if (pressedKeys.contains(KeyEvent.VK_D)) {tankA.setDirection(2);tankA.setSpeedX(4);}if (pressedKeys.contains(KeyEvent.VK_W)) {tankA.setDirection(1);tankA.setSpeedY(-4);}if (pressedKeys.contains(KeyEvent.VK_S)) {tankA.setDirection(3);tankA.setSpeedY(4);}// 处理坦克BtankB.setSpeedX(0);tankB.setSpeedY(0);if (pressedKeys.contains(KeyEvent.VK_LEFT)) {tankB.setDirection(0);tankB.setSpeedX(-4);}if (pressedKeys.contains(KeyEvent.VK_RIGHT)) {tankB.setDirection(2);tankB.setSpeedX(4);}if (pressedKeys.contains(KeyEvent.VK_UP)) {tankB.setDirection(1);tankB.setSpeedY(-4);}if (pressedKeys.contains(KeyEvent.VK_DOWN)) {tankB.setDirection(3);tankB.setSpeedY(4);}}private void updateGame() {if (gameOver) {return;}handleTankMovement(tankA);handleTankMovement(tankB);//更新子弹位置for (Bullet bullet : new ArrayList<>(bullets)) {bullet.move();//检测子弹与墙壁的碰撞if (map.isCollidingWithWall(bullet.getBounds())) {bullet.setActive(false);}//检测子弹与坦克碰撞if (bullet.isActive()) {if (bullet.isFormTankA() && bullet.getBounds().intersects(tankB.getBounds())) {gameOver = true;winner = "-TankA-";bullet.setActive(false);showGameOver();} else if (!bullet.isFormTankA() && bullet.getBounds().intersects(tankA.getBounds())) {gameOver = true;winner = "-TankB-";bullet.setActive(false);showGameOver();}}}bullets.removeIf(bullet -> !bullet.isActive());//移除不活跃的子弹}private void handleTankMovement(MoveObjects tank) {//保存移动前的位置int oldX = tank.getX();int oldY = tank.getY();//移动坦克tank.move();// 获取移动后的碰撞区域Rectangle newBounds = tank.getBounds();//检测是否会与墙体/敌方坦克发生碰撞if (map.isCollidingWithWall(newBounds)) {//碰撞后回退位置并重置速度tank.setX(oldX);tank.setY(oldY);tank.setSpeedX(0);tank.setSpeedY(0);} else if (tankA.getBounds().intersects(tankB.getBounds())) {tank.setX(oldX);tank.setY(oldY);tank.setSpeedX(0);tank.setSpeedY(0);}}private void showGameOver() {pressedKeys.clear();// 在显示对话框前清除按键状态SwingUtilities.invokeLater(() -> {int option = JOptionPane.showConfirmDialog(this, " " + winner + " Wins!!!\n WANT PLAY AGAIN?", "--Game Over--", JOptionPane.YES_NO_OPTION);if (option == JOptionPane.YES_OPTION) {resetGame();} else {System.exit(0);}});}private void resetGame() {// 重置游戏前再次确保清除按键状态pressedKeys.clear();tankA = generatePositionA(45, 35);tankB = generatePositionB(45, 35);bullets.clear();winner = "";gameOver = false;requestFocusInWindow();}@Overrideprotected void paintComponent(Graphics g) {//自动启用Swing双缓冲,避免闪烁super.paintComponent(g);// 清空背景,清除前一帧画面 确保每次绘制都是全新的画面,避免画面残留//底层原理:默认会使用组件的背景色填充整个区域Graphics2D g2d = (Graphics2D) g.create();//创建图形上下文副本map.paintMap(g2d);tankA.drawTankA(g2d);tankB.drawTankB(g2d);//绘制所有子弹for (Bullet bullet : bullets) {bullet.draw(g2d);}sPanel.drawTankPicture(g2d);g2d.dispose();//保证图形状态隔离}private Bullet createBullet(MoveObjects tank, boolean fromTankA) {int tankHeadX;int tankHeadY;if (tank.getDirection() == 0 || tank.getDirection() == 2) {tankHeadX = tank.getX() + (tank.getWidth() / 2);tankHeadY = tank.getY() + (tank.getHeight() / 2) - 2;} else {tankHeadX = tank.getX() + (tank.getHeight() / 2) - 2;tankHeadY = tank.getY() + (tank.getWidth() / 2);}return new Bullet(tankHeadX, tankHeadY, tank.getDirection(), fromTankA);}@Overridepublic void keyPressed(KeyEvent e) {pressedKeys.add(e.getKeyCode());//添加子弹发射功能if (e.getKeyCode() == KeyEvent.VK_Q) {bullets.add(createBullet(tankA, true));} else if (e.getKeyCode() == KeyEvent.VK_SLASH) {bullets.add(createBullet(tankB, false));}}@Overridepublic void keyReleased(KeyEvent e) {if (gameOver) {pressedKeys.clear();// 在显示对话框前清除按键状态} else {pressedKeys.remove(e.getKeyCode());}// 处理平滑停止(可选)processInput();}@Overridepublic void keyTyped(KeyEvent e) {} } /*注意:(1)Swing的绘图应该在事件分派线程(EDT)中进行,使用多线程可能导致不可预测的行为。因此,应该将所有绘图逻辑放在一个主循环中,使用Swing的Timer来定期触发重绘,而不是在独立的线程中使用while循环和Thread.sleep(2)KeyListener的keyPressed事件机制设计为单次触发模式,无法跟踪组合按键状态 当同时按下多个键时,操作系统会快速交替触发多个keyPressed事件,但无法保持持续状态(3)Swing使用被动绘制机制,应重写paintComponent()方法getGraphics()获取的是临 时图形上下文,无法持久化 */
6.2 思路与架构
6.2.1 类的设计思路
- 由于GamePanel类承载着我们整个游戏流程控制的核心方法, 核心可移动的物体(对象)部署以及游戏状态的实时检测与控制, 这决定了本类需要继承JPanel父类并且实现KeyListener监听器, 同时需要传入整个游戏项目中的核心对象作为私有参数; 简洁起见我们尽可能将游戏循环封装在类的无参构造器中便于主类在实例化对象时直接启动.
6.2.2 核心属性梳理
- 为了便于本类的众方法对对象的操作, 我们尽可能将所需的对象设置为全局变量, 将需要和GamePanel同时创建出来的对象于无参构造器中进行初始化
private TankA tankA;private TankB tankB;private final Set<Integer> pressedKeys = new HashSet<>();private final Timer gameTimer;private final Random ran = new Random();private final BattleMaps map;private final scorePanel sPanel;private final ArrayList<Bullet> bullets = new ArrayList<>();private boolean gameOver = false;private String winner = "";public GamePanel() {map = new BattleMaps();sPanel = new scorePanel();// 生成坦克A的合法位置tankA = generatePositionA(45, 35);tankB = generatePositionB(45, 35);// 生成坦克B的合法位置(且不与A重叠)// 初始化游戏定时器(每16ms≈60FPS)//使用游戏循环(Timer)来定期处理按键状态,更新坦克位置。gameTimer = new Timer(7, e -> {processInput();// 处理输入updateGame();// 更新游戏状态SwingUtilities.invokeLater(this::repaint);// 请求重绘});gameTimer.start();setFocusable(true);addKeyListener(this);}
6.3 核心方法梳理与分析
a. 坦克A/B坐标的初始化
- 创建一个随机器, 在地图的左上角到右下角的有效空间内随机生成坦克的x, y坐标, 利用BattleMap类中的: isCollidingWithWall()方法循环判断是否生成在了合法位置, 最终返回一个新的坦克对象(x,y)
private TankA generatePositionA(int width, int height) {Random ran = new Random();Rectangle tempRect;int x, y;do {x = 120 + ran.nextInt(900);y = 60 + ran.nextInt(750);tempRect = new Rectangle(x, y, width, height);} while (map.isCollidingWithWall(tempRect)); // 确保不生成在墙上return new TankA(x, y); // 或 TankB}
b. 处理键盘对坦克的操作
- 首先判断游戏的状态是否结束, 每次循环处理输入时先将坦克的速度置零, 根据当前按下的键设置坦克的方向与速度(A/B同理)
private void processInput() {// 游戏结束时忽略所有输入if (gameOver) return;// 处理坦克A// 每次循环先重置速度tankA.setSpeedX(0);tankA.setSpeedY(0);// 根据当前按下的键设置速度if (pressedKeys.contains(KeyEvent.VK_A)) {tankA.setDirection(0);tankA.setSpeedX(-4);}if (pressedKeys.contains(KeyEvent.VK_D)) {tankA.setDirection(2);tankA.setSpeedX(4);}if (pressedKeys.contains(KeyEvent.VK_W)) {tankA.setDirection(1);tankA.setSpeedY(-4);}if (pressedKeys.contains(KeyEvent.VK_S)) {tankA.setDirection(3);tankA.setSpeedY(4);}
c. 重写键盘监听器方法(点按, 按下, 松开)
- 我们在设置全局变量时将pressedKey设置为了HashSet类型, 这就代表着我们不允许两个键盘指令被加入到Set中, 保证了对坦克操作的特定性.
- 1. 按下: 每当按下键盘时获取指令并传入Set中, 如果是子弹发射键则创建新的子弹对象并传入bullets的ArrayList中进行绘制.
- 2. 松开: 每当松开当前按下的键时, 如果游戏结束直接清除Set中所有的键盘指令, 如果没有则调用remove()方法移除当前指令.
- 3. 点按: 无需此操作.
@Overridepublic void keyPressed(KeyEvent e) {pressedKeys.add(e.getKeyCode());//添加子弹发射功能if (e.getKeyCode() == KeyEvent.VK_Q) {bullets.add(createBullet(tankA, true));} else if (e.getKeyCode() == KeyEvent.VK_SLASH) {bullets.add(createBullet(tankB, false));}}@Overridepublic void keyReleased(KeyEvent e) {if (gameOver) {pressedKeys.clear();// 在显示对话框前清除按键状态} else {pressedKeys.remove(e.getKeyCode());}// 处理平滑停止(可选)processInput();}@Overridepublic void keyTyped(KeyEvent e) {}
d. 控制坦克移动
- 以我们的MoveObject类作为作为参数, 便于坦克状态的集中控制, 移动前我们先获取坦克的当前位置便于给碰撞方法传递参数, 之后调用移动坦克方法并获取移动后坦克矩形的外边界, 将边界传入碰撞检测方法检测坦克是否与墙或者另一坦克发生碰撞, 如果是则将坐标设为刚才获取的位置并将速度置零.
private void handleTankMovement(MoveObjects tank) {//保存移动前的位置int oldX = tank.getX();int oldY = tank.getY();//移动坦克tank.move();// 获取移动后的碰撞区域Rectangle newBounds = tank.getBounds();//检测是否会与墙体/敌方坦克发生碰撞if (map.isCollidingWithWall(newBounds)) {//碰撞后回退位置并重置速度tank.setX(oldX);tank.setY(oldY);tank.setSpeedX(0);tank.setSpeedY(0);} else if (tankA.getBounds().intersects(tankB.getBounds())) {tank.setX(oldX);tank.setY(oldY);tank.setSpeedX(0);tank.setSpeedY(0);}}
e. 创建子弹
- 首先确定返回值类型为Bullet类 , 设置参数为MoveObject对象与子弹来源; 通过简单计算确定子弹创建的位置与坦克当前的位置的偏差, 最终返回新的子弹对象
private Bullet createBullet(MoveObjects tank, boolean fromTankA) {int tankHeadX;int tankHeadY;if (tank.getDirection() == 0 || tank.getDirection() == 2) {tankHeadX = tank.getX() + (tank.getWidth() / 2);tankHeadY = tank.getY() + (tank.getHeight() / 2) - 2;} else {tankHeadX = tank.getX() + (tank.getHeight() / 2) - 2;tankHeadY = tank.getY() + (tank.getWidth() / 2);}return new Bullet(tankHeadX, tankHeadY, tank.getDirection(), fromTankA);}
f. 组件可视化方法(游戏画面的核心)
- 直接先看代码我慢慢解释
@Overrideprotected void paintComponent(Graphics g) {//自动启用Swing双缓冲,避免闪烁super.paintComponent(g);// 清空背景,清除前一帧画面 确保每次绘制都是全新的画面,避免画面残留//底层原理:默认会使用组件的背景色填充整个区域Graphics2D g2d = (Graphics2D) g.create();//创建图形上下文副本map.paintMap(g2d);tankA.drawTankA(g2d);tankB.drawTankB(g2d);//绘制所有子弹for (Bullet bullet : bullets) {bullet.draw(g2d);}sPanel.drawTankPicture(g2d);g2d.dispose();//保证图形状态隔离}
paintComponent
是 Swing 组件绘制的核心方法,负责组件的可视化呈现。以下是简明扼要的解释:- 1. 基本概念
- 作用: 自定义组件的绘制逻辑
- 继承关系:
JComponent.paint()
→paintComponent()
- 典型实现:
@Override protected void paintComponent(Graphics g) {super.paintComponent(g); // 1. 清空背景// 2. 自定义绘制代码 }
- 2. 触发调用:
- 3. 关键应用:
super.paintComponent(g); // 清屏Graphics2D g2d = (Graphics2D)g;map.paintMap(g2d); // 1. 绘制地图(底层)tankA.draw(g2d); // 2. 绘制坦克(中层) bullets.forEach(b->b.draw(g2d)); // 3. 绘制子弹(上层)
g. 刷新游戏
- 刷新的东西无非就是子弹和坦克:
- 首先判断游戏是否结束, 如没有则首先控制当前坦克的动作; 其次遍历List更新子弹位置, 移动子弹, 获取子弹的矩形边界 判断子弹是否与墙壁发生碰撞 如碰则直接设置为毁灭; 之后分别判断子弹边界与坦克A,B是否重合, 根据情况设置游戏状态,赢家, 展示游戏结束画面
private void updateGame() {if (gameOver) {return;}handleTankMovement(tankA);handleTankMovement(tankB);//更新子弹位置for (Bullet bullet : new ArrayList<>(bullets)) {bullet.move();//检测子弹与墙壁的碰撞if (map.isCollidingWithWall(bullet.getBounds())) {bullet.setActive(false);}//检测子弹与坦克碰撞if (bullet.isActive()) {if (bullet.isFormTankA() && bullet.getBounds().intersects(tankB.getBounds())) {gameOver = true;winner = "-TankA-";bullet.setActive(false);showGameOver();} else if (!bullet.isFormTankA() && bullet.getBounds().intersects(tankA.getBounds())) {gameOver = true;winner = "-TankB-";bullet.setActive(false);showGameOver();}}}bullets.removeIf(bullet -> !bullet.isActive());//移除不活跃的子弹}
h. 重置游戏
- 关键难点: pressedKeys.clear();
- 在游戏结束时(A坦克被击中时), 如果玩家A仍然按着 "W" 键, 随着游戏进程结束这个元素不会被remove()掉, 因此在游戏重新开始时为了防止某个坦克不受控制的向某个方向移动, 应清空Set内所有的键盘指令
private void resetGame() {// 重置游戏前再次确保清除按键状态pressedKeys.clear();tankA = generatePositionA(45, 35);tankB = generatePositionB(45, 35);bullets.clear();winner = "";gameOver = false;requestFocusInWindow();}
i. 游戏结束选择盘
应用: SwingUtilities.invokeLater 来启动EDT事件调度, 防止展示结束界面进入游戏主线程
private void showGameOver() {pressedKeys.clear();// 在显示对话框前清除按键状态SwingUtilities.invokeLater(() -> {int option = JOptionPane.showConfirmDialog(this, " " + winner + " Wins!!!\n WANT PLAY AGAIN?", "--Game Over--", JOptionPane.YES_NO_OPTION);if (option == JOptionPane.YES_OPTION) {resetGame();} else {System.exit(0);}});}
7. 装饰界面
- 逻辑类似于坦克图片的绘制: 独立于GamePanel便于后续增加有趣的功能
private final ImageIcon[] imgPic = new ImageIcon[2];public void drawTankPicture(Graphics2D g2d) {imgPic[0] = new ImageIcon("D:\\桌面\\Xing\\Photos\\TankB.png");imgPic[1] = new ImageIcon("D:\\桌面\\Xing\\Photos\\TankA.png");g2d.drawImage(imgPic[0].getImage(), 230, 830, 100, 60, null);g2d.drawImage(imgPic[1].getImage(), 830, 830, 100, 60, null);}
8. Summary
- 我的坦克大战基于本地Java Swing框架, 通过gameTimer定时刷新界面实现了游戏的流畅运行,在本地实现双人对战非常刺激与流畅, 虽然并非契合主流游戏开发的流程与现有标准框架, 但是实现了一个教学级的坦克大战游戏开发全流程, 非常适合新手熟悉Java的Swing框架 和 面向对象中类的设计与关联的基本思维, 是一个有趣而简单的实战项目
核心点再析:
JFrame:游戏的主窗口容器,负责处理操作系统级事件(关闭、最小化等)
GamePanel:继承自
JPanel
的自定义组件,作为游戏绘制表面(Surface)双缓冲机制:Swing默认启用双缓冲,通过
RepaintManager
管理后台缓冲区绘制触发:
repaint()
调用会触发Swing的异步重绘请求,最终由事件派发线程(EDT)执行paintComponent
事件分发线程(EDT):所有输入事件由Swing的EDT统一派发
DeepSeek对我的项目进行了理性的评估,内容如下