接續
前一篇文章,說明貪食蛇遊戲架構的後半部份,來完成整個遊戲的程式設計。
遊戲架構(續)
控制方向
要透過鍵盤來控制貪食蛇移動的方向,需要使用到Turtle模組的
按鍵事件方法,也就是透過鍵盤按鍵的按下或釋放來觸發某一段程式執行。在這裡,我們使用鍵盤的上、下、左、右鍵做為觸發事件的按鍵,當這些按鍵按下後,觸發某一段程式,其語法如下:
turtle.listen()
turtle.onkeypress(fun, key)
- fun – 按鍵按下後執行的函數
- key – 觸發事件的按鍵
當向上的按鍵按下後,如果當下貪食蛇的方向為向左或向右,在下一步即轉為向上移動,也就是說,如果當下貪食蛇的方向為向上或向下,該事件無效,因為蛇身已經同向,或是不能直接反轉,其它方向的事件以此類推。
首先我們在Snake類別的初始化函數內新增一個移動方向的串列。
# snake.py
self.move_angle_list = []
當上、下、左、右鍵按下後,立即在move_angle_list中新增一個元素,代表下一個要轉向的角度。
# snake.py
def move_up(self):
self.move_angle_list.append(90)
def move_down(self):
self.move_angle_list.append(270)
def turn_right(self):
self.move_angle_list.append(0)
def turn_left(self):
self.move_angle_list.append(180)
在move()方法中,新增一段程式檢查move_angle_list是否為空串列,若有元素存在,即執行轉向的動作,在下一步改變移動的方向。
# snake.py
def move(self):
# 檢查是否要轉彎
while self.move_angle_list:
next_move_angle = self.move_angle_list.pop(0)
if next_move_angle != self.head.heading() and \
next_move_angle != (self.head.heading() + 180) % 360:
self.head.setheading(next_move_angle)
break;
else:
continue;
# 往前移動
for body_seg in range(len(self.snake_body) - 1, 0, -1):
new_x = self.snake_body[body_seg - 1].xcor()
new_y = self.snake_body[body_seg - 1].ycor()
self.snake_body[body_seg].goto(new_x, new_y)
self.head.forward(MOVE_DISTANCE)
在主程式中,則利用listen()及onkeypress()方法,接受上、下、左、右鍵按下的事件。
# 主程式
# 按鍵設定
snake_screen.listen()
snake_screen.onkeypress(snake.move_up, "Up")
snake_screen.onkeypress(snake.move_down, "Down")
snake_screen.onkeypress(snake.turn_left, "Left")
snake_screen.onkeypress(snake.turn_right, "Right")
如此一來便可用鍵盤來控制貪食蛇移動的方向,下一步就可以開始讓貪食蛇吃食物並延長蛇身。
檢查是否吃到食物
在遊戲中吃食物必定是用蛇頭去吃,我們可以在每一步去檢查蛇頭和食物的距離,當距離足夠小即表示有吃到食物,這是和蛇相關的行為,因此在Snake類別中新增一個方法來執行這個任務。
# snake.py
def is_collision_with_food(self, food):
if self.head.distance(food) < 5:
return True
return False
在吃食物的同時也會增加身體的長度,同樣在Snake類別中新增一個方法,複製貪食蛇末端的元素到snake_body串列的最後方,當蛇身下一次移動時就會完成延長蛇身的動作。
# snake.py
def extend_snake(self):
self.add_snake_body(self.snake_body[-1].position())
最後,在主程式的while迴圈中,每一步移動之後即檢查是否吃到食物,若有,則呼叫food.random_food()隨機產生下一個食物,並執行snake.extend_snake()延長蛇身。
# 主程式
# 是否吃到食物
if snake.is_collision_with_food(food):
food.random_food()
snake.extend_snake()
執行結果:
建立記分板
上一篇文章畫出圍牆的時候,我們有在畫布的上方預留比較大的空間,做為顯示記分板文字的空間,在貪食蛇吃到食物的同時更新分數,例如一次加一分。做為一個相對獨立的功能,我們在snake資料夾內另開一個scoreboard.py檔案,建立Scoreboard類別,繼承自Turtle模組。在初始化函數中移動至適當的位置,設定初始分數為0,並用write()方法寫到畫布上。
# scoreboard.py
from turtle import Turtle
SCORE_COLOR = "white"
SCORE_POSITION = (0, 265)
class Scoreboard(Turtle):
def __init__(self):
super().__init__()
self.score = 0
self.hideturtle()
self.penup()
self.color(SCORE_COLOR)
self.speed("fastest")
self.goto(SCORE_POSITION)
self.write(f"score: {self.score}", False, align="center", font=("Arial", 20, "normal"))
def get_score(self):
self.score += 1
self.clear()
self.write(f"score: {self.score}", False, align="center", font=("Arial", 20, "normal"))
主程式中建立Scoreboard類別的物件,並在while迴圈中當偵測到蛇頭吃到食物時呼叫scoreboard.get_score()方法更新分數。
# 主程式
# 建立遊戲相關物件
scoreboard = Scoreboard()
# 遊戲主程式
is_game_on = True
while is_game_on:
# 遊戲畫面更新
snake_screen.update()
time.sleep(time_delay)
snake.move()
# 是否吃到食物
if snake.is_collision_with_food(food):
food.random_food()
snake.extend_snake()
scoreboard.get_score()
執行結果:
檢查是否撞到牆
程式來到了最後的步驟,要決定遊戲何時該結束,可以歸納出兩個條件:
- 蛇頭撞到牆。
- 蛇頭撞到身體。
先處理撞到牆的部份,判斷撞到牆的條件是蛇頭超出了圍牆的範圍,
前篇文章已知圍牆內緣的座標為:左上(-270, 250)、右上(270, 250)、左下(-270, -270)、右下(270, -270),我們便以此為條件在Snake類別新增is_collision_with_wall()方法來判斷。
# snake.py
def is_collision_with_wall(self, width, height):
if self.head.xcor() > (width / 2 - 30) or \
self.head.xcor() < -(width / 2 - 30) or \
self.head.ycor() > (height / 2 - 50) or \
self.head.ycor() < -(height / 2 - 30):
return True
return False
主程式的while迴圈中,每一次更新位置後呼叫snake.is_collision_with_wall()判斷是否撞牆。
# 主程式
# 是否撞牆
if snake.is_collision_with_wall(width=SCREEN_WIDTH, height=SCREEN_HEIGHT):
is_game_on = False
執行結果:
檢查是否撞到蛇身
要判斷蛇頭是否撞到蛇身,我們將蛇頭和蛇身每一節的距離一一比對,當距離小於20,也就是每前進一次的距離,就表示撞到自己了,當然設定更小也是沒有問題的。
# snake.py
def is_collision_with_body(self):
for body_seg in self.snake_body[1:]:
if self.head.distance(body_seg) < 5:
return True
return False
在主程式的while迴圈中,同樣在每一次更新位置後呼叫snake.is_collision_with_body(),來判斷是否相撞。
# 主程式
# 是否撞身體
if snake.is_collision_with_body():
is_game_on = False
執行結果:
結束遊戲
為了讓遊戲結束時有明確的標示,讓遊戲看起來更完整,我們讓前面兩個條件成立時進一步在畫布上顯示出"Game Over"字樣以提示遊戲結束,因為性質和記分板類似,我們也在Scoreboard類別中新增此功能。
# scoreboard.py
def game_over(self):
self.color(GAMEOVER_COLOR)
self.goto(GAMEOVER_POSITION)
self.write("Game Over", False, align="center", font=("Arial", 40, "normal"))
最後,在判斷是否相撞的條件內呼叫scoreboard.game_over()方法,整個遊戲便大功告成。
# 主程式
# 遊戲主程式
is_game_on = True
while is_game_on:
# 遊戲畫面更新
snake_screen.update()
time.sleep(time_delay)
snake.move()
# 是否吃到食物
if snake.is_collision_with_food(food):
food.random_food()
snake.extend_snake()
scoreboard.get_score()
# 是否撞牆
if snake.is_collision_with_wall(width=SCREEN_WIDTH, height=SCREEN_HEIGHT):
is_game_on = False
scoreboard.game_over()
# 是否撞身體
if snake.is_collision_with_body():
is_game_on = False
scoreboard.game_over()
"Game Over"顯示畫面:
程式範例