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管理项目的时候,我也感觉不习惯,不想要去更改现在已经习惯的东西。不过随着对它的使用,慢慢发现这是个很好的东西。现在大家年纪都还不大,还在学习和积累的阶段,接触的事情还很少,如果现在就有守旧的思想,实在是会影响自己的进步。所以在接触一个新事物时,一定要注意不要让守旧思想、经验主义束缚了自己的思想,而不去接受新事物。