05 April 2011

從我入行資訊界開始,我就幾乎在寫 web app,web 的架構已經深植到我的腦海 -- 一個伺服器,等待用戶開啟一個連線,送出一個 request 來,回答它一個 response 去。一個 use case、一項功能,就在一個 request/response 的循環中完成,而絕大部份物件的生命週期,就是這麼的短。當然 web app 還是會有複雜、長壽的物件 (例如 http session),不過那並非主幹。直到兩年前,我開始撰寫遊戲後,才接觸到遊戲的架構。在 desktop app,我們有行之以久的 MVC pattern。在 web app 中,request/response 主導整個思維。但在遊戲界,似乎是一團的混亂,不同類別的遊戲,有著不同的操作、不同的世界觀,需要不同的架構來滿足 (下棋和 RPG,兩者肯定天差地遠的)。我現在任職的公司,目前專注開發 flash 遊戲。flash 遊戲的開發成本、時程並不像常見於電視廣告的 online game 那樣的龐大,所以比較有機會嘗試不同的類別。而且隨著市場變化,我們也必須不斷的調整方向。結果,這兩三年來,我們公司的開發遊戲類型涵蓋很廣 -- 連線對奕,Facebook 社群遊戲,連線射擊遊戲,小型的 MMO ARPG...等等。

對個人而言,不同類型遊戲的開發,不斷改寫我腦袋裡原有的 web 開發思考方式,改變我對物件導向的認識。尤其我們的遊戲都是從零打造,這段期間的學習和摸索,給我很多意外的驚喜,a- ha moment 當然更是少不了。ok,閒話講完了,回到技術本題 -- State machine 和 Event bus,這兩個是偏架構,而不是演算法/資料結構 (我對架構比較有心得,演算法則有待加強...) 這幾年我寫的遊戲,就我上面提到的各種類別,都是以 state machine 為主幹,event bus 為渠道 架構而成的。

State machine (Finite State Machine)

Finite State Machine, FSM 按 Wiki 的定義是 -- 用固定數量的狀態,來編排行為 -- 的一種模型。遊戲世界裡的物件,狀態的種類大部份是固定的,然後隨著遊戲的進行,物件在這些狀態間來來回回的切換。舉例來說,Super mario bros. 中的 Mario 會站著、會走、會跑、會跳,然後他不斷的在這幾個狀態間切換;另一個例子像是暗棋,棋子有正面,反面,移動等不同的狀態,然後按照某個規則在切換。不只這些較確實的模擬物件,比較抽象的像是遊戲的流程,它也是在好幾個狀態間循環:遊戲封面 -> 選關 -> 戰鬥 -> game over -> 遊戲封面。透過這樣的模型,原本行為複雜的物件,變得有條有理。它在不同的狀態裡,受到刺激,會有不同的反應,會有不同的下個狀態。

FSM 有其原本的數理意義 (見 wiki),我們公司的遊戲裡大多採用的是 Event-driven finite-state machine 即由事件來趨動狀態的轉移。實作上,我們將物件的 state machine 包裝成像黑盒子,讓外界無法直接得知物件在哪個時間點,處於哪個狀態。要改變狀態,只能由透過將事件 (event) 傳給物件,物件的某個狀態收到後,才會做出反應。比方說: Mario物件 目前在 "站著狀態",玩家按了 B 鈕,產生了跳的指令 (事件),傳到 Mario 的 "站著狀態",它即刻反應,將狀態切換到 新的 "跳躍狀態",Mario 就跳了起來。如果這時玩家再按 B 鈕,將事件再傳給 Mario,這時 "跳躍狀態" 收到了,但它設計成不理會 B 鈕的事件。因此 Mario 才不會 "一直跳上去"。

透過這樣的模型,每個狀態只處理它關心的事件,並且做相對應的反應,在程式碼上達到 Single responsibility principle 的要求。另外,將 State Machine 封裝,限制外界無法得知實際的狀態,外界只能試著產生事件來趨動,這滿足Tell, Don't Ask的建議。使用 event driven state machine,可以讓撰寫的程式碼更好維護,而且可以開發更多更複雜的行為。

套用 State machine 時,可以很單純,也可以很複雜,簡單的物件可能一個 state machine 就夠了。複雜的行為可能需要兩個以上平行的 state machine 來管理。例如你的戰士物件要能邊跑邊開槍,你可能就需要一個 machine 管腳的移動,另一個 machine 管理手來發射火器。除了平行外,也需要階層式的 state machine -- 父層的 machine 管理眾多的子 machine。比方說剛才戰士的例子,手腳的兩個 machine 可能是由另一個上層的 machine 來管理 -- 它負責戰士的 生與死 兩個狀態。不難想像手腳的 machine 是歸 "生" 這個狀態來管吧? 在 "死" 的狀態時,手腳的 machine 要關閉不用的。

