iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
Software Development

30天從基礎學起Java,直到做出我的第一個遊戲系列 第 22

Day 22:Java Snake Game 增加遊戲功能(一)

  • 分享至 

  • xImage
  •  

今日內容:計分、重新開始、主畫面


計分

昨天我們打造完能夠遊玩的遊戲本體後,今天接著要做的就是繼續優化遊戲內容
我首先注意到的是遊戲執行期間應該要有一個計分表,告訴玩家目前已經獲得多少分
因此我打算先從這部分下手,我設置一個score的instance variable並在確定使用者吃到食物後修改數值

if(Objects.equals(snake.getFirst(), new Point(foodX, foodY))){
    ++score;
    newFood();
}

弄完計分設定,接下來要把它繪製到畫面上
我們在paintComponent中新增一個drawScore()

public void drawScore(Graphics2D g2d){
    g2d.setColor(scoreC);
    g2d.setFont(new Font("MV Boli", Font.PLAIN, 25));
    g2d.drawString("Score: " + score, 350, 50);
}

然後要稍微梳理一下繪製順序,避免有的組件被蓋掉
我們要先畫出蛇或食物,以免分數被這兩者蓋掉
而蛇跟食物的順序則應該要先畫食物再畫蛇,這樣當蛇要吃到食物時就可以把食物覆蓋過去

drawFood();
drawSnake();
drawScore();

image


重新開始

以上完成計分功能
接下來要做的是遊戲結束後的重新開始,畢竟沒有人會想要玩一次就重新打開這個應用程式
我的想法是在keyBindings中,設置一個變數用於接收空白鍵(space),當按下空白鍵時且遊戲不處於執行階段(isRunning == false)時就會重新開始

inputMap.put(KeyStroke.getKeyStroke((char)KeyEvent.VK_SPACE), "restartAction");
// Gemini告訴我可以用KeyEvent.VK_SPACE來接收空白鍵輸入

actionMap.put("restartAction", new AbstractAction(){
    @Override
    public void actionPerformed(ActionEvent e){
        if(!isRunning) startGame();
    }
});

接著,我們已經可以重新開始遊戲了,但是上一把遊戲的資料我們還沒進行清除
所以我們來到startGame()中,對特定資料進行重置

public void startGame(){
    score = 0;
    gameOverLabel.setVisible(false);
    newFood();
    initSnake();
    timer.start();
    isRunning = true;
    this.requestFocusInWindow();
}

蛇的身體需要重新繪製,所以我們透過initSnake()進行操作
我們需要先將原本的資料全部清除,然後再重新給予資料,所以我選擇先removeAll再重新add

public void initSnake(){
    snake.clear(); // 新增這行用於清除資料
    for(int i=6; i>0; --i){
        snake.add(new Point(i*BLOCK_SIZE, 4*BLOCK_SIZE)); // add往list後加
    }
}

接著我發現儘管我修改了initSnake(),我的遊戲還是會有一點問題:新生成的snake會撞到舊生成的snake!
我找了一陣子也沒看出問題,於是我向gemini求助,它讓我檢查我的startGame()
image
於是我發現,我的direction並沒有進行重置
因此我又修改我的startGame(),將direction重設為'd',並一同重設了directionChanged為false

至此,我的遊戲可以正常重新執行了,只是我的遊戲會在完全清空上一把畫面前就開始下一把,這會導致畫面不太流暢
自行查找bug花了一些時間,後來終於想到
縱使我在startGame()裡面做了很多畫面更新與修改,但我並沒有加上repaint(),這導致我所有的修改都需要靠等到timer更新之後才會進行更改,才會使得我感覺畫面不流暢
因此我在startGame()最下面加上repaint()後,一切就都符合我的預期了。


主畫面

為了讓使用者在打開應用程式之後可以有一點反應空間,我打算在點開應用程式之後先出現一個主畫面,告知使用者例如"Press space to start",然後再開始遊戲

因此我接下來要做的就是設定新的JLabel,命名為startLabel,並將其加在setLabels()裡面

public void setLabels(){

this.setLayout(null);
startLabel = new JLabel();
startLabel.setFont(new Font("MV Boli", Font.BOLD, 50));
startLabel.setBounds(100, 200, 800, 100);
startLabel.setText("Press Space To Start");
startLabel.setForeground(Color.WHITE);
startLabel.setVisible(true);
    
// gameOverLabel settings
}

設定完後我發現一個問題,在打開應用程式後,就算我還沒開始遊戲,score和食物都會先被畫出來,這是不應該發生的,所以我向Gemini再次求助
image
我才想起paintComponent指的是panel建立後就會先執行一次的method,因此我使用之前的isRunning變數,確認執行後再畫上分數跟食物

if(isRunning){
    drawFood(g2d);
    drawSnake(g2d);
    drawScore(g2d);
}

接著,我本來打算讓一個label顯示兩行文字,因此我嘗試用'\n',可是發現並沒有效果
因此我再次向Gemini求助
image

由於使用多個Label會降低寫程式時的可讀性,既然兩行的屬性相同,那我就直接使用html語法就好

startLabel2 = new JLabel();
startLabel2.setFont(new Font("MV Boli", Font.BOLD, 50));
startLabel2.setText("<html>Press Space To Start<br>Use WASD To Control</html>");
startLabel2.setForeground(Color.WHITE);
startLabel2.setVisible(true);

image

不過後來我想將成績顯示在結束畫面,最初我是這樣寫的

gameOverLabel2.setText("<html>Press Space To Restart<br>Your Final Score Is: " + score + "</html>");

但是由於我的程式設計不對,他永遠都是顯示為0

找了很久,最後才發現,我是將gameOverLabel2設置在constructor中,因此他只會在建構時被設定一次,且score值為0
後來我將setText改為放在gameOver()中,這樣每次遊戲結束就會更改分數,然後顯示在畫面上了
image


上一篇
Day 21:Java Snake Game 結束邏輯與打包
系列文
30天從基礎學起Java,直到做出我的第一個遊戲22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言