pygame 入門範例教學 1

2023-09-06 21:00:27

作者自我介紹:大爽歌, b站小UP主直播程式設計+紅警三python1對1輔導老師

本教學步驟明確,過程清晰簡明,最終程式碼量250行上下適合學習pygame的新手
專案程式碼已上傳到我的github: https://github.com/BigShuang/simple-brick-games-by-pygame
遊戲已錄製成視訊,投稿至本人b站:點選前往b站觀看遊戲視訊

遊戲執行效果,截圖如下

〇、初始準備工作

  • 本專案使用的python3版本(如果你用python2,我不知會怎麼樣)
  • Ide推薦大家選擇pycharm(不同ide應該沒影響)
  • 需要安裝第三方庫pygame,安裝方法(windows電腦,mac系統本人實測與pygame不相容,強行執行本專案卡成ppt)

電腦開啟cmd命令視窗,輸入pip install pygame
如果電腦上安的是pip3則是pip3 install pygame

補充說明:
由於眾所周知的原因,安裝過程中下載可能十分緩慢,甚至由此導致安裝失敗
此時建議大家嘗試使用映象下載

---國內源---
清華:https://pypi.tuna.tsinghua.edu.cn/simple
阿里雲:http://mirrors.aliyun.com/pypi/simple/
中國科技大學: https://pypi.mirrors.ustc.edu.cn/simple/
華中理工大學:http://pypi.hustunique.com/
山東理工大學:http://pypi.sdutlinux.org/
豆瓣:http://pypi.douban.com/simple/

使用辦法 pip install xxxx -i jinxiangurl
具體到pygame,則是:

pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple

一、實現基礎視窗

0 - 新建car_racing.py檔案,內容如下

import pygame

WIN_WIDTH = 600  # 視窗寬度
WIN_HEIGHT = 900  # 視窗高度

pygame.init() # pygame初始化,必須有,且必須在開頭
# 建立主表單
win=pygame.display.set_mode((WIN_WIDTH,WIN_HEIGHT))

此時執行car_racing.py,會發現一個一閃而逝的視窗,

1 - 進一步,我們自然而然的就要思考這些問題

  • 遊戲視窗大小設定為多少合適?
    由於是方塊遊戲,所以可以根據方格尺寸、行格字數、列格子數去計算出視窗的寬高
  • 怎麼維持住這個視窗?
    通過while迴圈去實現
  • 但是簡單的迴圈只是單純的將介面卡住,怎麼實現重新整理?
    在迴圈體內使用pygame.display.update()語句進行介面的更新
  • 迴圈的重新整理頻率不做節制的話,介面會飛速重新整理導致卡死,怎麼辦?
    pygame有專門的物件pygame.time.Clock用於去控制迴圈重新整理的頻率,建立pygame.time.Clock物件後,呼叫該物件的tick()方法,函數引數為每秒重新整理次數,就可以設定迴圈每秒重新整理頻率,術語叫做影格率

可前往官方檔案觀看pygame.time.Clock的更多細節,https://www.pygame.org/docs/ref/time.html

  • 根據上面的思路,修改car_racing.py後如下
import pygame

C, R = 11, 20  # 11列, 20行
CELL_SIZE = 40  # 格子尺寸

FPS=60  # 遊戲影格率
WIN_WIDTH = CELL_SIZE * C  # 視窗寬度
WIN_HEIGHT = CELL_SIZE * R  # 視窗高度

pygame.init() # pygame初始化,必須有,且必須在開頭
# 建立主表單
clock = pygame.time.Clock() # 用於控制迴圈重新整理頻率的物件
win = pygame.display.set_mode((WIN_WIDTH,WIN_HEIGHT))

while True:
    clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
    pygame.display.update()

此時執行car_racing.py, 就可以得到一個最最最基礎的視窗了,

2 - 但是,這個視窗有個大問題, 此時視窗的關閉按鈕很容易出bug(卡死)

所以需要自己去重新實現這個視窗關閉功能,需要在迴圈體內新增如下程式碼

