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

2022/02/17閱讀時間約 13 分鐘
接下來我們會運用之前所學過的各種觀念,包括Turtle 模組繼承等等,來製作這個大家都熟知的小遊戲-貪食蛇。完成品會長得像下圖這樣,包括可以設定難度(速度)、使用鍵盤的上、下、左、右鍵來移動貪食蛇的移動方向來吃到食物並加長蛇身、以及記分板等。

遊戲架構

這個遊戲到目前為止算是一個稍微複雜的程式,將程式的邏輯拆解成一些較簡單的功能,再將這些功能組合起來,會比較利於遊戲設計的進行,以免不知道從何下手。這篇文章會以以下的流程來說明:
  1. 建立遊戲進行的畫布
  2. 建立遊戲難度設定視窗
  3. 畫出遊戲開始時的貪食蛇身體
  4. 畫出圍牆
  5. 隨機產生食物
  6. 移動蛇身
  7. 控制方向
  8. 檢查是否吃到食物
  9. 建立記分板
  10. 檢查是否撞到牆
  11. 檢查是否撞到蛇身
  12. 結束遊戲

建立遊戲進行的畫布

從Turtle模組載入Screen類別,建立一個600*600大小的畫布,並設定標題與背景顏色,這個畫布就是讓遊戲進行的區域。tracer()方法的目的是暫停畫布的更新,讓後續在畫布上所改變的內容可使用update()方法一次更新,以減少視覺上的延遲感。
# 主程式
from turtle import Screen
# 遊戲參數
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 600
SCREEN_COLOR = "black"
SCREEN_TITLE = "貪食蛇"
# 建立Screen物件
snake_screen = Screen()
# 遊戲畫面設定
snake_screen.setup(width=SCREEN_WIDTH, height=SCREEN_HEIGHT)
snake_screen.bgcolor(SCREEN_COLOR)
snake_screen.title(SCREEN_TITLE)
snake_screen.tracer(0)
# 畫面暫停
snake_screen.exitonclick()

建立遊戲難度設定視窗

在這裡我們要讓使用者輸入遊戲的難度,所以必須另開一個對話視窗和使用者互動,並把輸入的內容做為參數傳入程式。在Screen類別中,可以使用textinput()方法來達成這個目標,用法如下:
turtle.textinput(title, prompt)
  • title – 對話視窗的標題
  • prompt – 對話視窗內顯示的文字
在這裡我們以1到9來代表遊戲的難度,並把輸入的內容存入變數,換算成畫面更新的時間間隔,接著就可以建立主程式的while迴圈,根據難度所換算出來的固定時間間隔更新畫面。
# 主程式
import time
# 遊戲難度
level = snake_screen.textinput(title="貪食蛇-遊戲難度", prompt="請選擇遊戲難度:1(簡單) ~ 9(困難)")
time_delay = (10 - float(level)) * 0.05
# 遊戲主程式
is_game_on = True
while is_game_on:
  # 遊戲畫面更新
  snake_screen.update()
  time.sleep(time_delay)

畫出遊戲開始時的貪食蛇身體

為了簡潔起見,我們在主程式所在的目錄底下建立一個snake資料夾,把所有和這個專題相關的檔案都放在這個資料夾中,在主程式中導入。
蛇身的屬性和操作可以視為一個功能單位,因此首先在snake資料夾中新增一個snake.py檔案,在其中建立一個Snake類別,在初始化中定義三個Turtle物件做為蛇身,並適當設定三個物件的距離,讓蛇身可以連在一起。
# snake.py
from turtle import Turtle
START_POSITION = [(0, 0), (-20, 0), (-40, 0)]
class Snake:
  def __init__(self):
    self.snake_body = []
    self.create_snake()
    self.head = self.snake_body[0]
    self.move_angle_list = []
  def create_snake(self):
    for position in START_POSITION:
      self.add_snake_body(position)
  def add_snake_body(self, position):
    body_seg = Turtle(shape="square")
    body_seg.color("Green")
    body_seg.penup()
    body_seg.goto(position)
    self.snake_body.append(body_seg)
在Snake類別中特別把add_snake_body()獨立成一個方法,因為之後在延長蛇身時會重覆使用同一段程式碼。在主程式中載入Snake類別,並建立一個Snake物件。
# 主程式
from snake.snake import Snake
# 建立遊戲相關物件
snake = Snake()
執行後,可以在畫布上看到一段遊戲開始時的小蛇。

畫出圍牆

