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對我的項目進行了理性的評估,內容如下