今天是開始實作我的Snake Game的第一天,會從建立遊戲畫面與靜態畫面生成開始!
一開始先建立視窗(frame)跟畫布(panel)
import javax.swing.JFrame;
public class GameFrame extends JFrame{
GameFrame(){
this.add(new GamePanel());
this.setTitle("SnakeGame");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setResizable(false);
this.pack();
this.setVisible(true);
this.setLocationRelativeTo(null);
}
}
接著是panel,由於我們要讓panel可以畫東西,我們先Override他的繪製函式 (paintComponent)
最一開始設定是1300x750的畫面大小
import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Dimension;
import java.awt.Color;
public class GamePanel extends JPanel{
static final int PANEL_WIDTH = 1300;
static final int PANEL_HEIGHT = 750;
GamePanel(){
this.setPreferredSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT));
this.setBackground(Color.black);
this.setOpaque(true);
this.setFocusable(true);
}
@Override
public void paintComponent(Graphics g){
Graphics2D g2d = (Graphics2D) g;
// 將Graphics轉換為Graphics2D
}
}
接著設定paintComponent的內容,我想讓他在畫布上畫出線條,於是用while迴圈計算並用drawLine進行繪圖
@Override
public void paintComponent(Graphics g){
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.white);
int xLine = 0, yLine = 0;
while(xLine < PANEL_WIDTH){
g2d.drawLine(xLine, 0, xLine, PANEL_HEIGHT);
xLine += BLOCK_SIZE;
}
while(yLine < PANEL_HEIGHT){
g2d.drawLine(0, yLine, PANEL_WIDTH, yLine);
yLine += BLOCK_SIZE;
}
}
可是發現我的背景不如我預期設定好的黑色,因此向gemini詢問,告知我需要在第一行寫上super.paintComponent(g),呼叫預設繪製行為:
補上後就正常了
接下來要做的是繪製食物,我的想法是把食物繪製成紅色,貪食蛇繪製成綠色。
食物用random生成,不過在繼續執行之前,我選擇先將任務從paintComponent中分出,我另外寫了幾個method,再傳入paintComponent中,以增加程式的可讀性
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
drawGrid(g2d);
drawFood(g2d);
drawSnake(g2d);
}
接著稍微修改一下class宣告的變數,屬於各個物件都會固定的就設定為static final
後來我將寬跟高重新設置為800x500,否則畫面會有點太大
public class GamePanel extends JPanel{
static final int PANEL_WIDTH = 800;
static final int PANEL_HEIGHT = 500;
static final int BLOCK_SIZE = 50;
static final int xBlocks = PANEL_WIDTH / BLOCK_SIZE;
static final int yBlocks = PANEL_HEIGHT / BLOCK_SIZE;
int foodX = 0;
int foodY = 0;
Color food = Color.red;
}
接著要設定random,用來隨機食物生成位置
但是食物生成的代碼要放在哪裡?
如果將整個代碼放在建構子(constructor)
食物只會在物件建構時生成一次
如果將代碼放在drawFood()內 (也就是paintComponent內)
食物會一直閃爍,因為每次需要重繪頁面時他就會被呼叫,然後食物就會重畫一次
所以正確的做法是再寫一個newFood(),只在偵測到遊戲開始與食物被吃掉後執行
public void newFood(){
foodX = random.nextInt(xBlocks) * BLOCK_SIZE;
foodY = random.nextInt(yBlocks) * BLOCK_SIZE;
// random生成0~xBlocks-1,我們再乘上SIZE就會剛好填滿格子
}
那麼這個newFood()要放在哪呢?
我的想法是我們再寫一個startGame(),用來控制遊戲開始與開始後的操作
public void startGame(){
newFood();
}
而當我們生成新的食物之後就可以呼叫drawFood()將它畫出來
public void drawFood(Graphics2D g2d){
g2d.setColor(food);
g2d.fillRect(foodX, foodY, BLOCK_SIZE, BLOCK_SIZE);
// 由於只會在發生改變時產生新的食物,因此這個foodX和foodY在通常情況下不會隨意改變數值
// 因此當畫面需要重繪時,食物仍然在同一個位置,就不會有閃爍的情況發生了
}
接著嘗試之後,newFood()和drawFood()都正常了,就可以來準備設計貪食蛇本體了
我這邊是用LinkedList儲存snake,因為LinkedList非常適合用於需要頻繁插入與刪除的資料
而LinkedList要儲存哪種資料型態呢?
我本來的想法是int,可是gemini告訴我:
接著我問它要如何使用point,第一次了解這個型態,發現還蠻好上手的
好,所以現在我們知道LinkedList要存point,於是我設計讓他開局的長度為6,且永遠從畫面的中左生成,頭朝右
public void initSnake(){
for(int i=6; i>0; --i){
snake.add(new Point(i*BLOCK_SIZE, 4*BLOCK_SIZE));
// add會把資料往後加 -> index 0 -> index 1 -> index 2 ...
}
}
初始化完蛇身,我們要把這個蛇身畫在畫布上,這邊使用到enhanced for loop是比較好的選擇
public void drawSnake(Graphics2D g2d){
g2d.setColor(snakeC);
for(Point point : snake){
g2d.fillRect(point.x, point.y, BLOCK_SIZE, BLOCK_SIZE);
}
}
最後我們就成功把蛇畫在畫布上了!
這樣我們就完成第一天的考驗了,成功把內容繪製在畫布上還蠻有成就感的,畢竟這是自己一步一步探索與研究出來的。明天就會進入到動態邏輯的部分了!
天今天也是快樂學習的一天,明天繼續!