圍牆同樣也具有獨立的屬性與功能,它在遊戲一開始建立後就不會再更動,目的是要讓蛇頭碰觸到圍牆後即觸發遊戲結束,因此圍牆的範圍需要考慮畫布的大小。我們同樣在snake資料夾中新增一個wall.py檔案,在其中建立Wall類別,讓它繼承自Turtle模組,以方便使用Turtle模組的所有功能,不須額外新增Turtle物件。Wall類別在初始化時從主程式傳入畫布的大小,以畫筆在畫布的四周圈出一個長方形的區域,這個區域即是貪食蛇可以在其中自由移動的範圍,注意上方預留了較大的空間,是顯示記分板文字的位置。
# wall.py
from turtle import Turtle
WALL_COLOR = "BlueViolet"
WALL_PEN_SIZE = 10
class Wall(Turtle):
  def __init__(self, width, height):
    super().__init__()
    self.hideturtle()
    self.pensize(WALL_PEN_SIZE)
    self.color(WALL_COLOR)
    self.speed("fastest")
    self.screen_width = width
    self.screen_height = height
    self.draw()
  def draw(self):
    self.penup()
    self.goto(-(self.screen_width / 2 - 25), self.screen_height / 2 - 45)
    self.pendown()
    self.forward(self.screen_width - 50)
    self.setheading(270)
    self.forward(self.screen_height - 70)
    self.setheading(180)
    self.forward(self.screen_width - 50)
    self.setheading(90)
    self.forward(self.screen_width - 70)
主程式中建立Wall類別的物件,即可在畫布四周畫出圍牆。
# 主程式
from snake.wall import Wall
# 建立遊戲相關物件
wall = Wall(SCREEN_WIDTH, SCREEN_HEIGHT)

隨機產生食物

食物必須隨機在圍牆內產生,由前一小節圍牆的設定可知,畫筆在四個頂點的座標分別為:左上(-275, 255)、右上(275, 255)、左下(-275, -275)、右下(275, -275),畫筆的寬度為10,所以圍牆內緣的座標為:左上(-270, 250)、右上(270, 250)、左下(-270, -270)、右下(270, -270),每一個蛇身的物件距離為20,因此產生食物的範圍必須再內縮10(蛇身的一半),也就是x座標在[-260, 260]、y座標在[-260, 240]這個區間內產生食物。此外,為了之後配合貪食蛇每次行走的距離,也就是一個蛇身的長度20,讓蛇身和食物可以對齊,我們利用一個ALIGNMENT_FACTOR讓出現食物的座標皆為20的整數倍。
# food.py
import random
from turtle import Turtle
FOOD_SHAPE = "circle"
FOOD_COLOR = "gold"
ALIGNMENT_FACTOR = 20
class Food(Turtle):
  def __init__(self, width, height):
    super().__init__()
    self.shape(FOOD_SHAPE)
    self.penup()
    self.color(FOOD_COLOR)
    self.speed("fastest")
    self.screen_width = width
    self.screen_height = height
    self.random_food()
  def random_food(self):
    random_x = random.randint(-(self.screen_width / 2 - 40), (self.screen_width / 2 - 40))
    random_x = ALIGNMENT_FACTOR * round(random_x / ALIGNMENT_FACTOR)
    random_y = random.randint(-(self.screen_height / 2 - 40), (self.screen_width / 2 - 60))
    random_y = ALIGNMENT_FACTOR * round(random_y / ALIGNMENT_FACTOR)
    self.goto(random_x, random_y)
主程式中建立Food類別的物件,即可隨機出現第一個食物。
# 主程式
from snake.food import Food
# 建立遊戲相關物件
food = Food(SCREEN_WIDTH, SCREEN_HEIGHT)

移動蛇身

先不考慮方向,若要讓蛇身往前移動,以目前的三節身體來說,就是讓第三節的身體往第二節身體移動、第二節身體往蛇頭移動、蛇頭往前移動一個單位,更多節的身體也可以以此類推,因此在Snake類別中新增move()方法實現以上邏輯。
# snake.py
MOVE_DISTANCE = 20
class Snake:
  def move(self):
    # 往前移動
    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)
在主程式的while迴圈中呼叫Snake類別的move()方法,執行後可以看到三節的小蛇一路往前移動到超出畫布範圍。
# 主程式
# 遊戲主程式
is_game_on = True
while is_game_on:
  # 遊戲畫面更新
  snake_screen.update()
  time.sleep(time_delay)
  snake.move()
接著還有六個步驟,留待下篇文章說明!
為什麼會看到廣告
Wei-Jie Weng
Wei-Jie Weng
留言0
查看全部
發表第一個留言支持創作者!