Skip to content

控制蛇的行走

Kun-Yi Li edited this page Oct 8, 2019 · 2 revisions

本章節介紹如何透過鍵盤來控制蛇的行走。

定義移動指令

首先決定蛇的移動指令:上、下、左、右。在遊戲物件檔(games/snake/game/gameobject.py)中定義 SnakeAction

from mlgame.utils.enum import StringEnum

class SnakeAction(StringEnum):
    UP = "UP"
    DOWN = "DOWN"
    LEFT = "LEFT"
    RIGHT = "RIGHT"
    NONE = "NONE"

StringEnum 繼承自 enum.Enum,主要功能為可以直接以字串與其成員作比較,例如:SnakeAction.UP == "UP"

SnakeAction 繼承自 StringEnum,定義基本的上下左右指令,還有一個額外的 NONE 指令,當玩家沒有輸入指令(例如:沒按按鍵),就會是 NONE 指令。

而一開始,要讓蛇的移動方向是向下的:

class Snake:
    def __init__(self):
        ...
        self._action = SnakeAction.DOWN

_action 是用來當作如果沒有指令或是移動的指令無效時,用來替代的指令,例如一開始沒有按按鍵時,蛇就會一直往下。而每次移動時,都會更新 _action_action 存的就是前次有效的指令。

讓指令移動蛇

在前面蛇的設計中,蛇身就是依照蛇頭上一動的位置在移動,所以控制蛇的移動,就是決定蛇頭要往哪裡動。流程如下:

  1. 先讓蛇身移動到蛇頭上一動的位置
  2. 檢查指令是否有效,如果指令無效,那就使用前次有效的指令
  3. 蛇頭移動到正確的位置

輔助函式

先在遊戲物件檔中的 Snake 新增一個函式來計算蛇頭可能的位置:

    def _get_possible_head_pos(self, action):
        """
        Get the possible head position accroding to the given action
        """
        if action == SnakeAction.UP:
            move_delta = (0, -10)
        elif action == SnakeAction.DOWN:
            move_delta = (0, 10)
        elif action == SnakeAction.LEFT:
            move_delta = (-10, 0)
        elif action == SnakeAction.RIGHT:
            move_delta = (10, 0)

        return self.head.rect.move(move_delta).topleft

_get_possible_head_pos() 會以傳入的指令回傳蛇頭可能的位置。回傳值使用的 pygame.Rect.move() 並不會影響原本 rect 的位置,而是另外回傳移動後的 rect,move_ip() 才會直接更改原本 rect 的位置。

這裡的回傳值也可以使用 SnakeBody.pos 來計算,但是要注意的是 topleft 屬性回傳的是 tuple,把兩個 tuple 相加是「相接」,(1, 2) + (3, 4) 會得到 (1, 2, 3, 4)。所以必需要把 tuple 裡的元素個別相加後再回傳:

        return (self.head.pos[0] + move_delta[0], self.head.pos[1] + move_delta[1])

套用流程

接著讓 Snake.move() 套用前面提到的流程:

  1. 首先讓 move() 多一個引數 action,可以傳入移動指令:
    def move(self, action):
        """
        Move the snake accroding to the given action
        """
  1. 蛇身移動到蛇頭上一動的位置,藉此讓蛇移動:
        # Move the body 1 step ahead
        tail = self.body.pop()
        tail.pos = self.head.pos
        self.body.appendleft(tail)
  1. 檢查指令是否有效,下面兩種情況都是無效的指令:

如果玩家沒有指定指令,那就使用前次有效的指令:

        # If there is no action, take the same action as the last frame.
        if action == SnakeAction.NONE:
            action = self._action

如果蛇會直接走回自己的身體(例如:蛇往左走,就不能直接往右走),那該指令也無效,替代成前次有效的指令:

        # If the head will go back to the body, this action is invalid and
        # take the same action as the last frame.
        possible_head_pos = self._get_possible_head_pos(action)
        if self.body[1].pos == possible_head_pos:
            action = self._action

首先要先取得蛇頭的可能位置,如果蛇頭的位置與蛇身最前頭的位置相同,那就代表蛇會直接走回自己的身體。要注意的是,因為蛇身已經先移動了,此時蛇身最前頭是在蛇頭的位置,而原本的最前頭會變成第二個,所以指定的元素是 1,不是 0,如下圖。

Imgur

  1. 以有效的移動指令去更新蛇頭的位置,並更新 _action 供下次更新使用。
        # Get the next head position accroding to the valid action
        next_head_pos = self._get_possible_head_pos(action)
        self.head.pos = next_head_pos

        # Store the action
        self._action = action

即使傳入的 action 是無效的,也會在前面檢查的過程中被替換成 _action,所以如果一直都沒有無效的指令傳入,那 _action 就會一直是最近一次有效的指令。

更新程式碼

最後,在遊戲核心檔(games/snake/game/gamecore.py)中的 Scene.update() 中也多一個引數 action,藉此接收玩家的指令:

class Scene:
    ...
    
    def update(self, action):
        self._snake.move(action)

設置鍵盤指令

將鍵盤指令配對到控制蛇的指令上。定義在 mlgame.gamedev.generic 裡的 KeyCommandMap 是一個幫助把鍵盤按鍵配置到指定指令上的類別,只要按下該鍵,就會回傳設定的指令。

在手動遊戲主檔案(games/snake/game/snake.py)中加入 KeyCommandMap 來取得玩家的指令:

from mlgame.gamedev.generic import KeyCommandMap

from .gameobject import SnakeAction

class Snake:
    def __init__(self):
        ...
        
        self._keyboard_action = KeyCommandMap({
            pygame.K_UP:    SnakeAction.UP,
            pygame.K_DOWN:  SnakeAction.DOWN,
            pygame.K_LEFT:  SnakeAction.LEFT,
            pygame.K_RIGHT: SnakeAction.RIGHT,
        }, SnakeAction.NONE)

首先匯入 KeyCommandMap 與定義好的 SnakeAction

KeyCommandMap 需要兩個初始化參數,第一個是指定鍵盤對應的 dictionary,key 是按鍵(pygame 中定義的按鍵值),value 是對應的指令。這邊將上下左右鍵分別對應控制蛇的四個方向。第二個是預設指令,當沒有指定按鍵被按下時,會回傳的指令。這邊設為 SnakeAction.NONE

接著在 game_loop() 中加入 _keyboard_action

    def game_loop(self):
        while not quit_or_esc():
            command = self._keyboard_action.get_command()
            self._scene_update(command)
            
            ...

get_command() 會檢查當下按下的按鍵,並回傳指定的指令。get_command() 會依照初始化時列出的按鍵順序檢查,只要有符合的按鍵就會立刻回傳指令,所以同時按下多鍵也只會回傳一個指令。當沒有符合的按鍵按下則回傳預設指令。最後將得到的指令傳給 Scene.update()

執行遊戲

執行遊戲看看成果。

python MLGame.py snake -m

Imgur