在開發遊戲時除錯之所以的原因之一是因為要管理的狀態太多了,如果仔細的觀察一個遊戲會發現遊戲就像是一個巨大的狀態機。從玩家所在的關卡畫面、關卡內的 NPC、怪物等等到玩家自身都帶有狀態。也因此常常會需要透過一個中央控管的系統來輔助,由統一的中央管理者去管理所有狀態的更新與變化才相對容易維護這些繁複的狀態。
這樣的特性其實就跟這幾年前端領域面臨的問題類似,因為網頁的互動上有非常多的狀態而且可能是互相依賴的。為了能夠管理越來越龐大的狀態,開始出現像是 Redux 這樣的套件,採取中央統一管理狀態的方式來處理這些複雜的變化。
如果仔細比較的話,會發現 Redux 似乎也有點 Singleton 的感覺存在。這幾年像是 Unity 這類引擎所所採用的元件式設計以及蛻變出來的 ECS (Entity Component System) 機制,在很多思考方向上跟 React/Redux 這類前端框架上有類似的地方,從這方面來看這種作法應該是一種目前相對好的狀態管理方式。
不過 Unlight 設計的時間還是在這些理論還沒有出現或者成熟之前因此並沒辦法用來當作前面提到的 ECS 這類機制的運作,如果我們想要了解 Unlight 在客戶端是怎麼去控制跟調整 UI 的呈現來配合遊戲的話,倒是可以參考 React 早期提出的 Flux 機制來簡單對照一下。
我們借用 React 官網上的這張圖片
在這邊我們可以看到每個操作都會透過「動作(Action)」對「觸發器(Dispatcher)」操作,然後 Dispatcher 在對儲存器(Store)進行處理,最後反映在畫面(View)上。
因為畫面等同於玩家的操作畫面,所以當玩家操作時會再次產生一個動作並且重複上述描述的行為藉此不斷反應出不同的行為。
我們以 Unlight 登入畫面的「登入」動作來當作範例,看看在 Unlight 中是如何應用 Singleton 跟 Event 機制的。
建議可以先對 React 的 Flux 或者 Redux 的設計概念有所了解在反過來思考這個機制,在遊戲中因為有很多狀態變化跟非同步的操作,因此有一個統一管理遊戲的狀態機以及配合非同步行為的事件處理系統是很重要的(JavaScript 和 ActionScript 都是基於 EMCAScript 設計的,因此天身就有不錯的事件管理機制)
我們先打開 src/view/image/title/LoginPanel.as
這個檔案,他是 src/view/TitleView.as
的一部分,也是我們打開遊戲看到的第一個畫面。
private function buttonClickHandler(e:MouseEvent):void
{
SE.playClick();
if (_state == 1)
{
_ctrl.login();
}
else if (_state ==2)
{
_ctrl.regist();
}
}
我們會找到這段程式碼,他是用於處理「送出」按鈕的機制,會看到他呼叫了 _ctrl
來針對不同狀況做不同的處理,這邊其實都是 LoginButton
的處理行為,大概是 Unlight 設計介面時沒有特別區分登入跟註冊,就直接混用的關係。
往回看前面定義 LoginPanel
屬性的時候,會先發現 _ctrl
其實是 TitleCtrl
的實例,並且是一個 Singleton 物件。
private var _ctrl:TitleCtrl = TitleCtrl.instance;
這邊的 Controller 扮演的角色類似於 Dispatcher 的部分,也就是如果要做任何操作的話都應該要使用 Controller 來操作,而不是直接呼叫。
我們繼續看到 src/controller/TitleCtrl.as
關於 login
方法的部分
/**
* ログイン
* @param userName 名前入力のTextInput
* @param passName PASS入力のTextInput
*
*/// ログイン
public function login():void
{
log.writeLog (log.LV_DEBUG,this,"Login buttun pushed ",_loginPanel.userName);
if (_loginPanel.userName == "")
{
// Alerter.showWithSize('名前を入力してください', 'Error');
Alerter.showWithSize(_TRANS_MSG_NAME, 'Error');
}
else
{
log.writeLog (log.LV_DEBUG,this,"Login",_loginPanel.userName,_loginPanel.pass);
_loginPanel.panelEditable(false);
// 各種ハンドラを登録
player.addEventListener(Player.AUTH_FAILED, authFailHandler);
player.addEventListener(Player.AUTH_SUCCESS, authSuccessHandler);
// プレイヤーがログイン
player.login(_loginPanel.userName,_loginPanel.pass);
}
}
在這邊 login
方法會直接抓取 Panel 上玩家輸入的資訊作為判斷的參考,如果是在 Redux 的話我們應該要製作成一個 Event 加上 Payload 放進去。
接下來 TitleCtrl 會對 player
變數綁定事件跟呼叫,我們先看一下 player
物件的定義是什麼。
我們無法在 TitlCtrl 找到,但是可以在 BaseCtrl 裡面發現關於 player
的定義。
protected var player:Player = Player.instance;
又是一個 Singleton 物件,而 Player 物件在這邊扮演的角色是 Model (src/model/Player.as
) 在大多數的情況下可以視為儲存資料的地方,也就剛好對應了前面提到的 Store 角色。
所以根據前面看到的 login
行為,TitleCtrl
會等待登入伺服器回傳結果來作出對應,反映到 View 上面(authFailHandler
和 authSuccessHandler
)
最後我們在看一次 src/modle/Player.as
裡面的 login
做了些什麼。
/**
* プレイヤーのログイン
*
*/
public function login(n:String, p:String):void
{
_name = n;
pass = p;
dispatchEvent(new Event(AUTH_START));
log.writeLog (log.LV_DEBUG,this,"Login ",n,p);
}
在 Player 裡面會把玩家的帳號跟密碼記錄下來,然後觸發 AUTH_START
事件(這跟 Unlight 的流程有關)後面則會進行一連串跟伺服器互動的認證,最後產生 AUTH_FAILED
和 AUTH_SUCCESS
的事件作為結果。不過大致上來說還是扮演 Store 的角色(以及一部分的 Dispatcher)
基本上在 Flux 或 Redux 裡面,Store 和 Dispatcher 應該都會是 Singleton 的形式,所以把一些通用的處理邏輯轉換來看 Unlight 在客戶端控制 UI 的設計剛好是能大致上呼應的。
在遊戲中我們其實是很常需要使用 Singleton 的機制,像是關卡切換的時候我們會清除前一個關卡建立新的關卡,假設是單一玩家的遊戲就需要讓玩家(狀態)物件是 Singleton 的,這樣才不會因為切換關卡而遺失了玩家的資訊。
雖然 Flux/Redux 的應用方式並不完全適合在遊戲中使用,不過我們可以借鑑這些思考方式來重新評估要怎麼在遊戲中切割與區分才能使遊戲更容易擴充與調整。
我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。