本文供有一定編程經驗,已經完成基本 python 語言學習的讀者使用。源碼工程全文地址:
遊戲程序,和 hello world 有什麼區別?
一般來說學習編程都會先寫 hello world,然而遊戲的 hello world 應該是怎樣的呢?這就需要先搞清楚遊戲和普通的 hello world 程序有什麼不同。
- 這是一個需要一直運行,直到用戶手動關閉,才退出的程序;而不是像一個 hello world 程序,運行完直接就退出了。
- 這是一個隨時間變化,程序自動會做不同事情的程序,有點像播放一段影片;而不像 hello world 程序一樣,運行的功能和時間無關。
因為有上面兩個區別,所以遊戲程序的基本結構,和其他的程序就會有明顯的不同。遊戲程序的基本結構,會包含以下部分:
- 一個無限循環,我們稱之為“主循環”。通過用戶操作退出了這個循環,遊戲程序就關閉了。
- 一個每秒被調用固定次數的函數,我們稱之為“update”函數。這個函數是大部分遊戲程序的入口;而每秒調用此函數的次數,在遊戲中稱為 fps。
一個遊戲運行起來,基本上就是進入主循環之後,通過每秒調用固定次數的 update 函數,去展示遊戲的內容,處理用戶的操作。
除了程序的運行時的結構,還需要有的兩個遊戲運行的必要能力:
- 顯示一個可供畫圖的窗口
- 檢測用戶的輸入,如鍵盤按鍵、鼠標點擊等
pygame 提供了這樣的能力,因此我們可以編寫一個遊戲的主循環如下(可以保存為 main.py 文件中運行):
pygame.display.set_mod() 會返回一個 Screen 類的對象,這個對象就是遊戲的屏幕,所有需要顯示的圖形, 都會用到這個對象。在上面的例子中沒有用到這個對象。從上面這個代碼,你可以發現,一個遊戲程序,是可以同時擁有多個畫面窗口的!雖然一般來說都只是一個。
上面的程序中, while running: 這個主循環中,如果 running 變成 False 了,就退出循環,遊戲就結束了。
pygame.time.Clock() 提供了一個定時器對象,通過調用 tick(60) 這個函數,輸入參數 60 表示等待 60 分之一秒,這個遊戲的 fps 就是 60。
pygame.event.get() 返回了當前瞬間的用戶所有的操作,包括點擊了關閉窗口,就是 pygame.QUIT 事件;還包括了當前鍵盤按鍵是否被按下,還是被釋放;鼠標點擊了哪個位置等等。
pygame.disaplay.flip() 刷新屏幕,必須要有這個調用,新的圖形才會被顯示到畫面上。
完成了上面的代碼,你就有了一個遊戲最基本架子:一個遊戲畫面窗口,並且可以被關閉。
遊戲就是電腦演出的一場戲
如果只是要顯示一個圖片到屏幕上,pygame 提供了一個函數,很簡單就能辦到:
screen.blit(image, (x,y))
其中 screen 變量,就是通過 pygame.display.set_mod() 返回的對象,代表了上面的遊戲畫面窗口。image 是圖片對象,(x,y) 表示圖片要顯示的位置,用兩個座標數表示。
但是,一般的遊戲都不會僅僅是顯示個圖片,而是需要把很多個不同的圖像,按照一定的規則來顯示。最常見的管理方法,就是把遊戲圖像分為多個“層”:
- 每一“層”都含有多個顯示的圖像
- 不同的“層”按照順序,在屏幕上先後顯示,形成固定的遮擋關係
譬如遊戲一般會有一個背景圖像,然後會有很多遊戲角色,遊戲角色之上,又會有一些 UI 界面的圖形。pygame 為我們已經準備了處理這些問題的工具:
- Sprite 類代表了一個遊戲角色,背景圖也可以是一個 Sprite。每個 Sprite 內部有屬性定義了顯示圖像內容(.image)和顯示的位置與大小(.rect)
- Group 類代表了一組遊戲角色,可以通過 Group.add(sprite) 用於存放多個 Sprite 對象,如果不想顯示某個對象,用 Group.remove(sprite) 從 Group 中刪除這個對象即可。Group.draw(screen) 方法把本組 Sprite 對象都顯示到屏幕上。
遊戲除了需要處理很多圖像,還需要隨著遊戲進度,切換不同的場景。譬如遊戲開始的標題場景,進入每一局不同的遊戲等等。這些就需要我們寫一些代碼來進行管理。一般我們會寫一個叫 Scenario 的類來代表一個場景,也就是“一幕劇”的意思。在 Unity 引擎中,叫 Level(一個關卡)。
因此一個遊戲,往往由多個 Scenario 組成,而每個 Scenario 又會包含很多個 Group。為了讓遊戲可以在多個“關卡”(或者叫劇幕)中切換,還需要一個核心調度和管理的類,這裡我叫做 Director(導演),通過對 Director 進行控制,可以讓遊戲切換不同的關卡。Director 對象身上,存放了 screen, events 這些遊戲唯一的,用於顯示、操作檢測的屬性,每個 Scenario 對象都可以通過 Director 對象調用這些屬性,從而實現任意的遊戲功能。
根據上述設計,我開發兩個簡單的框架類,方便後面的遊戲內容的填充:
文件名為 scenario.py
有了上面的框架類,就可以把主入口程序 mian.py 簡化為:
'''遊戲的啟動入口''' import pygameimport scenarioimport mahjong # 具體的遊戲邏輯模塊,下一章提供 # 初始化遊戲入口director = scenario.Director("麻將推推樂", 60, [255,255,255], [630,500])director.change_scenario(mahjong.MainScenario()) # 開始第一個關卡 # 進入主循環director.run()
這裡的 Director 類構造器,定義了遊戲窗口的標題、背景色、大小、幀率。而 change_scenario() 方法,則需要傳入一個 Scenario 類的子類,通過這個子類,定義具體的遊戲內容。而上面所說的主循環,關卡管理,遊戲對象分層顯示的代碼,都可以通過 scenario.py 重複使用。在 Unity 和 Unreal 引擎中,上述功能往往也是不需要開發者自己實現的。
Scenario 類最主要的編程接口,就是 start() 方法,在切換關卡的時候,新的 Scenario 對象的 start() 方法就會被調用,用來往遊戲屏幕上準備各種具體的遊戲對象 Group。一旦通過 Scenario.add_group() 放上屏幕,這個 Group 裡面的所有 Sprite 對象,每幀都會收到對於 update() 的調用,用以驅動遊戲邏輯運行。
下一篇講解繼承 Scenario 寫一個遊戲關卡。