iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Software Development

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

Day 20:Java Snake Game 動態邏輯(計時、移動、食物位置判斷)

  • 分享至 

  • xImage
  •  

在昨天我們完成靜態畫面後,目前只是一條不會動的綠色格子跟一個隨機生成的紅色方塊
所以今天我們要來處理動態的效果!

為了讓遊戲有「動起來」的效果,我們使用Timer,不過這個前面好像沒有學到,因此我現場問Gemini
image
了解如何使用之後,我在類別下宣告Timer timer; 然後在constructor中建立物件
這邊使用Anonymous Inner Class的方式傳遞listener參數

timer = new Timer(150, new ActionListener(){
    @Override
    public void actionPerformed(ActionEvent e){
        
    }
});

接著我們在startGame()中加入 timer.start(),這樣開始遊戲時就會開始計算時間
接著我們要完善actionPerformed
每150ms,我們要讓貪食蛇移動,那移動的邏輯就是根據當前方向,在蛇頭新增新格子,然後將末端的格子remove掉

要做到移動蛇身,我們首先需要知道方向

char direction = 'd'; // 使用wasd,而非WASD,否則會需要按著Shift

現在我們可以寫一個move(),用來控制蛇的移動

public void move(){
    Point head = snake.getFirst();
    int headX = head.x;
    int headY = head.y;
    switch(direction){
        case 'w' ->headY -= BLOCK_SIZE;
        case 'a' ->headX -= BLOCK_SIZE;
        case 's' ->headY += BLOCK_SIZE;
        case 'd' ->headX += BLOCK_SIZE;
    }
    snake.addFirst(new Point(headX, headY));
    snake.removeLast();
}

完成後將他加入timer的listener
由於我們修改了畫面,我們還要呼叫paintComponent,我們使用repaint()進行呼叫

timer = new Timer(150, new ActionListener(){
    @Override
    public void actionPerformed(ActionEvent e){
        move();
        repaint();
    }
});

設定完蛇的移動,我們接下來接收使用者的鍵盤輸入
我們用KeyBinding,而他會需要用到 InputMap負責把按鍵對應到一個名稱 和 ActionMap負責把名稱對應到一個動作

// 把 wasd 鍵的「被按下」事件,分別映射到對應的 "Action" 的名稱
inputMap.put(KeyStroke.getKeyStroke('w'), "upAction");
inputMap.put(KeyStroke.getKeyStroke('a'), "leftAction");
inputMap.put(KeyStroke.getKeyStroke('s'), "downAction");
inputMap.put(KeyStroke.getKeyStroke('d'), "rightAction");

而接下來要設定各個Action對應的動作,這邊我們一樣用到Anonymous Inner Class的概念,在不破壞封裝的情況下,我們可以直接存取Outside Class的資料

actionMap.put("upAction", new AbstractAction(){
    @Override
    public void actionPerformed(ActionEvent e){
        direction = 'w';
    }
});
actionMap.put("leftAction", new AbstractAction(){
    @Override
    public void actionPerformed(ActionEvent e){
        direction = 'a';
    }
});
actionMap.put("downAction", new AbstractAction(){
    @Override
    public void actionPerformed(ActionEvent e){
        direction = 's';
    }
});
actionMap.put("rightAction", new AbstractAction(){
    @Override
    public void actionPerformed(ActionEvent e){
        direction = 'd';
    }
});

只不過,以上這些設定按鍵的功能全部都需要在constructor內執行
但是這樣會導致可讀性降低,因此我們再寫一個setKeyBinding(),用來存放這些代碼,然後再在constructor內呼叫就好

現在基本的上下左右控制設定完了,我們要來做一些判斷
由於在貪食蛇中,180度轉彎是不被允許的,所以在調整方向的部份我們需要做一點if判斷

// 舉一個例子 剩下都一樣
actionMap.put("upAction", new AbstractAction(){
    @Override
    public void actionPerformed(ActionEvent e){
        if(direction != 's') direction = 'w';
    }
});

好,接著我們可以來進行「吃到食物」的邏輯判斷了
如果我們吃到了食物,我們就要呼叫newFood(),並且不要移除這次移動的蛇尾
那我們要怎麼判斷「吃到食物」呢,我們本來打算用

if(snake.getFirst() == new Point(foodX, foodY)){
    newFood();
}
else{
    snake.removeLast();
}

可是IntelliJ IDE告訴我,對於新增的Object,建議我使用

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

完成這些後,要來處理一點小問題
有時候食物會剛好生成在蛇身的位置,這通常是不會發生的
因此我們可以在newFood()裡面寫一個簡單的判斷,我這邊用的是do while

public void newFood(){
    boolean samePos = false;
    do {
        samePos = false;
        foodX = random.nextInt(xBlocks) * BLOCK_SIZE;
        foodY = random.nextInt(yBlocks) * BLOCK_SIZE;
        for(Point point : snake){
            if(Objects.equals(point, new Point(foodX, foodY))){
                samePos = true;
            }
        }
    }while(samePos);
}

另一個問題是短時間快速按兩個移動鍵,就會原地GameOver
問了Gemini之後確認了問題:我在Timer更新週期前快速按了兩個鍵,導致遊戲還沒來得及更新
而解決辦法則是可以再建立一個變數:directionChanged

boolean directionChanged = false;

// setKeyBinding(),當我們按下一個按鍵後,先把directionChanged設為true
// 並在判斷中增加檢查是否 not directionChanged
actionMap.put("upAction", new AbstractAction(){
    @Override
    public void actionPerformed(ActionEvent e){
        if(direction != 's' && isRunning && !directionChanged){
            direction = 'w';
            directionChanged = true;
        }
    }
});

// 而當我們完成move()動作時,才會把directionChanged改回false

至此我們就完成這個SnakeGame的動態邏輯了!目前看起來已經有模有樣且有一點可玩性了,明天我們要來設定遊戲的結束邏輯,並嘗試將整個遊戲打包成可執行檔。
今天也是快樂學習的一天,明天繼續!/images/emoticon/emoticon76.gif


上一篇
Day 19:Java Snake Game 建立遊戲 - 靜態畫面
系列文
30天從基礎學起Java,直到做出我的第一個遊戲20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言