Skip to content

功能簡單的蛇

LanKuDot edited this page Jun 15, 2020 · 5 revisions

在這個章節會先撰寫一隻功能簡單的蛇,並提供一個空間讓蛇可以在裡面移動。在教學開始前,先在任意找一個地方建立一個資料夾,來放接下來教學所建立的程式碼。

撰寫蛇身 - SnakeBody

在資料夾裡面建立一個檔案 gameobject.py,這個檔案將會放置遊戲中會用到的物件。

設計蛇身為 10 x 10 像素大小的矩形,而蛇頭與蛇身有不同的顏色。在 gameobject.py 中建立一個蛇身的類別 SnakeBody

外觀

先匯入製作遊戲物件需要的類別:

from pygame import Rect, Surface, draw
from pygame.sprite import Sprite

SnakeBody 是遊戲中要被繪製到畫面上的物件,所以繼承自 Sprite。而 __init__() 的參數 init_pos 用來指定蛇身的起始位置,其值為一個 (x, y) 的 tuple;color 指定蛇身的顏色,其值為一個 (r, g, b) tuple。注意要在 __init__() 中呼叫父類別的 __init__()。繼承自 Sprite 的類別,則必須要有兩個屬性:

  • rect:為 Rect 物件。定義物件的位置還有大小。
  • image:為 Surface 物件。定義物件的外觀。
class SnakeBody(Sprite):
    def __init__(self, init_pos, color):
        super().__init__()

        self.rect = Rect(init_pos[0], init_pos[1], 10, 10)

        width = self.rect.width
        height = self.rect.height

        self.image = Surface((width, height))
        self.image.fill(color)
        draw.line(self.image, (0, 0, 0), (width - 1, 0), (width - 1, height - 1))
        draw.line(self.image, (0, 0, 0), (0, height - 1), (width - 1, height - 1))

為了讓蛇身之間有明顯的間隔,利用 pygame.draw.line() 在蛇身的 surface 的右邊與下邊各畫上一條黑線。要注意的是指定畫線的座標是 9 不是 10,以下圖為例:

Imgur

粗黑線的範圍是 surface 的範圍,可以看到 surface 的像素座標範圍是 0 ~ 9,藍色區域是黑線實際畫出的位置。如果指定畫線座標為 10,則會畫到紅色區域,因而超出 surface 的範圍,即使有畫線也不會顯示出來。

取得與設置位置

再來加入可以直接取得 SnakeBody 物件位置的函式 pos()pos() 用途是幫助簡化取得位置的程式碼及增加可讀性,snake_body.pos 比起 snake_body.rect.topleft 容易理解:

    @property
    def pos(self):
        return self.rect.topleft

pos() 會回傳 rect 的 topleft 座標,也就是該 SnakeBody 物件的位置(一個 rect 物件的 xy 座標是位在該物件的左上角)。pos() 上加的 @property 可以讓函式以屬性的方式使用(類似 getter),像是 snake_body.pos

另外也加入 pos 的 setter,可以直接指定 SnakeBody 的位置。要注意 pos 的 setter 要宣告在 pos 的 getter 之後。

    @pos.setter
    def pos(self, value):
        self.rect.topleft = value

基本的蛇 - Snake

接著把 SnakeBody 組合成 Snake,由一個蛇頭與三個蛇身組成。蛇頭是綠色的蛇身,一開始在 (40, 40) 的位置,而蛇身是白色的,從蛇頭往上方長,所以蛇一開始方向是朝下的:

from collections import deque

class Snake:
    def __init__(self):
        self.head = SnakeBody((40, 40), (31, 204, 42))  # Green

        self.body = deque()
        self.body_color = (255, 255, 255)   # White
        # Note the ordering of appending elements
        self.body.append(SnakeBody((40, 30), self.body_color))
        self.body.append(SnakeBody((40, 20), self.body_color))
        self.body.append(SnakeBody((40, 10), self.body_color))

蛇身用 deque 來儲存,是為了方便管理蛇身的順序,定義在 collections 套件中。要注意將蛇身加入到 deque 的順序,由距離蛇頭最近的先加入。

移動

蛇的移動以下圖為例:

Imgur

蛇身上的數字代表在 deque 中的順序,左圖是移動前,右圖是往下一步的樣貌。可以看到把最後一個元素放到 deque 的前端就可以達成蛇身順序的更新。deque 是 double-ended queue,也就是說元素可以從 queue 的兩端加入或移除,deque 也特別優化這項操作(使用 list 也可以,只是對於從 list 頭插入或移除元素會比 deque 慢)。至於被移動到前面的那個蛇身位置會等於前一動的蛇頭位置,而蛇頭就繼續往下一步。在 Snake 定義 move() 來移動蛇身:

    def move(self):
        tail = self.body.pop()
        tail.pos = self.head.pos
        self.body.appendleft(tail)

        self.head.rect.move_ip(0, 10)

pygame.Rect.move_ip() 會直接移動 rect 一定的距離,這邊先直接讓蛇頭往下 10 像素。(後面讓蛇可以走不同方向時,這段程式碼會被取代)

建立場景 - Scene

當然蛇的功能還不完全,只會往下走,不過功能會在後面慢慢補齊。這裡先將剛製作好的蛇放到場景中,看看效果。場景功能為管理遊戲物件,像是設置物件位置、安排物件的更新順序。

在資料夾下新增一個 gamecore.py 檔案,用來定義場景 Scene

除了 pygame 相關的類別之外,也要從 gameobject 匯入要使用的遊戲物件:

from pygame import Rect
from pygame.sprite import Group

from .gameobject import Snake

類別 Scene 擁有一個類別變數 area_rect,利用 Rect 來定義場景的大小為 300 x 300:

class Scene:
    area_rect = Rect(0, 0, 300, 300)

使用 Rect 是為了能夠以 widthheight 屬性來取得場景大小的資訊,提高程式碼易讀性。如果要使用 tuple 來定義也可以,如 area_size = (300, 300)。兩者差別以取得場景寬度為例:area_rect.widtharea_size[0],前者比較容易辨識。

放入遊戲物件到場景中

在類別 Scene__init__() 中會呼叫 _create_scene() 來配置場景物件,而目前的場景物件只有蛇:

    def __init__(self):
        self._create_scene()

    def _create_scene(self):
        self._snake = Snake()

        self._draw_group = Group()
        self._draw_group.add(self._snake.head, *self._snake.body)

    def draw_gameobjects(self, surface):
        self._draw_group.draw(surface)

在建立蛇後,把每個蛇身加到 _draw_group 中(為 pygame.sprite.Group物件),以便在 draw_gameobjects() 中一次把場景中所有遊戲物件畫到傳入的畫布上。

當變數或函式的命名以 _ 為開頭,則代表這個變數或函式是 private 的,也就是說希望這類變數或函式只在該類別中使用,如:_create_scene()_snake。在 python 中可以透過 __(雙底線)來更接近 private,如:__create_scene()。(詳見 python 文件

更新場景

update() 是用來更新場景的函式,每一次更新都要讓蛇移動一次:

    def update(self):
        self._snake.move()