-
Notifications
You must be signed in to change notification settings - Fork 49
功能簡單的蛇
在這個章節會先撰寫一隻功能簡單的蛇,並提供一個空間讓蛇可以在裡面移動。在教學開始前,先在任意找一個地方建立一個資料夾,來放接下來教學所建立的程式碼。
在資料夾裡面建立一個檔案 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,以下圖為例:
粗黑線的範圍是 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
接著把 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
的順序,由距離蛇頭最近的先加入。
蛇的移動以下圖為例:
蛇身上的數字代表在 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 像素。(後面讓蛇可以走不同方向時,這段程式碼會被取代)
當然蛇的功能還不完全,只會往下走,不過功能會在後面慢慢補齊。這裡先將剛製作好的蛇放到場景中,看看效果。場景功能為管理遊戲物件,像是設置物件位置、安排物件的更新順序。
在資料夾下新增一個 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
是為了能夠以 width
或 height
屬性來取得場景大小的資訊,提高程式碼易讀性。如果要使用 tuple 來定義也可以,如 area_size = (300, 300)
。兩者差別以取得場景寬度為例:area_rect.width
與 area_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()