如何用 Python 實現超級瑪麗的介面和狀態機?

bibi119發表於2019-12-25

01狀態機介紹

遊戲中的狀態機一般都是有限狀態機,簡寫為FSM(有限狀態機),簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。
狀態機的每一個狀態至少需要有以下三個操作:
啟動:當從其他狀態進入這個狀態時,需要進行的初始化操作;
更新:在這個狀態執行時進行的更新操作;
清理:當從這個狀態退出時,需要進行的清除操作。

狀態需要的變數: 下一個:表示這個狀態退出後要轉到的下一個狀態;
堅持:在狀態間轉換時需要傳遞的資料;
完成:表示這個狀態是否結束,狀態機會根據這個值來決定轉換狀態。

遊戲介面狀態機的狀態轉換圖如下,箭頭表示可能的狀態轉換方向:(注意有個轉換不太好畫出來:超時狀態可以轉換到Game Over狀態。)
image.png

這幾個狀態的意思比較簡單,下面把遊戲介面的截圖發一下。
主選單:主選單,啟動程式就進入這個狀態,可以用上和下鍵選擇玩家1或玩家2,按Enter鍵開啟遊戲。
image.png

載入螢幕:遊戲開始前的載入介面。image.png

遊戲執行:遊戲執行時的狀態,在程式碼實現中是Level類。image.png

遊戲結束:人物死亡且生命數量為0時到這個狀態。
image.png

超時:在遊戲中時間超時會到這個狀態,這個和Game Over類似,就不截圖了。

02狀態機程式碼實現

因為這篇文章的目的是遊戲介面的狀態機實現,所以專門寫了一個state_demo.py檔案,讓大家可以更加方便的看程式碼。

遊戲啟動程式碼
開始是pygame的初始化,設定螢幕大小為c.SCREEN_SIZE(800,600)。所有的常量都儲存在單獨的constants.py中。

import os
import pygame as pg
import constants as c

pg.init()
pg.event.set_allowed([pg.KEYDOWN, pg.KEYUP, pg.QUIT])
pg.display.set_caption(c.ORIGINAL_CAPTION)
SCREEN = pg.display.set_mode(c.SCREEN_SIZE)
SCREEN_RECT = SCREEN.get_rect()

load_all_gfx函式查詢指定目錄下所有符合字尾名的圖片,使用pg.image.load函式載入,儲存在圖形set中。

GFX儲存在資源/圖形目錄找到的所有圖片,後面獲取各種圖形時會用到。

def load_all_gfx(directory, colorkey=(255,0,255), accept=('.png', '.jpg', '.bmp', '.gif')):
    graphics = {}
    for pic in os.listdir(directory):
        name, ext = os.path.splitext(pic)
        if ext.lower() in accept:
            img = pg.image.load(os.path.join(directory, pic))
            if img.get_alpha():
                img = img.convert_alpha()
            else:
                img = img.convert()
                img.set_colorkey(colorkey)
            graphics[name] = img
    return graphics

GFX = load_all_gfx(os.path.join("resources","graphics"))

下面是demo的入口函式,先建立了一個儲存所有狀態的state_dict設定,呼叫setup_states函式設定啟動狀態是MAIN_MENU。

if __name__=='__main__':
    game = Control()
    state_dict = {c.MAIN_MENU: Menu(),
                  c.LOAD_SCREEN: LoadScreen(),
                  c.LEVEL: Level(),
                  c.GAME_OVER: GameOver(),
                  c.TIME_OUT: TimeOut()}
    game.setup_states(state_dict, c.MAIN_MENU)
    game.main()

