1、虛拟仿真引擎和遊戲引擎在消息機制方面的異同
虛拟仿真引擎與遊戲引擎極為相似,但又有其不同之處。遊戲引擎重在遊戲體驗,是以60-120HZ的畫面重新整理率和事件重新整理率依然足夠。但虛拟仿真引擎不但需要仿真體驗,更需要更高速度的消息機制。試想在動作捕捉的應用中,動作捕捉裝置的重新整理率的典型值是120HZ,或者是需要記錄一個裝置的運動軌迹,也需要更高的重新整理率。但是在中等以上複雜度的渲染中根本無法達到120HZ,同時如果需要進行立體渲染,可以預見渲染速率是不會超過60HZ的,因為超過60HZ的立體渲染已經超過了顯示裝置的極限。
是以虛拟仿真系統需要更為高速的消息機制,這也就是為什麼VRPN的标準伺服器的重新整理率是1000HZ。典型的渲染循環重新整理率在30-60HZ之間,這就造成了一個巨大的不平衡:渲染的低速和操作的高速。正确的方式應該是操作消息能夠以1000HZ以上的重新整理率在引擎中存在,是以必須為操作消息提供一個新的線程。而消息管理器之是以定義為1000HZ是因為可以簡單的使用Sleep(1)來節省大量的CPU開銷,這樣消息線程的CPU消耗率會降低到單核的1%以内。如果需要更高的速度,隻需要取消等待,或者使用其他的等待方式,則幾萬、幾十萬的重新整理率都是可實作的。
2、為什麼需要一個通用的消息管理器
并不是所有的子產品都必須用到Delta3D的資料類型和各種對象,如果為我們的每一個子產品都加入這麼一個限制是不好的。比如我需要我的矩陣能進行一個特别的變換,Delta3D中的矩陣并不具備這個功能,這個時候,我不應該去重寫Delta3D
的矩陣類,因為這樣會造成很大的改變。以及需要傳遞的消息,不應該是複雜的已經定義的類型,而應該是簡單的每個子產品都能解析的資料。這樣,每一個子產品才能更簡單的獲得所需要的資料。
另一方面,在渲染一個場景時,如果已經顯示了場景内的UI,則場景内的其他操作都應該被停止。這時,如果沒有消息管理器,就必須更改場景的狀态,更新各個節點或者相機管理器告訴他們,你們暫時不要動。而如果消息管理器具有優先級的概念,UI
目前擷取了這個消息,并且已經處理,傳回該消息已經被處理,則該消息不再繼續傳遞,這樣就避免了很多無謂的狀态更改。
3、如何使用消息管理器獲得資料
看到消息管理器有那麼多的功能,必然會想到使用消息管理器會不會很複雜,需要很多的步驟。實際上使用消息管理器獲得資料是很簡單的事情,隻需要繼承一個接口,并實作兩個函數即可。
需要繼承的接口是IEventListener,它具有兩個方法需要被實作:OnEvent和GetListenerOption。下面是IEventListener的代碼:
enum EPriority
{
RealTime,
High,
AboveNormal,
Normal,
BelowNormal,
Idle
};
struct SListenerOpt
{
int mRegMessages; // 偵聽消息的類型
EPriority mPriority; // 消息優先級
};
struct IEventListener
{
IEventListener()
{ // 向消息管理器注冊此偵聽器的代碼 }
virtual SListenerOptGetListenerOption() = 0;
virtual boolOnEvent(EEventType event, void* eventDat) = 0;
// 多線程支援 内部在OnEvent之前會檢查是否被Lock 如果Lock則跳過
void Lock(); // 當你需要讀取消息相關資料時調用,則消息管理器暫不更新目前偵聽器的資料
void Unlock(); // 當資料讀取完畢時調用,消息管理器可以更新其資料。
};
EEventType是傳回的消息類型,将在下一節中實作。典型的實作方式為:
class CCameraManager : public IEventListener
{
//Members
//Functions
SListenerOptmListenerOpt; //在構造函數中定義或實時修改擷取消息的優先級以及擷取的消息類型
virtual SListenerOptGetListenerOption() { return mListenerOpt; }
virtual boolOnEvent(EEventType event, void* eventDat)
{
switch(event)
//将資料放入正确的位置
return //如果消息不再傳遞傳回true;
//接收事件的實質是阻止此事件的繼續發送,需配合優先級謹慎使用
}
}
4、如何實作消息管理器
消息管理器管理各個消息偵聽者所能獲得的消息以及其權限。消息管理器中消息可以分為多類,典型的有:輸入消息、UI消息、引擎消息和實體消息等。首先看一下消息類型的定義,消息類型以BitMask的形式定義:
#define MESSAGE_TYPE_INPUT 0x00000001
#define MESSAGE_TYPE_UI 0x00000002
#define MESSAGE_TYPE_ENGINE 0x00000004
#define MESSAGE_TYPE_PHYSICS 0x00000008
#define MESSAGE_TYPE_COUNT 0x00000004
如果需要接受特定類型的資料則在消息偵聽器的mListenerOpt中的mRegMessages中使用這些定義如:mListenerOpt. mRegMessages= MESSAGE_TYPE_INPUT | MESSAGE_TYPE_UI;這樣,這個偵聽器就可以偵聽輸入消息和UI的消息了。同樣,如果修改了mListenerOpt. mPriority則消息的接收權限會更改。這兩個偵聽器的屬性可以配置為固定或者實時更改,可以根據需要進行配置。
消息管理器是一個引擎中非常重要的部分,它就像一個交通樞紐,讓各個部分能夠快速的交換資料。下面就來描述一下消息管理器的實作和使用。它需要儲存2個清單:
- 所有已經注冊的偵聽器的清單
- 每個優先級的偵聽器的清單
已經注冊的偵聽器清單可以友善的便利所有的偵聽器檢視他們的狀态改變,優先級清單可以保證高優先級的偵聽器率先被賦予資料。
下面是消息管理器的簡單實作:
class CMessageManager
{
public:
CMessageManager() { // 初始化并啟動消息管理器線程 }
~ CMessageManager() { // 銷毀清單并結束消息管理線程 }
void MainLoop(); // 檢查輸入裝置狀态 發送消息
void CheckOpt(); // 檢查每個偵聽器的狀态 優先級改變後改變所在的清單
voidPostMessage(EEventType event, void* eventDat); // 其他子產品發送的消息,多線程支援
}
這樣就實作了一個簡單的消息管理器,隻需要在引擎初始化的最開始建立一個它的新執行個體,并且在引擎結束時銷毀它就可以了。偵聽器會在被建立時自動注冊到這個消息管理器,消息管理器内部以1000HZ或更高的速度重新整理資料,并發送到各個需要這些消息的子產品。典型的消息處理流程将在第七節進行描述。
下面說一下消息的類型,一個引擎中有很多種的消息類型,但可以預見的是總消息類型基本不會超過一個32位整數所表達的最大數值,消息的分類應該不會超過1024種,單種類型的消息其細分量不會大于32 * 65536 = 2097152種,是以消息類型的定義如下:
#define EVENT_TYPE_BASE_INPUT 0x00000000
#define EVENT_TYPE_BASE_UI 0x00200000
#define EVENT_TYPE_BASE_ENGINE 0x00400000
#define EVENT_TYPE_BASE_ PHYSICS 0x00600000
enum EEventType
{
EVENT_INPUT_MENU_BUTTON= EVENT_TYPE_BASE_INPUT,
EVENT_INPUT_XX_XX,
…
EVENT_UI_OPEN_MAINPAGE=EVENT_TYPE_BASE_UI,
EVENT_UI_XX_XX,
…
EVENT_ENGINE_INIT_BEGIN= EVENT_TYPE_BASE_ENGINE,
EVENT_ENGINE_XX_XX,
…
EVENT_ PHYSICS_CREATE_BOX= EVENT_TYPE_BASE_ PHYSICS,
EVENT_ PHYSICS_XX_XX,
…
}
消息資料定義為何使用void*,首先它可以傳遞任何指針資訊,是以使用它不會出現無法傳送的消息。如果使用執行個體來進行傳輸如:VEC3、MAT4之類,每次的消息發送都會調用其構造函數并進行指派,這樣對性能是巨大的損失,是以選擇使用指針方式進行資料傳輸,如果此資料是有用的,則由OnEvent中實作的代碼來解析出對本子產品有用的資料并儲存。
5、如何向消息管理器發送消息
不管目前的代碼處于哪個線程,哪個子產品,都能随時向消息管理器發送消息,這些消息将實時的被傳送到需要這些消息的各個子產品當中去。
當一個子產品想要發送一個消息時,僅需要調用消息管理器的PostMessage方法即可,标注好消息類型,構造好消息資料,就能夠将資料發送出去了。
6、典型的消息處理流程
本節将描述一個按鍵從按下到觸發UI或引起場景内改變的一系列過程。首先從按鍵按下開始說起。
當鍵盤、滑鼠或者任何VR裝置的按鈕按下時,VRPN的伺服器将獲得這些資料并将其廣播出來,VRPN的用戶端接受到這些消息後,調用其按鍵回調來将這些資料發出。每一個VRPN的用戶端都是一個裝置的執行個體,是以需要定義一個裝置的類來完成這個步驟,當然還需要一個裝置管理器來重新整理這些裝置,這些類都已經實作,暫時隻講述裝置類:
class CDevice
{
std::stringmDeviceName;
int mDeviceID;
intmTrackCount, mButtonCount, mAnalogCount;
vrpn_Tracker_Remote*mTrackers;
vrpn_Button_Remote*mButtons;
vrpn_Analog_Remot*mAnalogs;
// 構造和解析函數
voidUpdate(); // 重新整理VRPN用戶端,向消息管理器發出消息,内容有裝置ID、改變的類型、改變的資料。
}
現在通過調用每個裝置的重新整理函數,按鍵消息已經發送到了消息管理器,發送的消息類型是MESSAGE_TYPE_INPUT,現在假設消息管理器中有三維場景中的UI和Engine兩個子產品在偵聽Input類型的資料,并且UI的優先級較高。
消息管理器重新整理時,發現有一個按鍵的狀态改變的事件,這時首先周遊所有的偵聽器,檢視是否有偵聽器的優先級、偵聽事件發生了改變,如果改變了,則移動到新的優先級清單中。然後根據優先級,逐一向偵聽器發送此消息,一旦某個偵聽器傳回了消息已經被處理,則跳出此消息的發送。
void CMessageManager::MainLoop()
{
// 檢查優先級并改變清單
void CheckOpt();
for (每個優先級)
for(每個優先級的每個偵聽器)
偵聽器. OnEvent
如果傳回true,則處理下一條消息
}
此時在較高的優先級将消息發送給了UI,如果UI處于激活狀态,則UI會在OnEvent處傳回true,接受了這個按鍵按下的事件。反之,UI不接收這個事件,稍後會被Engine來接收到,并在場景中做出相應的改變。繼續說UI激活的狀态,UI獲得了這個消息,并将其存儲在偵聽器的某個變量内,UI更新線程發現此變量的改變,然後将檢視它具體改變了哪些内容。假設這個按鈕是在場景中增加一個球體,則UI處理後,會使用PostMessage向消息管理器發送增加一個球體的消息,這次消息管理器的循環中,發現UI不接收這個消息,是以消息被傳送給了Engine,然後Engine接收這個消息,并且在三維場景中增加一個球體。這樣一個典型的消息處理流程就顯現出來了。任何子產品都能夠使用這個模型來接收、處理、發出消息,并且是在多線程的情況下,互不幹擾的完成。
7、從消息管理器看到的
一個功能完備的引擎就像是一台汽車或者是一個團隊,每個部件或者成員必須通力合作才能真正的完成想要完成的工作。
試想如果每個人接到資訊就先揣在兜裡,然後在想告訴别人的時候再去告訴别人,這應該不是一個正确的資訊通路。因為資訊的傳遞可能會因為主觀因素而變了味道。
引擎中就是使用一個部件接受了這些資料,然後在傳遞給别的部件,這樣有可能會因為部件設計中沒有完備的考慮到資料的所有情況而導緻資料傳遞失真。同樣會導緻其他部件出現不應出現的毛病,而這樣的毛病是很難定位的。
目前是資訊社會,資訊的傳遞被認為是非常重要的因素,而引擎也可以看作一個團體,花費不多的時間為引擎添加一個可靠的消息傳導機制我覺得是很有必要的。就像一個團隊也需要一個人能夠把消息準确的傳達給團隊裡的每一個人,而不是感覺需要的時候再告訴别人。
二來,守舊思想實在是阻礙新技術的絆腳石。前段時間推行CMake管理項目的時候,我也感覺不習慣,不想要去更改現在已經習慣的東西。不過随着對它的使用,慢慢發現這是個很好的東西。現在大家年紀都還不大,還在學習和積累的階段,接觸的事情還很少,如果現在就有守舊的思想,實在是會影響自己的進步。是以在接觸一個新事物時,一定要注意不要讓守舊思想、經驗主義束縛了自己的思想,而不去接受新事物。