# 獲取所有事件
for event in pygame.event.get():
    if event.type == pygame.QUIT:
        # 判斷當前事件是否為點選右上角退出鍵
        pygame.quit()
        sys.exit() # 需要提前 import sys

同時我們一般會希望能夠設定下背景的顏色
比如,這個遊戲的背景色是輕灰色(200, 200, 200)
那麼設定背景的程式碼為

bg_color = (200, 200, 200)
win.fill(bg_color)

不過需要注意的是,這段程式碼放在不同的位置會產生不同的效果。
放在while迴圈之前,代表只繪製一次背景,會被後面繪製的東西遮住。
放在while迴圈中,則是每一幀都會繪製一次背景,一般用於去覆蓋掉那些希望刪掉的元素。

3 - 給視窗設定標題
win = pygame.display.set_mode((WINWIDTH,WINHEIGHT))後面,
新增程式碼如下,設定視窗標題(Big Shuang是我的英文名,可以刪掉或者修改為你的名字)

pygame.display.set_caption('Car Racing by Big Shuang')

本階段最後car_racing.py如下

import pygame
import sys

FPS=60 # 遊戲影格率
WINWIDTH = 600  # 視窗寬度
WINHEIGHT = 900  # 視窗高度

pygame.init() # pygame初始化,必須有,且必須在開頭
# 建立主表單
clock = pygame.time.Clock() # 用於控制迴圈重新整理頻率的物件
win = pygame.display.set_mode((WINWIDTH,WINHEIGHT))
pygame.display.set_caption('Car Racing by Big Shuang')

bg_color = (200, 200, 200)
win.fill(bg_color)

while True:
    # 獲取所有事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # 判斷當前事件是否為點選右上角退出鍵
            pygame.quit()
            sys.exit()

    clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
    pygame.display.update()

到這裡,基礎視窗就完成了~

二、實現基礎方塊繪製

1 - 最基礎的繪製方法

pygame裡面,繪製一個小方格實際上是很簡單的。
新建一個方格色塊(放在while迴圈之前)

area = pygame.Surface((CELL_SIZE, CELL_SIZE))
enemy_color = (50, 50, 50)
area.fill(enemy_color)

再將方格色塊放在視窗物件上(放在while迴圈中,clock.tick(FPS)之前)

win.blit(area, (CELL_SIZE, 0))

此時執行,效果如圖

不過這個繪製方法的問題在於, 後面的移動操作管理起來頗為不便。
要在while迴圈中編寫各種程式碼來實現area這個色塊的位置變換的話,程式碼寫起來麻煩,管理起來也亂。

pygame 給我們提供了一個Sprite類,用於實現可以移動的二維影象物件。

我們將繼承這個類,並封裝一些需要的方法,方便移動以及管理。

2 - 使用pygame的Sprite類

In computer graphics, a sprite is a two-dimensional bitmap that is integrated into a larger scene, most often in a 2D video game.
在計算機圖學中,精靈是一種二維點陣圖,它被整合到一個更大的場景中,通常在二維電動遊戲中。
個人理解,sprite是一個計算機術語,代表介面中可以移動的二維點陣圖。

繼承Sprite, 新建Block類如下(在新建win物件,應該是14行,後面新增如下程式碼)

class Block(pygame.sprite.Sprite):
    def __init__(self, c, r, color):
        super().__init__()

        self.cr = [c, r]
        self.x = c * CELL_SIZE
        self.y = r * CELL_SIZE

        self.image  = pygame.Surface((CELL_SIZE, CELL_SIZE))
        self.image.fill(color)

        self.rect = self.image.get_rect()
        self.rect.move_ip(self.x, self.y)

刪掉前面 1 最基礎的繪製方法中, area的相關程式碼

在while迴圈之前, 新增如下程式碼

enemy_color = (50, 50, 50)
block = Block(1, 1, enemy_color)
win.blit(block.image, block.rect)

此時執行效果和1中相同。

三、 基礎方塊移動

1 - 給Block新增移動方法