狀態類
先定義一個狀態基類,按照上面說的狀態需要的三個操作分別定義函式(啟動,更新,清理)。在init函式中定義了上面說的三個變數(next,persist,```js done),還有start_time和current_time用於記錄時間。 class State(): definit(self): self.start_time = 0.0 self.current_time = 0.0 self.done = False self.next = None self.persist = {}

@abstractmethod def startup(self, current_time, persist): '''abstract method'''

def cleanup(self): self.done = False return self.persist

@abstractmethod def update(sefl, surface, keys, current_time): '''abstract method'''

看一個狀態類LoadScreen的具體實現,這個狀態的顯示效果如圖3。

startup函式儲存了預期的persist,設定next為Level狀態類,start_time儲存進入該狀態的開始時間。初始化一個Infoclass,這個就是專門用來顯示介面資訊的。
update函式根據在這個狀態已執行的時間(current_time-self.start_time),決定顯示內容和是否結束狀態(self.done = True)。

class LoadScreen(State):
   def __init__(self):
       State.__init__(self)
       self.time_list = [2400, 2600, 2635]

   def startup(self, current_time, persist):
       self.start_time = current_time
       self.persist = persist
       self.game_info = self.persist
       self.next = self.set_next_state()

       info_state = self.set_info_state()
       self.overhead_info = Info(self.game_info, info_state)

   def set_next_state(self):
       return c.LEVEL

   def set_info_state(self):
       return c.LOAD_SCREEN

   def update(self, surface, keys, current_time):
       if (current_time - self.start_time) < self.time_list[0]:
           surface.fill(c.BLACK)
           self.overhead_info.update(self.game_info)
           self.overhead_info.draw(surface)
       elif (current_time - self.start_time) < self.time_list[1]:
           surface.fill(c.BLACK)
       elif (current_time - self.start_time) < self.time_list[2]:
           surface.fill((106, 150, 252))
       else:
           self.done = True

資訊類
下面介紹的資訊類,介面的顯示大部分都是由它來完成,初始化函式中create_info_labels函式建立通用的資訊,create_state_labels函式對於不同的狀態,會初始化不同的資訊。

class Info():
   def __init__(self, game_info, state):
       self.coin_total = game_info[c.COIN_TOTAL]
       self.total_lives = game_info[c.LIVES]
       self.state = state
       self.game_info = game_info

       self.create_font_image_dict()
       self.create_info_labels()
       self.create_state_labels()
       self.flashing_coin = FlashCoin(280, 53)

create_font_image_dict函式從之前載入的圖片GFX ['text_images']中,擷取字母和數字對應的圖形,儲存在一個設定中,在後面建立文字時會用到。

def create_font_image_dict(self):
    self.image_dict = {}
    image_list = []

    image_rect_list = [# 0 - 9
                       (3, 230, 7, 7), (12, 230, 7, 7), (19, 230, 7, 7),
                       (27, 230, 7, 7), (35, 230, 7, 7), (43, 230, 7, 7),
                       (51, 230, 7, 7), (59, 230, 7, 7), (67, 230, 7, 7),
                       (75, 230, 7, 7), 
                       # A - Z
                       (83, 230, 7, 7), (91, 230, 7, 7), (99, 230, 7, 7),
                       (107, 230, 7, 7), (115, 230, 7, 7), (123, 230, 7, 7),
                       (3, 238, 7, 7), (11, 238, 7, 7), (20, 238, 7, 7),
                       (27, 238, 7, 7), (35, 238, 7, 7), (44, 238, 7, 7),
                       (51, 238, 7, 7), (59, 238, 7, 7), (67, 238, 7, 7),
                       (75, 238, 7, 7), (83, 238, 7, 7), (91, 238, 7, 7),
                       (99, 238, 7, 7), (108, 238, 7, 7), (115, 238, 7, 7),
                       (123, 238, 7, 7), (3, 246, 7, 7), (11, 246, 7, 7),
                       (20, 246, 7, 7), (27, 246, 7, 7), (48, 246, 7, 7),
                       # -*
                       (68, 249, 6, 2), (75, 247, 6, 6)]

    character_string = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ -*'

    for character, image_rect in zip(character_string, image_rect_list):
        self.image_dict[character] = get_image(GFX['text_images'], 
                                        *image_rect, (92, 148, 252), 2.9)

get_image函式從一個大的表面工作表中按照面積(x,y,寬度,高度)擷取的部分圖片放入表面影象對應的起始位置(0,0),並按比例引數調整大小。

pygame的blit函式介紹如下:

pg.Surface.blit(source, dest, area=None, special_flags=0) -> Rect
    draw one image onto another
def get_image(sheet, x, y, width, height, colorkey, scale):
       image = pg.Surface([width, height])
       rect = image.get_rect()

       image.blit(sheet, (0, 0), (x, y, width, height))
       image.set_colorkey(colorkey)
       image = pg.transform.scale(image,
                                  (int(rect.width*scale),
                                   int(rect.height*scale)))
       return image

看一下create_info_labels函式中其中一個字串'MARIO'是如何在介面上顯示的。

create_label函式引數(x,y)表示字串在介面上的起始位置,從self.image_dict中根據字元獲取對應的表面物件。 set_label_rects函式會設定字串中每一個表面物件rect的(x,y)值。

pygame.Rect 物件中常用的成員變數(x,y),表示這個Surface的左上角的位置。
top, bottom: 表示Surface 在y軸上最上邊和最下邊的值, 所以top和y 值是一樣的
left,  right: 表示Surface 在x軸上最左邊和最右邊的值,所以left 和x 值是一樣的

下面的座標圖可以看到,在左上角是整個螢幕的原點(0,0),圖中標識了附件矩形的四個頂點的座標。
image.png

def create_info_labels(self):
        ...
        self.mario_label = []
        ...
        self.create_label(self.mario_label, 'MARIO', 75, 30)

    def create_label(self, label_list, string, x, y):
        for letter in string:
            label_list.append(Character(self.image_dict[letter]))
        self.set_label_rects(label_list, x, y)

    def set_label_rects(self, label_list, x, y):
        for i, letter in enumerate(label_list):
            letter.rect.x = x + ((letter.rect.width + 3) * i)
            letter.rect.y = y
            if letter.image == self.image_dict['-']:
                letter.rect.y += 7
                letter.rect.x += 2

控制類Control是狀態機類,main函式是遊戲的主迴圈,setup_states函式設定遊戲啟動時執行的狀態。

class Control():
   def __init__(self):
       self.screen = pg.display.get_surface()
       self.done = False
       self.clock = pg.time.Clock()
       self.fps = 60
       self.current_time = 0.0
       self.keys = pg.key.get_pressed()
       self.state_dict = {}
       self.state_name = None
       self.state = None

   def setup_states(self, state_dict, start_state):
       self.state_dict = state_dict
       self.state_name = start_state
       self.state = self.state_dict[self.state_name]

   def main(self):
       while not self.done:
           self.event_loop()
           self.update()
           pg.display.update()
           self.clock.tick(self.fps)

event_loop函式負責監聽輸入(鍵盤輸入和退出按鈕),slef.keys儲存鍵盤輸入。

如果檢測到當前狀態結束,就呼叫flip_state函式進行舊狀態的清理操作,並轉換到下一個狀態。更新函式會檢測狀態的完成值,呼叫狀態的更新函式。

def update(self):
     self.current_time = pg.time.get_ticks()
     if self.state.done:
         self.flip_state()
     self.state.update(self.screen, self.keys, self.current_time)

 def flip_state(self):
     previous, self.state_name = self.state_name, self.state.next
     persist = self.state.cleanup()
     self.state = self.state_dict[self.state_name]
     self.state.startup(self.current_time, persist)

 def event_loop(self):
     for event in pg.event.get():
         if event.type == pg.QUIT:
             self.done = True
         elif event.type == pg.KEYDOWN:
             self.keys = pg.key.get_pressed()
         elif event.type == pg.KEYUP:
             self.keys = pg.key.get_pressed()

03完整程式碼
有兩個檔案constants.py和state_demo.py,constants.py儲存了所有的字串定義和常量。 constants.py GAME_TIME_OUT表示遊戲的超時時間,這邊為了demo演示,設成5秒,實際是300秒。

閱讀原文獲取完整程式碼:  https://developer.aliyun.com/ask/267501?ut...

相關文章