# JAVA - 貪吃蛇遊戲製作 ##### 061225 - by [Hy.C](https://hyc.eshachem.com/) --- 前端的習慣 - 拆成index與componets。 所以專案架構我會寫成 index.java與game.java ## index.java 定義視窗介面的地方 :::warning 貪吃蛇這個遊戲是有限制邊界的,所以**不允許**使用者**調整視窗大小** ::: * !一切準備完成後才能顯示介面setVisible(true)! * 遊戲componets -> 建構子: **game** ```java= import javax.swing.JFrame; // 建立視窗 public class index { public static void main(String[] args) { JFrame frame = new JFrame("貪吃蛇!!"); // 標題為 "貪吃蛇!!"的視窗 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 關閉視窗時就終止程式 frame.setResizable(false); // 禁止調整視窗大小 game gamePanel = new game();// 呼叫建構子 frame.add(gamePanel); // 將遊戲面板加入視窗中 frame.pack(); // 自動調整視窗大小以符合內容 frame.setLocationRelativeTo(null); // 視窗顯示於畫面中央 frame.setVisible(true); // 設定視窗可見 } } ``` ## game.java ### 匯入主要套件 * `javax.swing.*` : 用來建立視窗、按鈕、文字框、選單等GUI元件 * `java.awt.*` : 佈局管理器、顏色、字型、畫筆等 * `java.awt.event.*` : 事件偵測 * `java.util.Random` : 隨機產生(蘋果) ```java= import javax.swing.*; // GUI import java.awt.*; import java.awt.event.*; // 事件偵測 import java.util.Random; ``` --- ### 常數設定 需要設定的起始值,固定值: * 視窗大小`S_WIDTH`,`S_HEIGHT` * 單位大小`UNIT_SIZE` * 總單位數`T_UNITS` :::warning 因為是要做會動的,所以要設定刷新頻率`DELAY` ::: 因為是常數所以static表**類別層級**:所有物件都共同的東西 ```java= // 視窗大小 static final int S_WIDTH = 600; static final int S_HEIGHT = 600; static final int UNIT_SIZE = 25;// 單位大小 static final int T_UNITS = (S_WIDTH * S_HEIGHT) / (UNIT_SIZE * UNIT_SIZE); // 計算遊戲格數量 static final int DELAY = 50; // 遊戲渲染頻率(毫秒) ``` --- ### 變數設定 * 蛇的座標用陣列x,y控制,且最大長度就是整個頁面 ```java= final int x[] = new int[T_UNITS]; // 蛇每節身體的 X 座標 final int y[] = new int[T_UNITS]; // 蛇每節身體的 Y 座標 int bodyParts = 6; // 蛇的長度,起始為6 int cnt_apples = 0; // 吃到蘋果的數量 int appleX, appleY; // 蘋果的座標 char direction = 'R'; // 蛇的移動方向(R=右,L=左,U=上,D=下) boolean running = false; // 遊戲是否進行中 Timer timer; // Swing 的計時器控制遊戲循環 Random r; // 隨機物件,用來產生蘋果位置 ``` --- ### game建構子 在index.java呼叫時會自動執行,初始化整個遊戲介面: * 隨機物件 * 視窗介面 一切準備好後就可以開始了! ```java= public game() { r = new Random(); // 初始化隨機物件 setPreferredSize(new Dimension(S_WIDTH, S_HEIGHT)); // 設定面板尺寸 setBackground(Color.black); // 設定背景為黑色 setFocusable(true); // 允許焦點以接收鍵盤事件 addKeyListener(new MyKeyAdapter()); // 加入鍵盤監聽器 startGame(); // 開始遊戲! } ``` --- ### startGame 因為是會動的遊戲,需要不斷渲染, 所以會使用`Timer(T,觸發事件(ActionListener))`,表每T時間,我就執行ActionListener。 如果開始了一局新遊戲: * 隨機建立一個頻果 * 讓玩家移動蛇 * 檢查是否加分或結束 :::info 每100ms都要重新刷新畫面(來更新蛇的位置),會需要用到內建函式:`repaint()`; ::: 而這個repaint會自動去呼叫 `paintComponent(Graphics g)` ```java= public void startGame() { newApple(); // 產生新蘋果 running = true; // 設定遊戲進行中 timer = new Timer(DELAY, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (running) { move(); // 移動蛇 checkApple(); // 檢查是否吃到蘋果 checkCollisions(); // 檢查是否碰撞 } repaint(); // 更新蛇的位置 } }); timer.start(); // 開始計時器 } ``` --- ### paintComponent ```java= public void paintComponent(Graphics g) { super.paintComponent(g); draw(g); // 渲染畫面內容 } ``` `super.paintComponent(g)`: 有點像F5重新整理得概念,如果不寫這行,舊的圖形就不會被清掉,會一堆在畫面上 --- ### draw 所以畫面主要的樣式要寫在這 ```java= public void draw(Graphics g) { if (running) { g.setColor(Color.red); // 設定蘋果為紅色 g.fillOval(appleX, appleY, UNIT_SIZE, UNIT_SIZE); // 繪製蘋果 for (int i = 0; i < bodyParts; i++) { if (i == 0) { g.setColor(Color.green); // 蛇頭為綠色 g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE); // 畫出蛇頭 } else { g.setColor(new Color(45, 180, 0)); // 蛇身為深綠色 g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE); // 畫出蛇身 } } g.setColor(Color.white); // 設定文字顏色 g.setFont(new Font("", Font.BOLD, 25)); // 預設字體,放大 FontMetrics metrics = getFontMetrics(g.getFont()); // 取得字體寬度資訊 g.drawString("分數: " + cnt_apples, (S_WIDTH - metrics.stringWidth("分數: " + cnt_apples)) / 2, g.getFont().getSize()); // 畫出分數 } else { gameOver(g); // 若遊戲結束,畫出結束畫面 } } ``` --- ### gameOver 結束時的畫面樣式。 ```java= public void gameOver(Graphics g) { g.setColor(Color.red); // 設定顏色為紅色 g.setFont(new Font("", Font.BOLD, 25)); // 設定字體樣式 FontMetrics metrics1 = getFontMetrics(g.getFont()); g.drawString("得分: " + cnt_apples, (S_WIDTH - metrics1.stringWidth("得分: " + cnt_apples)) / 2, g.getFont().getSize()); // 畫出分數 g.setColor(Color.red); // 設定顏色為紅色 g.setFont(new Font("", Font.BOLD, 25)); // 設定字體樣式 FontMetrics metrics2 = getFontMetrics(g.getFont()); g.drawString("遊戲結束", (S_WIDTH - metrics2.stringWidth("遊戲結束")) / 2, S_HEIGHT / 2); // 畫出遊戲結束 } ``` :::warning 計算`字體寬度/2`是為了讓文字置中 ::: --- ### 判斷 在startGame中寫的加減分,頻果位置的判斷 #### newApple ```java= public void newApple() { appleX = r.nextInt((int)(S_WIDTH / UNIT_SIZE)) * UNIT_SIZE; // 隨機 X 座標位置(對齊格子) appleY = r.nextInt((int)(S_HEIGHT / UNIT_SIZE)) * UNIT_SIZE; // 隨機 Y 座標位置(對齊格子) } ``` #### move ```java= public void move() { for (int i = bodyParts; i > 0; i--) { x[i] = x[i - 1]; // 身體每節向前移動 y[i] = y[i - 1]; } switch (direction) { case 'U': y[0] = y[0] - UNIT_SIZE; break; // 向上移動 case 'D': y[0] = y[0] + UNIT_SIZE; break; // 向下移動 case 'L': x[0] = x[0] - UNIT_SIZE; break; // 向左移動 case 'R': x[0] = x[0] + UNIT_SIZE; break; // 向右移動 } } ``` #### checkApple ```java= public void checkApple() { if (x[0] == appleX && y[0] == appleY) { // 若蛇頭位置與蘋果相同 bodyParts++; // 增加身體長度 cnt_apples++; // 分數增加 newApple(); // 產生新蘋果 } } ``` #### checkCollisions ```java= public void checkCollisions() { for (int i = bodyParts; i > 0; i--) { if (x[0] == x[i] && y[0] == y[i]) { // 如果蛇頭撞到自己 running = false; // 結束遊戲 break; } } if (x[0] < 0 || x[0] >= S_WIDTH || y[0] < 0 || y[0] >= S_HEIGHT) { // 撞牆 running = false; } if (!running) timer.stop(); // 停止渲染 } ``` --- ### 鍵盤偵測 使用者一切的一切都建立在鍵盤偵測的操作上 ```java= public class MyKeyAdapter extends KeyAdapter { @Override // 複寫 KeyAdapter 的 keyPressed public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: if (direction != 'R') direction = 'L'; // 向左且不能原本是向右 break; case KeyEvent.VK_RIGHT: if (direction != 'L') direction = 'R'; break; case KeyEvent.VK_UP: if (direction != 'D') direction = 'U'; break; case KeyEvent.VK_DOWN: if (direction != 'U') direction = 'D'; break; } } } ```