首先,給Block 類新增移動到指定行列的類方法如下

    def move_cr(self, c, r):
        self.cr[0] = c
        self.cr[1] = r
        self.x = c * CELL_SIZE
        self.y = r * CELL_SIZE
        self.rect.left = self.x
        self.rect.top = self.y

但是這個方法還是不夠的的,
因為遊戲中的移動,一般都是操作上下左右來移動。

那麼,我們需要把上下左右,轉換成c、r的變換。
所以建立字典如下

DIRECTIONS = {
    "UP": (0, -1),  # (dc, dr)
    "DOWN": (0, 1),
    "LEFT": (-1, 0),
    "RIGHT": (1, 0),
}

然後再給Block 類新增按方向移動的方法如下

    def move(self, direction):
        dc, dr = DIRECTIONS[direction]
        next_c, next_r = self.cr[0] + dc, self.cr[1] + dr
        self.move_cr(next_c, next_r)

2 - 響應鍵盤按鍵移動

1 中只是新增了移動的方法,但是玩家要通過鍵盤來移動的話,
還需要程式中有能夠響應處理鍵盤操作
所以在while迴圈中的for event in pygame.event.get()迴圈裡,新增程式碼如下

if event.type == pygame.KEYDOWN:
    if event.key == pygame.K_LEFT or event.key == ord('a'):
        block.move("LEFT")
    if event.key == pygame.K_RIGHT or event.key == ord('d'):
        block.move("RIGHT")
    if event.key == pygame.K_UP or event.key == ord('w'):
        block.move("UP")
    if event.key == pygame.K_DOWN or event.key == ord('s'):
        block.move("DOWN")

同時移動後,需要再進行繪製才能看到在上面的for迴圈後面(外面)新增

win.blit(block.image, block.rect)

但是這個時候會有一個問題,移動後原來的位置色塊還在,沒有消失掉(被清掉)。
多移動幾下後,結果就像下圖一樣

3 - 清除之前的繪製

所以我們需要擦除之前繪製的色塊。
pygame裡面,一般採用重新繪製整個介面的方式擦除之前的繪製。
重新繪製介面後,再在介面上新增新的需要繪製的東西。

即在while迴圈中,win.blit(block.image, block.rect)之前新增程式碼:

win.fill(bg_color)

此時, while 迴圈之前的這兩句程式碼刪不刪除沒啥區別

win.fill(bg_color)
win.blit(block.image, block.rect)

(個人視為多餘的,所以刪除)

此時移動小方塊就不會有之前的色塊殘留了

不過此時還有一個小小的問題,就是小方塊可以移動到介面邊界外
雖然可以再移動回來,但是這不符合我們這個程式的規則。
所以需要再進行邊界處理,使其無法移到邊界外。

4 - 邊界處理

Block 類新增檢查能否移動的方法如下

    def check_move(self, direction=""):
        move_c, move_r = DIRECTIONS[direction]
        next_c, next_r = self.cr[0] + move_c, self.cr[1] + move_r

        if 0 <= next_c < C and 0 <= next_r < R:
            return True

        return False

再在while迴圈中的for迴圈中的每次呼叫move方法前,使用check_move檢查是否 能移動。
修改後的while迴圈如下

while True:
    # 獲取所有事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # 判斷當前事件是否為點選右上角退出鍵
            pygame.quit()  # 關閉視窗
            sys.exit()  # 停止程式

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                if block.check_move("LEFT"):
                    block.move("LEFT")
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                if block.check_move("RIGHT"):
                    block.move("RIGHT")
            if event.key == pygame.K_UP or event.key == ord('w'):
                if block.check_move("UP"):
                    block.move("UP")
            if event.key == pygame.K_DOWN or event.key == ord('s'):
                if block.check_move("DOWN"):
                    block.move("DOWN")

    win.fill(bg_color)
    win.blit(block.image, block.rect)

    clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
    pygame.display.update()

當然這麼寫不夠優雅,if block.check_move("RIGHT"):重複了四次,
有的朋友可能會覺得把check_move的呼叫放在move方法裡面開頭更好。

