-
Notifications
You must be signed in to change notification settings - Fork 49
控制蛇的行走
本章節介紹如何透過鍵盤來控制蛇的行走。
首先決定蛇的移動指令:上、下、左、右。在遊戲物件檔(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
存的就是前次有效的指令。
在前面蛇的設計中,蛇身就是依照蛇頭上一動的位置在移動,所以控制蛇的移動,就是決定蛇頭要往哪裡動。流程如下:
- 先讓蛇身移動到蛇頭上一動的位置
- 檢查指令是否有效,如果指令無效,那就使用前次有效的指令
- 蛇頭移動到正確的位置
先在遊戲物件檔中的 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()
套用前面提到的流程:
- 首先讓
move()
多一個引數action
,可以傳入移動指令:
def move(self, action):
"""
Move the snake accroding to the given action
"""
- 蛇身移動到蛇頭上一動的位置,藉此讓蛇移動:
# Move the body 1 step ahead
tail = self.body.pop()
tail.pos = self.head.pos
self.body.appendleft(tail)
- 檢查指令是否有效,下面兩種情況都是無效的指令:
如果玩家沒有指定指令,那就使用前次有效的指令:
# 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,如下圖。
- 以有效的移動指令去更新蛇頭的位置,並更新
_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