在昨天我們完成靜態畫面後,目前只是一條不會動的綠色格子跟一個隨機生成的紅色方塊
所以今天我們要來處理動態的效果!
為了讓遊戲有「動起來」的效果,我們使用Timer,不過這個前面好像沒有學到,因此我現場問Gemini
了解如何使用之後,我在類別下宣告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的動態邏輯了!目前看起來已經有模有樣且有一點可玩性了,明天我們要來設定遊戲的結束邏輯,並嘗試將整個遊戲打包成可執行檔。
今天也是快樂學習的一天,明天繼續!