這裡之所以不這麼做,是因為後面會在方塊組成的賽車類裡,進行這個check_move的工作。
具體見下文。

四、多個方塊組成方塊賽車

1 - 使用方塊組繪製賽車

無論是玩家的車,還是敵人的車,
都是由多個方塊組成的。
所以首先要定義下,玩家車和敵人車的方格組成,程式碼如下

CARS = {  # 車的形狀,即格子位置
    "player": [
        [0, 1, 0],
        [1, 1, 1],
        [1, 0, 1],
    ],
    "enemy": [
        [1, 0, 1],
        [1, 1, 1],
        [0, 1, 0],
    ]
}

根據這個方格祖,繪製賽車方法如下:
(放在while迴圈中,重置背景之後)

car_c, car_r = 2, 2
for ri, row in enumerate(CARS["enemy"]):
    for ci, cell in enumerate(row):
        if cell == 1:
            i_block = Block(car_c + ci, car_r + ri, enemy_color)
            win.blit(i_block.image, i_block.rect)

2 - 使用Group管理方塊

接下來要建立一個賽車類,用來管理這些方塊。
pygame為我們提供了一個管理多個Sprite的工具pygame.sprite.Group
這個Group類可以使用draw方法,將內部的Sprite一起繪製在螢幕視窗上。
這裡賽車類就繼承這個Group類,如下。

class Car(pygame.sprite.Group):
    def __init__(self, c, r, car_kind, car_color):
        super().__init__()

        for ri, row in enumerate(CARS[car_kind]):
            for ci, cell in enumerate(row):
                if cell == 1:
                    block = Block(c+ci, r+ri, car_color)
                    self.add(block)

同時,給這個賽車類新增移動方法,用於統一移動內部的方塊

    def move(self, direction=""):
        if all(block.check_move(direction) for block in self.sprites()):
            for block in self.sprites():
                block.move(direction)

然後刪掉其他的建立方塊與繪製方塊的程式碼
此時while迴圈上下程式碼如下


bg_color = (200, 200, 200)
enemy_color = (50, 50, 50)
player_color = (65, 105, 225)  # RoyalBlue

bottom_center_c = (C - len(CARS["player"][0])) // 2
bottom_center_r = R - len(CARS["player"])
car = Car(bottom_center_c, bottom_center_r, "player", player_color)

while True:
    # 獲取所有事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # 判斷當前事件是否為點選右上角退出鍵
            pygame.quit()  # 關閉視窗
            sys.exit()  # 停止程式

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                car.move("LEFT")
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                car.move("RIGHT")
            if event.key == pygame.K_UP or event.key == ord('w'):
                car.move("UP")
            if event.key == pygame.K_DOWN or event.key == ord('s'):
                car.move("DOWN")

    win.fill(bg_color)

    car.draw(win)

    clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
    pygame.display.update()

此時效果如圖,小車可以左右移動

五、重新整理敵人賽車

敵人的賽車繪製,同上面第四部分。
不過敵人的賽車

  1. 需要隔一段時間就能自動生成一個
  2. 不能由玩家的鍵盤操作移動,而是自動地向下移動。
  3. 向下移動出邊界後需要清理掉

1 - 為Car新增新的方法

這裡有幾個問題需要先解決,

  • 敵人的賽車是可以從上邊界移動出來, 可以向下移動出下邊界的。
    所以需要修改下原有的賽車內
    Car新增無邊界限制的移動方法free_move如下
    def free_move(self, direction=""):
        for block in self.sprites():
            block.move(direction)

再修改原有的move方法如下

    def move(self, direction=""):
        if all(block.check_move(direction) for block in self.sprites()):
            self.free_move(direction)
  • 敵人賽車完全移動出邊界後要清理掉
    所以要給賽車類新增一個方法判斷是否完全出了邊界
    先給組成賽車的方格Block新增方法is_out如下
    def is_out(self):
        if 0 <= self.cr[0] < C and 0 <= self.cr[1] < R:
            return False
        return True

再為Car新增方法is_out如下

    def is_out(self):
        return all(block.is_out() for block in self.sprites())

