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