【python實戰】使用 pygame 寫一個 flappy-bird 類小遊戲 | 涉及思路+項目結構+代碼詳解 | 新手向

基於 pygame 的 Amazing-brick 實現

本文涉及三個 .py 文件:

amazing_brick / amazing_brick_utils.py
              / wrapped_amazing_brick.py
keyboard_play.py


項目地址:https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL

微信公衆號:Piper蛋窩

Bilibili:枇杷鷺

設計思路


從玩家角度看,該遊戲是動態的;但實際上,由於我沒有使用已有物理引擎/遊戲引擎,我是基於每一幀對遊戲進行設計、並迭代畫面的。

keyboard_play.py 在操作時,遊戲類實體:game_state.frame_step(action) 處於一個無限循環中:

  • 每執行一次 game_state.frame_step(action)game_state 會判斷位移、是否碰撞、是否得分,並繪製這一幀,並顯示;
  • 默認收到的動作 action=1 ,即什麼也不幹;
  • 玩家按下按鈕,將改變 action 的賦值。

1. 整體思路

如圖,在遊戲中需要繪製在屏幕上的,一共有三種實體:

  • 玩家(黑色方塊);
  • 方塊障礙物;
  • 中間留有空隙的長條障礙物。

基於這三個實體,我們主要需要考慮以下五個事件:

  • 簡易的物理引擎,考慮重力、阻力與加速度;
  • 當玩家上升時,屏幕要隨之上升;
  • 檢測得分,當玩家穿過間隙時,得分加一;
  • 檢測碰撞,當玩家碰到障礙物或撞牆時,遊戲結束;
  • 新建隨機障礙物。

下面我將展開分別講解上述事件的實現。

2. 簡易的物理引擎

簡易物理引擎是最簡單的部分,我們爲玩家(黑色方塊)聲明幾個變量,作爲定位的依據,我這裏選擇的是左上點 (x, y)

此外,玩家還應該具有速度變量。在 2D 空間裏,速度是一個矢量(有大小,有方向),爲了方便計算,我用橫軸座標方向的速度值表示 (velX, velY) ,即:單位時間內的 X 、 Y 軸位移量來表示速度。

此外,還有加速度系統。爲玩家聲明四個變量,分佈表示重力加速度、橫向空氣阻力帶來的加速度、按下按鈕後帶來的橫向加速度、按下按鈕後帶來的縱向加速度: gravity, dragForce, AccX, AccY

因此,我們就能很輕鬆地實現符合物理公式的運動系統:

  • 首先根據加速度計算速度;
  • 接下來根據速度計算玩家應該處於什麼位置。

game/amazing_brick_utils.py

class Play:
    def __init__(self):
        self.x = ...
        self.y = ...
        self.x_= ...
        self.y_= ...
        # 如果你覺得遊戲太難的話,可以改變這些物理參數
        self.gravity = 0.35
        self.dragForce = 0.01
        self.velX = 0
        self.velY = 0
        self.AccX = 4.5
        self.AccY = 2.5
    
    def lFlap(self):
        # 按下左邊按鈕時,玩家獲得一個向左上的力
        # 因此速度發生改變
        self.velX -= self.AccX
        self.velY -= (self.AccY - self.gravity)
    
    def rFlap(self):
        # 按下右邊按鈕時,玩家獲得一個向右上的力
        # 因此速度發生改變
        self.velX += self.AccX
        self.velY -= (self.AccY - self.gravity)
    
    def noneDo(self):
        # 沒有按按鈕
        # 玩家因爲橫向空氣阻力而減緩橫向速度
        # 此外,還因爲重力向下加速
        if self.velX > 0:
            self.velX -= self.dragForce
        elif self.velX < 0:
            self.velX += self.dragForce
        self.velY += self.gravity

game/wrapped_amazing_brick.py 中,我在每幀的迭代代碼中,添加了下述代碼,用來根據當前速度,確定玩家的新位置:

class GameState:
    def __init__(self, ifRender=True, fps=30):
        ...
    def frame_step(self, action):
        ...
        if action == 0:
            self.player.noneDo()
        elif action == 1:
            self.player.lFlap()
        elif action == 2:
            self.player.rFlap()
        ...
        # player's movement
        self.player.x += self.player.velX
        self.player.x_ += self.player.velX
        self.player.y += self.player.velY
        self.player.y_ += self.player.velY