2 - 新增敵人賽車管理類

我們需要一個管理敵人賽車的類EnemyManager
其程式碼如下

import random  # 在程式碼檔案開頭部分新增

# 在 enemy_color 宣告之後的位置新增
class EnemyManager():
    def __init__(self):
        self.enemies = []

        self.move_count = 0

    def gen_new_enemies(self):  # 生成敵人賽車
        # 設定敵人賽車的生成間隔, 隔兩倍的敵人賽車行數+1
        if self.move_count % (2 * len(CARS["enemy"]) + 1) == 1:

            ec = random.randint(1, C - len(CARS["enemy"][0]))
            enemy = Car(ec, 0, "enemy", enemy_color)

            self.enemies.append(enemy)

    def move(self):  # 自動向下移動敵人賽車
        # 超出邊界後,自動清理掉
        to_delete = []
        for i, enemy in enumerate(self.enemies):
            enemy.free_move("DOWN")
            if enemy.is_out():
                to_delete.append(i)

        for di in to_delete[::-1]:  # 倒著按序號來刪除
            self.enemies.pop(di)

        self.move_count += 1

        self.gen_new_enemies()

    def draw(self, master):
        # 繪製敵人賽車
        for enemy in self.enemies:
            enemy.draw(master)

3 - 繪製敵人賽車

在while迴圈之前範例化EnemyManager,程式碼如下

emg = EnemyManager()

while中對敵車進行繪製,即在win.fill(bg_color)之後新增程式碼如下

emg.move()
emg.draw(win)

但是此時敵人賽車運動的太快了,是每幀一次移動。
所以需要設定一下,讓敵人的賽車每過MOVE_SPACE幀才進行一次移動
在開頭新增程式碼如下

MOVE_SPACE = 5

再在while迴圈之前新增幀計數器

frame_count = 0

while迴圈中的開頭新增

frame_count += 1

再修改剛才的敵人賽車繪製程式碼如下

if frame_count % MOVE_SPACE == 0:
    emg.move()
    
emg.draw(win)

此時執行效果如圖

六、碰撞檢測

1 - 新增檢測方法

要實現碰撞檢測,只需給敵人賽車管理類EnemyManager新增方法
遍歷其中現有的敵人賽車,檢查是否與玩家賽車相撞即可。
賽車相撞的判斷方法為兩車的方塊有重疊,即有相同位置的方塊。
所以給類Car新增check_collide方法,程式碼如下

    def check_collide(self, other_car):
        for block in self.sprites():
            bcr1 = tuple(block.cr)
            for other_block in other_car.sprites():
                bcr2 = tuple(other_block.cr)
                if bcr1 == bcr2:
                    return True
        
        return False

再給敵人賽車管理類EnemyManager新增check_collide方法,程式碼如下

    def check_collide(self, player):
        for enemy in self.enemies:
            if enemy.check_collide(player):
                return True

        return False

2 - 碰撞後結束遊戲

while迴圈中,繪製完賽車後新增碰撞檢測
即在clock.tick(FPS)之前新增程式碼如下

    if emg.check_collide(car):
        break

此時玩家賽車碰到敵人賽車,遊戲就會結束。

不過,此時遊戲結束後會直接關閉視窗。
玩家體驗並不好,所以需要進一步的優化下。
最好是遊戲結束後,視窗不關閉,展示Game Over的提示,並告訴玩家得分。

七、綜合優化

1 遊戲流程優化

遊戲流程上,執行程式碼後,敵人賽車就直接迎面而來,留給玩家的緩衝準備時間太短了。
所以這裡修改為程式碼執行後,遊戲處於等待狀態,玩家按鍵盤任意鍵開始遊戲。
同時遊戲結束後,玩家可按鍵盤任意鍵重新開始。

首先需要在while迴圈前,新增一個遊戲執行狀態變數,預設遊戲未開始

running = False

再修改while迴圈如下