Event bus (Publish/subscribe)

Publish/subscribe 這個 pattern 沒有一個好記得名字。短一點嘛有人直接叫 pubsub,不過這也不是正式的名稱。pubsub 有一個實作叫 Event bus,這名字好多了,也比較容易望文生義。所以我比較傾向管這個 pattern 叫 event bus。Event Bus 透過發佈 (publish) 事件到某個主題 (topic),將事件傳送給所有訂閱人 (subscribe)。python pubsub 有一張圖可以看得很清楚:


圖片來源: python pubsub

Event bus 有很多的用途,其中一樣是用來克服 Observer pattern 的問題。Observer 需要取得要觀察對象的 reference,而且還要在對象上加上 listener。這在一般商用的 GUI 程式中,沒什麼,但是遊戲裡就不是這樣了。大量的物件散佈在遊戲世界裡各個層級,而且需要複雜的溝通和互動。要將物件 reference 傳到不同的層級太困難了 (arguements 一大串),而且越多的物件互相 reference,表示耦合度越高,程式將越難管理。導入 Event bus 後,每個物件只將自己發生的事散佈出去 (broadcast),其他物件要交流,只要訂閱該物件的主題 (topic) 即可。而原來的物件本身不再涉及別的物件的行為,達到 Loose coupling。Loose coupling 也代表容易寫測試,無怪 google 裡的測試狂們在 GWT 裡也要推 Event bus 的概念了。

Event Bus + State Machine

我們的遊戲實作中,State Machine 設計成由事件趨動,而 Event Bus 主要用來傳遞事件。是的,這是混然天成的組合。遊戲中很多部份是由 State machine 做為主幹,透過 event bus 傳送事件,將狀態不斷的切換,推行整個遊戲的進行。如果用 web 來類比的話,就像是將 controller 做為 web 的主幹,透過 request 來完成服務這樣。

我剛接觸遊戲程式時,我們團隊就有一位資深遊戲開發,他的程式就已經是 Event Bus + State Machine 的架構了。如果 google 查詢,也可以發現 state machine 用在很多成功的遊戲上。不過資料都是零零碎碎的,而 event bus 則資料更少。在遊戲界,似乎沒有一本類似 Effective Java 的書給開發者一個好的指引。而且看別人用是一回事,自己實際開發又是另一回事。我照著之前的開發技術延伸,將這兩個 pattern 逐漸應用在不同的遊戲類別裡。很驚奇的是全都套用的很好,(當然這是事後論,套用過程中免不了一直撞牆),程式的可讀性自然清楚不少 (我的第一個遊戲沒有使用 state machine,現在我一點都不想再維護它了!) 當然,我可能是犯了全天下程式設計師都會犯的錯 -- "有了鐵鎚什麼都當釘子敲"。不過這幾年下來,在眾多的遊戲類別上都成功的運用這兩個 pattern,讓我有自信可以下一個結論:

當設計、架構一個遊戲時,State machine 和 Event bus 是個好的開始

這就像是寫 Java application,我們有 Spring Framework 可以運用,雖然不見得每個 app 用 Spring 都是最佳解,但選用 Spring 是個不錯的開始,在架構上它經過了業界的考驗,它也提供很多方便的功能解決商用常見的問題。

遊戲界相較 web 界是比較封閉,也比較沒有統一的需求,因此有一個好的開始,清楚的架構方向,我個人認為相當的重要,可以節省很多試誤的工夫。而且,如果架構確立,有一個統一的準則讓開發人員參考,專案就可以拆細,轉發給不同人開發,這代表可以 scale,你的團隊可以開發較大的遊戲。做 web app 專案的人應該很習慣某些需求 (按 controller 拆) 拆給不同的開發者做 -- 只要開發人員習慣 MVC, SQL 常見的開發架構環境,各個子功能就可以交給不同人各自開發。原本我們的遊戲開發做法很混亂,該遊戲如果原本由兩人負責,就只能由那兩人開發,很難拆出個子功能出來,讓別人加入、加速開發。因此有一個可以滿足不同類型遊戲的統一架構,統一概念,開發團隊的資源分配就更靈活。

不同遊戲間的需求差異很大,統一的架構有助於開發團隊 scale

當然,我本文中的遊戲界是專指 flash 遊戲,開發時程最長也是一年吧?其他 desktop, console game 那種大資本的遊戲就不在我討論的範圍內了。(我也不大清楚他們是怎麼開發的,用的架構是什麼)


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>