2010年12月17日 星期五

引擎中不可或缺的事件訊息系統

嗯...對...

很久沒有發文了...

「懶」是唯一的解釋...

前一陣子看到一個引擎中的訊息系統架構,突然間,對整個引擎的架構設計有了新的想法。或者該說,解決了我心中長久以來的難題。

說穿了,不過就是事件訊息驅動式 ( Event Driven ) 的架構設計。

這東西被我晾在一旁太久,已經完全忘記它的存在了。

簡單舉個例子說吧...

遊戲中有很多系統,會受到攝影機的位置方向的影響,例如,天空盒子 ( Sky box ) 必須隨著攝影機的位置變動位置,動態讀取的無接縫地圖系統也必須檢查攝影機的位置來決定是否讀取地圖或是釋放地圖。如果在一個單純的程序式的架構中,我們會在攝影機位置變動的時候,呼叫這兩個物件系統來做相應的更新檢查。

像是這樣:

CameraManager::UpdatePosition(Camera* camera, Vector3 pos)
{
  camera->UpdatePosition(pos);
  skybox->UpdatePosition(pos);
  seamless_world->CheckLoading(pos);
}

只不過,這麼一來,我們可以非常確定的是, Camera Manager 這個物件已經跟 Skybox 、 Seamless World 分不開了。三個物件綁在一起的結果是,物件的再利用性大大降低。( 想想如果我們需要將 sky box 改為 sky doom, 又或者我們不再需要無接縫地圖的時候... )

這還只是其中一個問題。

另外一個問題是,在我們遊戲越做越大,引擎越寫越深入的時候,我們發現, LOD ( Level of Detail ) 的計算也需要隨著攝影機變動而更新,水面倒影效果也跟攝影機有關,粒子系統的繪製也需要攝影機的資料...

於是乎,我們又把 LOD 系統、水面倒影、粒子系統全綁到了攝影機物件裡。

更甚至於,第三人稱視角的遊戲裡,攝影機是跟著主角走的,所以,又把攝影機綁到了主角身上,跟著主角更新。

最後的結果就是,整個引擎裡各式各樣的系統物件,因為彼此之間相互影響的關係,全部混成一團。

現在換個角度,改用事件驅動來做。

攝影機變動的時候,我們不管 Skybox 、 Seamless World,只送個訊息給事件系統。

像是這樣:

CameraManager::UpdatePosition(Camera* camera, Vector3 pos)
{
  camera->UpdatePosition(pos);
  event_system->Send(idCameraPositionUpdated, camera);
}

而對於 Skybox, Seamless World 來說,必須有函式負責來接收這個事件,同時,在物件的初始化的時候,必須先把這個函式登記到事件系統裡。

Sky Box 的接收函式大概會長得像這樣:

Skybox::OnCameraPositionUpdated(Camera* camera)
{
  UpdatePosition(camera->GetPosition());
}

其他的,需要隨著攝影機更新而更新的物件,也都會有相類似的函式,同時也都需要將函式登記到事件系統中,事件系統在收到攝影機送來的事件後,就會分發事件,一一呼叫每個需要的事件處理函式。

這樣子,事件系統將我們的各個物件獨立開來,減少了很多錯綜複雜的關連。

更進一步來,我們可以在玩家主角的位置更新的時候,發送一個事件出來,然後在攝影機系統裡,寫個這樣的函式:

CameraManager::OnPlayerPositionUpdate(Player* player)
{
  UpdatePosition(player->GetPosition());
}

當然了,這樣的事件驅動架構不是沒有缺點的。缺點就是,為了遵循這個架構模式,我們必須多寫很多 code ,例如,本來可以直接在攝影機裡呼叫的函式,現在必須寫一個事件處理函式來呼叫它,而且還必須要把事件處理函式登記到系統裡,有點煩...

但是多花這些工絕對是值得的...

2 則留言:

steven's home 提到...

您好,想請教,無接縫地圖的做法是甚麼?如果是預先判斷地圖的更換方向而採用預先讀取的話,那對記憶體的需求就提高了,有辦法盡量不會用到太多記憶體嗎?

藍斯洛 提到...

據我所知的無接縫地圖的做法,通常就是將玩家所在位置,附近連接的地圖區塊,預先讀取。同時,也可以將已經不在範圍內的地圖區塊卸載。

至於記憶體的使用麼,這樣的做法是必然會用到幾倍以上的記憶體,我也還沒有想到有什麼方法可以在這個機制下降低大量的記憶體使用量。