while True:
    frame_count += 1

    # 獲取所有事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # 判斷當前事件是否為點選右上角退出鍵
            pygame.quit()  # 關閉視窗
            sys.exit()  # 停止程式

        if event.type == pygame.KEYDOWN:
            if running:  # 遊戲開始時,響應上下左右按鍵
                if event.key == pygame.K_LEFT or event.key == ord('a'):
                    car.move("LEFT")
                if event.key == pygame.K_RIGHT or event.key == ord('d'):
                    car.move("RIGHT")
                if event.key == pygame.K_UP or event.key == ord('w'):
                    car.move("UP")
                if event.key == pygame.K_DOWN or event.key == ord('s'):
                    car.move("DOWN")
            else: # 遊戲結束後,響應任意按鍵,開始遊戲
                # reset game, 重置變數和遊戲狀態
                car = Car(bottom_center_c, bottom_center_r, "player", player_color)
                frame_count = 0
                emg = EnemyManager()
                running =True

    if running:
        win.fill(bg_color)

        if frame_count % MOVE_SPACE == 0:
            emg.move()

        emg.draw(win)

        car.draw(win)

        if emg.check_collide(car):
            running = False  # 撞車後,遊戲狀態改變為結束

    clock.tick(FPS) # 控制迴圈重新整理頻率,每秒重新整理FPS對應的值的次數
    pygame.display.update()

此時遊戲流程就修改好了,但是沒有文字提示,玩家會不明就裡,感到迷惑,所以需要文字在開始時和結束時進行提示。

2 文字提示與得分展示

在遊戲開頭新增大中小三種字型(必須要在pygame.init()之後,能後不能前)

# 大中小三種字型,48,36,24
FONTS = [
    pygame.font.Font(pygame.font.get_default_font(), font_size) for font_size in [48, 36, 24]
]

程式碼執行後,遊戲尚未開始,需要文字提示玩家按任意鍵開始遊戲
即在while迴圈之前新增程式碼如下

score_color = (0,128,0)
start_info = FONTS[2].render("Press any key to start game", True, score_color)
text_rect = start_info.get_rect(center=(WIN_WIDTH / 2, WIN_HEIGHT / 2))
win.blit(start_info, text_rect)

遊戲過程中,展示玩家得分(玩家堅持時間)
while迴圈中,car.draw(win)之後新增程式碼如下

text_info = FONTS[2].render("Scores: %d" % (frame_count / FPS), True, score_color)
win.blit(text_info, dest=(0, 0))

玩家撞車後,一輪遊戲結束,需要展示Game Over,玩家得分,與按任意鍵結束遊戲
修改while迴圈中,原有的程式碼

        if emg.check_collide(car):
            running = False  # 撞車後,遊戲狀態改變為結束

        if emg.check_collide(car):
            running = False  # 撞車後,遊戲狀態改變為結束

            over_color = (255,0,0)
            texts = ["Game Over", "Scores: %d" % (frame_count / FPS), "Press Any Key to Restart game"]
            for ti, text in enumerate(texts):
                over_info = FONTS[ti].render(text, True, over_color)
                text_rect = over_info.get_rect(center=(WIN_WIDTH / 2, WIN_HEIGHT / 2 + 48 * ti))
                win.blit(over_info, text_rect)

3 程式碼優化

我們注意到,關於顏色這個常數,都是直接寫在用的地方。
後面多了後,容易找起來不方便,所以建議把所有的顏色宣告都統一移到開頭位置。
這裡放在CELL_SIZE = 40之後,移動後顏色設定程式碼如下

bg_color = (200, 200, 200)
enemy_color = (50, 50, 50)
player_color = (65, 105, 225)  # RoyalBlue
score_color = (0,128,0)  # SpringGreen
over_color = (255, 0, 0)

4 後續

  • 其實,由於顏色值比較多,我更喜歡把顏色統一放在一個字典裡面,根據鍵來取值,本文github專案下的car_racing.py就是這樣寫的
  • 遊戲中賽車移動,是按一下移動一個,可能有的人跟喜歡能夠連續移動,這個已在專案下的1 car racing資料夾下的car_racing_plus1.py中實現。