清源游民 [email protected]
在第一部分里分析了 ExampleFrameListener 大部内容,不过没有穷尽,它其实也包含了输入部分。因为 CEGUI 本身没输入侦测功能,他需要外部力量的帮助。在 ogre1.4 之间的 demo 中,输入部分是 ogre 自带的,功能有限(对于我这个菜鸟,暂时还用不到特别的功能 ^_^ )。新版本里,干脆把这部分去掉了,输入部分采用了新的类库 OIS - Object-oriented Input Library . 他的作者本身就是 OGRE team 的成员,我们可以相信,他可以与 Ogre 一起工作得很好 .OIS 支持键盘,鼠标,游戏杆,最后一项暂时不讨论 . 在 windows 平台下, OIS 提供了对 DirectInput 的封装,基本的输入功能是由后者来实现的。既然是学习,我们不防先大概理解一下 DirectInput 基本功能与使用流程以及 OIS 是如何将这些功能封装进去的。我的目的是要讨论 CEGUI 的,现在转到了 OIS, 又转到了 DirectxInput, 似乎是离目标越来越远了。呵,在菜鸟的眼中,什么东西都是菜,都有营养,不防咀嚼个一下下。 CEGUI 固然是好东西,但是如果我们菜鸟整天只会学一堆堆没完没了的类库,那么我们可能永远就只是个菜鸟了,一句话,知道他做了些什么,比光知道他怎么用那可是相当有效!费话少说了,先看一下 DirectInput, 从 MSDN 上直接抄了一段, 懒得翻译了: 下面第一段话,说了理解directInput需要了解的一些基本概念与术语,因为以前看过一点,大概知道是些什么。而我现在主要是做学习笔记,不是教程,所以不解释了。
To understand DirectInput, it is essential to understand the following terms.
DirectInput object: The root DirectInput interface. · Device: A keyboard, mouse, joystick, or other input device. · DirectInputDevice object: Code representing a keyboard, mouse, joystick, or other input device.·
Device object: Code representing a key, button, trigger, and so on found on a DirectInput device object. Also called device object instance.
第二段话说明了使用 DirectInput 的基本步骤, OIS 把这些东西封装起来了 .
The following steps represent a simple implementation of DirectInput in which the application takes responsibility for ascertaining what device object (button, axis, and so on) generated each item of data.
1. Create the DirectInput object. You use methods of this object to enumerate devices and create DirectInput device objects.
2. Enumerate devices. This is not an essential step if you intend to use only the system mouse or keyboard. To ascertain what other input devices are available on the user's system, have DirectInput enumerate them. Each time DirectInput finds a device that matches the criteria you set, it gives you the opportunity to examine the device's capabilities. It also retrieves a unique identifier that you can use to create a DirectInput device object representing the device.
3. Create a DirectInputDevice object for each device you want to use. To do this, you need the unique identifier retrieved during enumeration. For the system mouse or keyboard, you can use a standard GUID.
4. Set up the device. For each device, first set the cooperative level, which determines the way the device is shared with other applications or the system. You must also set the data format used for identifying device objects, such as buttons and axes, within data packets. If you intend to retrieve buffered data—that is, events rather than states—you also need to set a buffer size. Optionally, at this stage you can retrieve information about the device and tailor the application's behavior accordingly. You can also set properties such as the range of values returned by joystick axes.
5. Acquire the device. At this stage you tell DirectInput that you are ready to receive data from the device.
6. Retrieve data. At regular intervals, typically on each pass through the message loop or rendering loop, get either the current state of each device or a record of events that have taken place since the last retrieval. If you prefer, you can have DirectInput notify you whenever an event occurs.
7. Act on the data. The application can respond either to the state of buttons and axes or to events such as a key being pressed or released.
8. Close DirectInput. Before exiting, your application should unacquire all devices and release them, then release the DirectInput object.
了解了这些基本步骤,下面我们打开OIS的源码,看看上面这8步骤封装到了哪里。然后我们返回 ExampleFrameListener, 看看 demo 中是如何使用 OIS 的 .
第一个要看的是InputManager类。他提供了平台无关的接口,负责输入系统的创建。没啥好说的,看源码吧(只列出核心代码):
InputManager * InputManager::createInputSystem( ParamList ¶mList )
{
InputManager* im = 0;
#elif defined OIS_WIN32_PLATFORM
im = newWin32InputManager();
#endif
im->_initialize(paramList);
return im;
}
自然,我们只关心windows平台下的。于是找到源码继续看:
void Win32InputManager::_initialize( ParamList ¶mList )
{
//Create the device
hr = DirectInput8Create(hInst, DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&mDirectInput, NULL );
/Ok, now we have DirectInput, parse whatever extra settings were sent to us
_parseConfigSettings( paramList );
_enumerateDevices();
}
首先,完成了8步骤中的第1步, Create the DirectInput object 。 在_parseConfigSettings ( paramList ); 中对以后将要创建的设备(键盘,鼠标等)的属性,例如独占模式,前台模式等。这些属性列表paramList 经过处理后,之后在创建设备的时候传入。
void Win32InputManager::_enumerateDevices()
{ //Enumerate all attached devices
mDirectInput->EnumDevices(NULL, _DIEnumKbdCallback, this, DIEDFL_ATTACHEDONLY);
}
完成了8步骤中的第2 部分 Enumerate devices ,它是针对游戏杆这类设计的,对于键盘,鼠标并不需要。InputManager 创建之后,就可以用它来创建键盘,鼠标了。它提供了如下的方法:
Object * Win32InputManager::createInputObject( TypeiType, boolbufferMode )
{
Object* obj = 0;
switch( iType )
{
case OISKeyboard: obj = newWin32Keyboard( this, mDirectInput, bufferMode, kbSettings ); break;
case OISMouse: obj = newWin32Mouse( this, mDirectInput, bufferMode, mouseSettings ); break;
obj->_initialize();
return obj;
}
鼠标,键盘都有各自的类封装。下面以键盘为例,看看它的源码中都做了些什么:
void Win32Keyboard::_initialize()
{
1 mDirectInput->CreateDevice(GUID_SysKeyboard, &mKeyboard, NULL);
2 mKeyboard->SetDataFormat(&c_dfDIKeyboard)
3 HWNDhwin = ((Win32InputManager*)mCreator)->getWindowHandle();
4 mKeyboard->SetCooperativeLevel( hwin, coopSetting)))
5 mKeyboard ->SetProperty( DIPROP_BUFFERSIZE, &dipdw.diph )))
6 HRESULThr = mKeyboard->Acquire();
}
这里可是做了不少的工作,看看方法名就可以明白。为说明方便加上了标号。
1, 它做了8步骤中的第3步, Create a DirectInputDevice object for each device you want to use 。
2, 3 4,5它们共同做了8步骤中的第4步- Set up the device 。这里面包括:设置键盘的数据格式,协作模式,其他属性。很明显,6做了8 步步骤中的第5步- Acquire the device 。 意思是告诉DirectInput,我准备从设备上获取数据了,给我做好生伺侯着。准备工作都做好了,可以获取数据了,通过设备提供的下面这个方法来做这件事。这就是8 步骤中的第6件事- Retrieve data
void Win32Keyboard::capture()
{ if( mBuffered )
_readBuffered();
else
_read();
}
从源码中我们可以看到,根据数据的不同,进行了不同的处理。呵,什么不同呢,再从MSDN上抄一段吧,一看就明白: Buffered and Immediate Data
DirectInput supplies two types of data: buffered and immediate. Buffered data is a record of events that are stored until an application retrieves them. Immediate data is a snapshot of the current state of a device. You might use immediate data in an application that is concerned only with the current state of a device - for example, a flight combat simulation that responds to the current position of the joystick and the state of one or more buttons. Buffered data might be the better choice where events are more important than states - for example, in an application that responds to movement of the mouse and button clicks. You can also use both types of data, as you might, for example, if you wanted to get immediate data for joystick axes but buffered data for the buttons.
呵,清楚了吧。继续看代码, capture()有两个分枝,分别处理缓冲数据与立即数据。
首先是缓冲数据,主干代码如下:
void Win32Keyboard::_readBuffered()
{
DIDEVICEOBJECTDATA diBuff[KEYBOARD_DX_BUFFERSIZE];
DWORD entries = KEYBOARD_DX_BUFFERSIZE;
mKeyboard->GetDeviceData( sizeof(DIDEVICEOBJECTDATA), diBuff, &entries, 0 );
//Update keyboard and modifier states.. And, if listener, fire events
for(unsignedinti = 0; i < entries; ++i )
{
KeyCode kc = (KeyCode)diBuff[ i ].dwOfs;
if( diBuff[ i ].dwData & 0x80 )
{
if( listener )
istener->keyPressed( KeyEvent( this,kc,_translateText(kc) ) );
}
else
{
//Fire off event
if( listener )
listener->keyReleased( KeyEvent( this, kc, 0 ) );
}
}
}
这段代码中,GetDeviceData( sizeof(DIDEVICEOBJECTDATA), diBuff, &entries, 0 );取得了缓冲数据,添入了一个数组:尺寸是EYBOARD_DX_BUFFERSIZE,可以看到entries也等于此值。Entries的作用除了开始调用时说明数组的大小,同时在函数调用返回时说明数组里有多少个数据填入(没有足够多的缓冲数据,当然填不满啦,假如entries返回6,说明还有6个按键信息在缓冲里等待处理)。很明显,for循环就是对这些未处理的信息进行处理。KeyCode kc = (KeyCode)diBuff[ i ].dwOfs; 这是看看,这个事件是由哪个键引走.KeyCode是个枚举型:
enum KeyCode
{
C_1 = 0x02,
KC_2 = 0x03,
KC_3 = 0x04,
KC_A = 0x1E,
KC_S = 0x1F,
KC_D = 0x20,
}
确定了是哪个键,接下来就要问了,是按下还是释放?
if ( diBuff[ i ].dwData & 0x80 ) 回答了这个问题,下面做的是
if ( listener )
istener->keyPressed( KeyEvent( this,kc,_translateText(kc) ) );
它把相关的信息内容包装成KeyEvent,作为参数发给了,已经注册了的侦听者.可能不太明白,解释一下:当我们按下了某个键,可以认为是发生了个KeyEvent,程序里会响应这个事件,方法就是把某个类实例注册为侦听者(listener),说白了就是告诉OIS,当键盘按下释放的时候你告诉我啊,我要响应他。这里OIS检测到按键事件发生了,根据侦听者的请求,调用侦听者的方法(keyPressed)。也许有疑问,OIS 怎么知道侦听者实现了keypressed()方法呢?不急先看以下代码,在GUI demo里:
class GuiFrameListener : publicExampleFrameListener, publicOIS::KeyListener, publicOIS::MouseListener
看到了吧,它继承自 OIS:KeyListener ,从OIS的源码中找到它:
class _OISExport KeyListener
{
public:
virtual ~KeyListener() {}
virtual bool keyPressed( constKeyEvent &arg ) = 0;
virtual bool keyReleased( constKeyEvent &arg ) = 0;
};
哈哈,他正是定义了这两个接口,既然继承自他,当然也就拥有了这两个接口,于是GuiFrameListener成了合理合法的Listener了。
哦,它在哪里注册的呢?很简单.在GuiFrameListener的构造函数里我们看到:
{
mMouse ->setEventCallback(this);
mKeyboard->setEventCallback(this);
}
继续挖源码:
virtual void Keyboard : :setEventCallback ( KeyListener *keyListener ) {listener=keyListener;}
这下应该就明白了吧。很明显GUI Demo里使用了缓冲模式.
剩下来就是立即模式的数据了,他很好理解:
void Win32Keyboard::_read()
{
mKeyboard->GetDeviceState( sizeof(KeyBuffer), &KeyBuffer );
}
它把键盘当下的状态保存到缓冲中去,要想知道哪个键是否按下,只要对照缓冲“按图索骥”就可以了。
bool Win32Keyboard::isKeyDown( KeyCodekey )
{
return (KeyBuffer[key] & 0x80) != 0;
}
这个键按下了吗?那个键按下了吗?那那个呢?呵,真啰嗦,得一个个的问。
于是我们 GUI Demo 中可以看到下列的代码:
virtual bool processUnbufferedKeyInput(const FrameEvent& evt)
{
if(mKeyboard->isKeyDown(KC_A))
mTranslateVector.x = -mMoveScale; // Move camera left
if(mKeyboard->isKeyDown(KC_D))
mTranslateVector.x = mMoveScale; // Move camera RIGHT
if(mKeyboard->isKeyDown(KC_UP) || mKeyboard->isKeyDown(KC_W) )
mTranslateVector.z = -mMoveScale; // Move camera forward
if(mKeyboard->isKeyDown(KC_DOWN) || mKeyboard->isKeyDown(KC_S) )
mTranslateVector.z = mMoveScale; // Move camera backward
}
我们用键盘要不是缓冲模式,要么立即模式,不可能一起上,这所有在 demo 中都能看到, demo 的作者懒得多写代码 ( 嘿嘿 ) ,它继承了 ExampleFrameListener 的许多功能,而 ExampleFrameListener 也实现了些立即模式的按键处理。 Demo 没有用到。写了这么多了,还得 8 大步骤?? 该第 7 步了吧: ―― Act on the data , 这有什么好说的呢?爱咋咋地!最后一步,第 8 步――天龙八部 ! 呵错了,是 Close DirectInput 其实很简单, OIS 把他们封装在各个类的析构函数里了,想当然,源码也不用贴了 . 说了 OIS 如何把 DirectInput 封装起来,参考 8 大步骤,如何使用 OIS 也基本很明白了。OIS ,就学到这里,接下来该主角上场 CEGUI 得休息了,打字,翻资料,看代码,很累了。