不間斷 Python 挑戰 Day 26 - 專題:貪食蛇(下)

2022/02/22閱讀時間約 14 分鐘
接續前一篇文章,說明貪食蛇遊戲架構的後半部份,來完成整個遊戲的程式設計。

遊戲架構(續)

控制方向

要透過鍵盤來控制貪食蛇移動的方向,需要使用到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()
執行結果:

檢查是否撞到牆

程式來到了最後的步驟,要決定遊戲何時該結束,可以歸納出兩個條件:
  1. 蛇頭撞到牆。
  2. 蛇頭撞到身體。
先處理撞到牆的部份,判斷撞到牆的條件是蛇頭超出了圍牆的範圍,前篇文章已知圍牆內緣的座標為:左上(-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"顯示畫面:

程式範例

為什麼會看到廣告
Wei-Jie Weng
Wei-Jie Weng
留言0
查看全部
發表第一個留言支持創作者!