3. 屏幕上升機制

有兩個思路:

  • 第一個是,讓所有障礙物在每幀下移固定距離,從而造成“玩家在上升”的假象;
  • 另一個是,建立一個“攝像頭”,攝像頭本身有一個座標,攝像頭隨着玩家的上升而上升。無論是障礙物還是玩家,都有兩套座標,一套是真實的、絕對的座標,另一套是相對於“攝像頭”的座標。我們計算碰撞時,基於前者即真實的座標;繪圖時,基於後者即相對於“攝像頭”的座標。

我採用了第二個思路。這樣做的好處是,無需每時每刻對所有障礙物的座標進行更新,且讓鏡頭的移動更加靈活。

我在 game/wrapped_amazing_brick.py 中將這個“攝像頭”實現了:

class ScreenCamera:
    def __init__(self):
        self.x = 0
        self.y = 0
        self.width = CONST['SCREEN_WIDTH']
        self.height = CONST['SCREEN_HEIGHT']
        self.x_ = self.x + self.width
        self.y_ = self.y + self.height
    
    def __call__(self, obj: Box):
        # output the obj's (x, y) on screen
        x_c = obj.x - self.x
        y_c = obj.y - self.y
        # 每個實體:玩家、障礙物都有一套相對座標,即 x_c, y_c
        # obj.set_camera(x_c, y_c) 將其在屏幕上的新位置告訴它
        # 繪圖時,就根據其 x_c, y_c 來將其繪製在屏幕上
        obj.set_camera(x_c, y_c)
        return obj
    
    def move(self, obj: Player):
        # 如果玩家此時在屏幕上的座標將高於屏幕的 1/2
        # 鏡頭上移
        # 即不允許玩家跑到屏幕上半部分去
        self(obj)
        if obj.y_c < self.height / 2:
            self.y -= (self.height / 2 - obj.y_c)
        else:
            pass

值得注意的是,pygame中的座標系是右下爲正反向的。

如圖,因爲相機的移動,我們的玩家一直處於屏幕中央。

4. 檢測得分

game/wrapped_amazing_brick.py 中,我在每幀的迭代代碼中,添加了下述代碼,用來檢測得分:

class GameState:
    def __init__(self, ifRender=True, fps=30):
        ...
    def frame_step(self, action):
        ...
        # check for score
        playerMidPos = self.s_c(self.player).y_c + self.player.height / 2
        for ind, pipe in enumerate(self.pipes):
            if ind % 2 == 1:
                continue
            self.s_c(pipe)
            # 判斷 Y 軸是否處於間隙中央
            if pipe.y_c <= playerMidPos <= pipe.y_c + pipe.height:
                if not pipe.scored:
                    self.score += 1
                    # 不能在一個間隙中得兩次分
                    pipe.scored = True
                    # reward 用於強化學習
                    reward = 1

只要在Y軸方向經過了間隙中央,則得分。

5. 檢測碰撞

以下情況視爲碰撞發生,遊戲結束:

  • 碰到障礙物;
  • 碰到邊緣鏡頭。

其中,“碰到障礙物”用實際座標計算:

  • 對於兩個物體,取其中心點;
  • 當滿足如下圖片兩個條件時,視爲碰撞。

碰到邊緣鏡頭則用相對座標判斷。

6. 新建障礙物

因爲每次碰撞都要遍歷所有障礙物,因此當障礙物淡出屏幕後,就要將障礙物從內存中刪除,以確保程序不會越來越卡頓。

我使用兩個列表保存所有已有障礙物:

