1、虚拟仿真引擎和游戏引擎在消息机制方面的异同
虚拟仿真引擎与游戏引擎极为相似,但又有其不同之处。
游戏引擎重在游戏体验。所以60-120HZ的画面刷新率和事件刷新率依旧足够。但虚拟仿真引擎不但须要仿真体验。更须要更快速度的消息机制。试想在动作捕捉的应用中,动作捕捉设备的刷新率的典型值是120HZ,或者是须要记录一个设备的运动轨迹,也须要更高的刷新率。可是在中等以上复杂度的渲染中根本无法达到120HZ,同一时候假设须要进行立体渲染,能够预见渲染速率是不会超过60HZ的,由于超过60HZ的立体渲染已经超过了显示设备的极限。
所以虚拟仿真系统须要更为快速的消息机制。这也就是为什么VRPN的标准server的刷新率是1000HZ。典型的渲染循环刷新率在30-60HZ之间。这就造成了一个巨大的不平衡:渲染的低速和操作的快速。正确的方式应该是操作消息可以以1000HZ以上的刷新率在引擎中存在,因此必须为操作消息提供一个新的线程。
而消息管理器之所以定义为1000HZ是由于可以简单的使用Sleep(1)来节省大量的CPU开销。这样消息线程的CPU消耗率会减少到单核的1%以内。假设须要更高的速度。仅仅须要取消等待,或者使用其它的等待方式。则几万、几十万的刷新率都是可实现的。
2、为什么须要一个通用的消息管理器
并非全部的模块都必须用到Delta3D的数据类型和各种对象。假设为我们的每个模块都增加这么一个限制是不好的。比方我须要我的矩阵能进行一个特别的变换。Delta3D中的矩阵并不具备这个功能。这个时候,我不应该去重写Delta3D
的矩阵类,由于这样会造成非常大的改变。以及须要传递的消息。不应该是复杂的已经定义的类型。而应该是简单的每个模块都能解析的数据。这样,每个模块才干更简单的获得所须要的数据。
还有一方面,在渲染一个场景时,假设已经显示了场景内的UI。则场景内的其它操作都应该被停止。
这时,假设没有消息管理器,就必须更改场景的状态,更新各个节点或者相机管理器告诉他们。你们临时不要动。而假设消息管理器具有优先级的概念。UI
当前获取了这个消息,而且已经处理。返回该消息已经被处理,则该消息不再继续传递,这样就避免了非常多无谓的状态更改。
3、怎样使用消息管理器获得数据
看到消息管理器有那么多的功能,必定会想到使用消息管理器会不会非常复杂,须要非常多的步骤。实际上使用消息管理器获得数据是非常easy的事情。仅仅须要继承一个接口,并实现两个函数就可以。
须要继承的接口是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设备的button按下时,VRPN的server将获得这些数据并将其广播出来,VRPN的client接受到这些消息后,调用其按键回调来将这些数据发出。每个VRPN的client都是一个设备的实例,所以须要定义一个设备的类来完毕这个步骤,当然还须要一个设备管理器来刷新这些设备。这些类都已经实现,临时仅仅讲述设备类:
class CDevice
{
std::stringmDeviceName;
int mDeviceID;
intmTrackCount, mButtonCount, mAnalogCount;
vrpn_Tracker_Remote*mTrackers;
vrpn_Button_Remote*mButtons;
vrpn_Analog_Remot*mAnalogs;
// 构造和解析函数
voidUpdate(); // 刷新VRPNclient。向消息管理器发出消息,内容有设备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更新线程发现此变量的改变,然后将查看它详细改变了哪些内容。
如果这个button是在场景中添加一个球体,则UI处理后,会使用PostMessage向消息管理器发送添加一个球体的消息,这次消息管理器的循环中。发现UI不接收这个消息。所以消息被传送给了Engine,然后Engine接收这个消息,而且在三维场景中添加一个球体。
这样一个典型的消息处理流程就显现出来了。不论什么模块都可以使用这个模型来接收、处理、发出消息,而且是在多线程的情况下,互不干扰的完毕。
7、从消息管理器看到的
一个功能完备的引擎就像是一台汽车或者是一个团队。每一个部件或者成员必须通力合作才干真正的完毕想要完毕的工作。
试想假设每一个人接到信息就先揣在兜里。然后在想告诉别人的时候再去告诉别人,这应该不是一个正确的信息通路。由于信息的传递可能会由于主观因素而变了味道。
引擎中就是使用一个部件接受了这些数据。然后在传递给别的部件,这样有可能会由于部件设计中没有完备的考虑到数据的全部情况而导致数据传递失真。相同会导致其它部件出现不应出现的毛病。而这种毛病是非常难定位的。
当前是信息社会,信息的传递被觉得是非常重要的因素,而引擎也可以看作一个团体,花费不多的时间为引擎加入一个可靠的消息传导机制我觉得是非常有必要的。就像一个团队也须要一个人可以把消息准确的传达给团队里的每个人,而不是感觉须要的时候再告诉别人。
二来。守旧思想实在是阻碍新技术的绊脚石。前段时间推行CMake管理项目的时候,我也感觉不习惯,不想要去更改如今已经习惯的东西。只是随着对它的使用,慢慢发现这是个非常好的东西。如今大家年纪都还不大。还在学习和积累的阶段,接触的事情还非常少。假设如今就有守旧的思想,实在是会影响自己的进步。所以在接触一个新事物时。一定要注意不要让守旧思想、经验主义束缚了自己的思想,而不去接受新事物。
版权声明:本文博客原创文章,博客,未经同意,不得转载。