class GameState:
    def __init__(self, ifRender=True, fps=30):
        ...
        self.pipes = []
        self.blocks = []
    
    def frame_step(self, action):
        ...
        # 判斷是否新增障礙物
        low_pipe = self.pipes[0]
        if self.s_c(low_pipe).y_c >= self.s_c.height - low_pipe.width \
                and len(self.pipes) < 6:
            # 滿足條件,新增障礙物
            self._getRandomPipe()
        # 如果條形障礙物超出屏幕,則刪除
        if self.s_c(low_pipe).y_c >= self.s_c.height \
                and len(self.pipes) > 4:
            self.pipes.pop(0)
            self.pipes.pop(0)
        
        # 如果塊狀障礙物超出屏幕,則刪除
        for block in self.blocks:
            self.s_c(block)
            x_flag = - CONST['BLOCK_WIDTH'] <= block.x_c <= self.s_c.width
            y_flag = block.y_c >= self.s_c.height

此外,還需新增障礙物。這裏我使用隨機數生成。

class GameState:
    ...
    def _getRandomPipe(self, init=False):
        if self.score % 5 == 4:
            self.color_ind = (self.color_ind + 1) % 5

        gap_left_topXs = list(range(100, 190, 20))
        if init:
            index = random.randint(0, len(gap_left_topXs)-1)
            x = gap_left_topXs[index]
            y = CONST['SCREEN_HEIGHT'] / 2 - CONST['PIPE_WIDTH'] / 2
            first_pipes = pipes(x, y, self.color_ind)
            self.pipes.append(first_pipes[0])
            self.pipes.append(first_pipes[1])
            self._addBlocks()
        index = random.randint(0, len(gap_left_topXs)-1)
        x = self.s_c.x + gap_left_topXs[index]
        y = self.pipes[-1].y - CONST['SCREEN_HEIGHT'] / 2
        pipe = pipes(x, y, self.color_ind)
        self.pipes.append(pipe[0])
        self.pipes.append(pipe[1])
        self._addBlocks()
    
    def _addBlocks(self):
        x = (self.pipes[-2].x_ + self.pipes[-1].x) / 2
        y = (self.pipes[-2].y + self.pipes[-2].y_) / 2
        for i in range(2, 0, -1):
            y_block = y + i * CONST['BLOCK_SPACE']
            x_block = x + np.random.normal() * CONST['PIPE_GAPSIZE'] / 2.5
            block = Block(x_block, y_block, self.color_ind)
            self.blocks.append(block)

程序結構

amazing_brick

整個遊戲的核心,包括負責加載圖片與存儲實體類的 amazing_brick_utils.py 與運算迭代用的 wrapped_amamzing_brick.py

amazing_brick_utils.py

依次實現以下功能:

  • 設置尺寸常量;
  • 加載圖片;
  • 聲明實體類。

wrapped_amamzing_brick.py

包含:

  • 相機類;
  • 計算迭代繪圖類(核心)。

keyboard_play.py

用於與玩家交互。

import os.path as osp
import sys
dirname = osp.dirname(__file__)
sys.path.append(dirname)

import pygame
from amazing_brick.game.wrapped_amazing_brick import \
        GameState, SCREEN

game_state = GameState(True)
ACTIONS = (0, 1, 2)

while True:
    action = ACTIONS[0]
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFTBRACKET:
                action = ACTIONS[1]
            if event.key == pygame.K_RIGHTBRACKET:
                action = ACTIONS[2]

    game_state.frame_step(action)
pygame.quit()

在遊戲中,玩家控制一個小方塊,按 “[” 鍵給其一個左上的力,按 “]” 鍵給其一個右上的力,什麼都不按,小方塊會由於重力原因下落。

你可以運行 keyboard_play.py 文件,嘗試手動控制該遊戲。如上圖,推薦使用命令行的方式啓動該文件:

python keyboard_play.py

源碼:https://github.com/PiperLiu/Amazing-Brick-DFS-and-DRL

接下來的文章中,我將講解:

  • DFS 算法是怎麼回事,我是怎麼應用於該小遊戲的:DFS自動控制
  • BFS 算法是怎麼回事,我是怎麼應用於該小遊戲的:BFS自動控制
  • 強化學習爲什麼有用?其基本原理:強化學習算法緒論
  • 爲了解決此問題,我構建的算法一:基於CNNs的算法構建
  • 爲了解決此問題,我構建的算法二:2幀輸入的線性NN模型
  • 爲了解決此問題,我構建的算法三:輸入速度的線性NN模型

歡迎 star 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章