导读:
DirectInput编程基础 - 简介 出 处: 中国游戏开发者
[ 2001-09-09] 作 者:
目 录
1.1 DirectInput概念
1.2 设置DirectInput
1.3 列举设备
1.4 设置设备
1.5 取得输入数据
绪言
输入相对于图形和声音而言从未成为游戏开发中的非常重要的论题。读取键盘按键、鼠标移动和游戏杆位置似乎并没有什么困难,但随着新输入设备对市场的强烈冲击以及DirectX的发行,这一问题变得日益重要了。
如果用户是DirectInput编程的新手,那么应先排除掉一些旧观念。应记住的重要一点是DirectInput的得名是因为它直接与设备驱动器通讯,对鼠标和键盘也就意味着立即响应硬件中断,而不是等待Windows发送消息证明已发生输入事件。当然,DirectInput放弃了Windows的消息队列,在虚构WM_KEYDOWN或WM_MOVSEMOVE等消息的同时也就不再享受Windows提供的一些服务。这种服务大都不是对游戏输入特别有用,但它有助于理解缺少这些服务时所产生输入信息的含义,后面有关鼠标和键盘输入的章节中,将深入地讨论这一问题。
当然,DirectInput放弃了Windows的消息队列,在虚构WM_KEYDOWN及WM_MOVSEMOVE等消息的同时也就不再享受Windows提供的一些服务。这种服务大都不是对游戏输入特别有用,但它有助于理解缺少这些服务时所产生输入信息的含义,后面有关鼠标和键盘输入的章节中,将深入地讨论这一问题。
1.1、DirectInput概念
下面看一下DirectInput所关心的内容:输入设备及其部件。
1.1.1 设备
DirectInput可识别三种基本设备类型:
键盘,标准系统键盘。
鼠标,这一类中包括所有类似于鼠标的设备,如触摸板、跟踪球以及相应的按键。
游戏杆,相对于其它类型的控制器和力反馈设备而言,这是易于产生误解的名称,其范围从简单的游戏杆直至虚拟现实的复杂设备。本书中,我们使用这一名称是为了与DirectInput术语保持一致,但应记住,当谈到“游戏杆”时,这一说法一般是“特殊游戏控制器”的简称。 只要讲到DirectInput,那么设备就是指由设备驱动器代表的螺丝、螺母集合, 带有内置跟踪球或触摸板的膝上键盘算作两个设备,但带有力反馈激励器的游戏杆却是一个设备。DirectInput API和文档中把返回数据或产生力反馈效果的设备的不同部分都称为设备对象实例或简称对象,这两种叫法都不是最佳的。因为“对象”一词(在其它情况下)指代码对象,“实例”批COM接口的例示。在本文中,我们尽量避免使用这种叫法,而用更准确的“设备物”取代。我们认为,设备物可以是任何的键、按钮、视点帽或轴等。
另一个更易混淆的问题是“设备”可以指物理对象(如其驱动器)也可以指DirectInput创建的DirectInputDevice对象。当讲到创建一个设备时,我们指获得一个指向IDirectInputDevice接口的指针,以便可以用相应方法与设备驱动器通讯。
1.1.2 按钮和轴
按钮是任何具有开和关状态的设备。DirectInput在概念上不对按钮加以区分,它们可以位于鼠标上、游戏杆上或游戏板上,还可以是键盘上的键。
轴是设备中的一种控制而不是按钮 — 有位置而不是开、关状态的事物。严格地说,轴不是物理对象,而是由物理对象向某一方向的移动产生的一个值。游戏杆、跟踪球、鼠标球和触摸板等一个物理对象控制两个轴的值,分别称为X轴和Y轴,因为它们一般控制的是二维计算机屏幕上的指针或游戏元素。游戏杆上的减速把手或滑块只控制一个轴,它与MicrosoftIntelliMouse鼠标中的转轮是一样的。游戏杆上也可能有其它的轴,如用于控制舵方向的扭转动作等,用于驾驶、飞行摸拟等的特殊控制器依靠操纵轮、轭、踏板等操纵多个轴。
视点帽是例外的一种情况,它不是有任何按钮或轴,后面有关游戏杆的章节中将详细讨论这一设备。应记住的重要一点是,轴与物理空间或其它事件之间不存在固定关系。
程序上下文中设备的Z轴(尽管与三维坐标系统中一个轴的名称相同)可能与空间的第三维毫无关系,而其作用却可能是控制翻页、音量或其它非空间属性。虽然已有一些成形的约定,如把X 轴与控制器的左右运动和屏幕上的水平方向相关联,但映射设备中的物理量与DirectInput中逻辑轴的关系是由设备驱动器实现的,而把这些轴与它们发挥作用的游戏概念关联起来是由程序设计者完成的 — 或给使用者以选择的机会并由他们完成。
轴可以是绝对的或相对的。游戏杆中的X轴和Y轴天生是绝对的,因为在任何方向它们都只能移动有限的距离,他们的位置也通常是以与某一固定中心点的距离而测量的。相反,鼠标天生是一种相对的设备,因为它可以在任何方向上无限地移动,而且也设有“原点”,对它所关心的只是在上一次检测之后它沿每个轴运行的距离。
DirectInput可以违反天性而把任何轴当作绝对的或相对的来处理。但多数情况下把鼠标(包括转轮)处理为绝对的轴,把其它设备 — 杆、滑块、拨号盘当作相对轴处理,也就是说,对鼠标要考虑的是其运动,而其它轴考虑的则是位置。
1.2、设置DirectInput
简单地调用DirectInputCreate函数(见表1-1)就可以初始化DirectInput系统,此函数将返回一个指向IDirectInput接口的指针,然后可以用此接口的方法列举设备并创建设备对象,这些设备对象是DirectInput的工作部分。
表1-1 DirectInputCreate函数
HRESULT WINAPI DirectInputCreate(
HINSTANCE hinst,
DWORD dwVersion,
LPDIRECTINPUT * lplpDirectInput,
LPUNKNOWN punkOuter
);
参数 说明
HINSTANCE hinst 创建DirectInput对象的程序或DLL的实例句柄
DWORD dwVersin 设计程序所用的DirectInput版本号,此值通常为DIRECTINPUT_VERSION,但看到的是文本
LPDIRECTINPUT *lplpDirectInput 新DirectInput对象的指针地址
LPUNKNOWN pUnkOuter 必须为NULL
注意:DirectX非常灵活地允许用户用以前的版本(实际即为版本3)进行设计,只要给DirectInputCreate函数传递一个非DIRECTINPUT_VERSION值就行了。如果传递的是OXO300,则程序在装有DirectX 3运行文件的系统中会很好地运行。但必须确保在所用结构与DirectX与定义的不同时,使用与DirectX3兼容的结构,如DIDEVCAPS_DX3。
简单有效的方法是在包含DINPUT.H文件前给出自己对DIRECTINPUT_VERSION的定义,如:现在不用担心使用旧结构了,因为DINPUT.H被解释过以后,所存的新结构都奇迹般地转换成对应的早期结构,其中DIDEVCAPS与DIDEVCAPS_DX3一致,如此类推。
提示:即使是用新版DirectInput进行设计,也可以使用旧版本中的结构,用途之一是IDirectInputDevice:: GetCapabilities方法,通过传递传来DIDEVCAPS_DX3结构将省去DirectInput查询力反馈驱动器的一步,可使速度略有提高。当然只有在对力反馈不感兴趣时才可这样做,同时不要忘记初始化dwSize成员为sizeof(DIDEVCAPS_DX3)。
1.3、列举设备
你一般不需要列举鼠标和键盘。即使与用户系统连接的这两种设备都不止一个(当然这种可能很小),我们也可以通过在IDirectInput::CreateDevice中使用预定义的变量GUID_SYSMOUSE 和GUID_SYSKEYBOARD来解决。
游戏杆则属于另一种情况,因为不存在所谓的“系统游戏杆”。一些用户会安装多种游戏控制器的驱动程序,甚至把多个这种设备连接到系统。至少,为了获得了一个合适的设备或多个设备的GUID,要对其进行列举,或许为了使用户能进行选择还需要列举所有可用的设备。后面有关游戏杆的章节中将详细讨论这一问题。
要列举设备需要先建立一个回调函数,然后调用EnumDevices方法,见表1-2。
表1-2 EnumDevices方法
HRESULT IDirectInput::EnumDevices(
DWORD dwDevType,
LPDIENUMCALLBACK lpCallback,
LPVOID pvRef,
DWORD dwFlags
);
参数 说明
DWORD dwDevType DIDEVTYPE_* 值,指定要查找的设备类型,如为0则查找所有类型
LPDIENUMCALLBACK lpCallback 回调函数地址
LPVOID pvRef 可为任何值,典型值为一指向数据的指针,可在回调函数中使用或修改。这是传给回调函数的第二个参数
DWORD dwFlags 对列举的更严格的限制标志。可以为(a)DIEDFL_ALLDEVICES(=0)或(b)DIEDFL_ATTACHEDONLY和DIEDFL_FORCEFEEDBACK之一或两者皆有
EnumDevices用到的回调函数可以有许多用途。被列举的特定设备的许多信息都在DIDVICEINSTANCE结构中,此结构由DirectInput自动建立并传给回调函数,下面是回调函数如何使用这些信息的几个例子:
1)创建此设备。此设备的实例GUID已位于DIDEVICEINSTANCE结构中,因此回调函数可以很方便地获取IDirectInputDevice接口并进行其它初始化工作。
2)检查设备的子类型,例如,可以检查键盘子类型并对游戏功能对应的键做必要的修改(但是,如果列举键盘没有别的重要目的,则可以通过调用IDirectInputDevice::GetDeviceInfo简单地实现)。下面的例子说明了如何在回调函数中更改对键功能的分配。这里假定调用EnumDevices时已限定了列举的是键盘。
BOOL CALLBACK DIEnumKbdProc( LPCDIDEVICEINSTANCE lpddi,
LPVOID pvRef )
{
switch ( GET_DIDEVICE_SUBTYPE( GET_DIDEVICE_SUBTYPE( lpddi->dwDevType ))
{
case DIDEVTYPEKEYBOARD_PCXT;
case DIDEVTYPEKEYBOARD_PCAT; // No F11 and F12 keys. so reassign here.
... break;
// And so on. ALL the subtypes are listed in the
// reference for the DIDEVICEINSTANCE structure.
... }
return DIENUM_CONTINUE;
} 3)检测设备性能。假设现在正在查找有至少8个按钮的游戏杆,对列举的每种游戏杆都可以创建一个临时的IDirectInputDevice接口并调用其GetCapabilities方法,所需的信息将由此方法填入DIDEVCAPS结构的dwButtons成员中。
4)产生一个列表框,用户可从中选择一种设备。
1.4、设置设备
准备用DirectInput使用设备有时有点象飞行前的检查清单,但不要把这当成琐事 — 应把它看成一种机会,一种避免错误发生,使DirectInput正常工作的机会。
1.4.1 创建设备
选定一种输入设备或由用户选出一种后,接下来可以为其获取一个接口。继续工作之前所需的一条基本信息是设备的实例GUID,如果没有在列举时从DirectInput传给回调函数的DIDEVICEINSTANCE结构的guidInstance成员中获取这一GUID,则需要使用预定义变量GUID_SysKeyboard或GUID_SysMouse。表1-3为 CreateDevice的方法。
表1-3 CreateDevice方法
HRESULT CreateDevice(
REFGUID rguid,
LPDIRECTINPUTDEVICE *lplpDirectInputDevice,
LPUNKNOWN pUnkOuter
);
参数 说明
REFGUID rguid 设备的实例GUID
LPDIRECTINPUTDEVICE *lplpDirectInputDevice 指向新IDirectInputDevice接口的指针地址
LPUNKNOWN pUnkOuter 必须为NULL
注意:GUID是按引用传递的,这也许是因为GUID_SysKeyboard和GUID_SysMouse是全局变量而不是宏。在C中,不允许按引用传递,因此只能传递GUID的指针。
获得IDirectInput Device接口后,通常应立即查询IDirectInputDevice2接口并代替IDirectInput接口。这一步对于力反馈编程非常重要,但即使不需要力反馈,也还可能想用IDirectInputDevice2::Poll方法,其原因稍后再解释。下面的程序先创建设备,然后为其获取IDirectInputDevice2接口。读者可以在其中加入自己的错误处理语句。
LPDIRECTINPUTDEVICE2 CreateDevice2( LPDIRECTINPUT lpdi, GUID* pguid )
{
HRESULT hr, hr2;
LPDIRECTINPUTDEVICE lpdid1; // Temporay.
LPDIRECTINPUTDEVICE2 lpdid2; // The keeper.
hr = lpdi->CreateDevice( *pguid, &lpdid1, NULL );
if ( SUCCEEDED( hr ) )
{
hr2 = lpdid1->QueryInterface( IID_IDirectInputDevice2,
( void ** )&lpdid2 );
lpdid1->Release();
}
else
{
OutputDebugString(
"Could not create IDirectInputDevice device" );
return NULL;
}
if ( FAILED( hr2 ) )
{
OutputDebugString(
"Could not create IDirectInputDevice2 device" );
return NULL;
}
return lpdid2;
}// CreateDevice2.
1.4.2 设置数据格式
从设备获取数据之前,必须先设置其数据格式。这只是一个在设备的状态改变后获取数据包并与已知结构进行匹配的问题。例如,从游戏杆得到的数据包显然和从鼠标那里得来的不一样:格式的大小不同,并且按照按钮和轴组合的不同有不同格式大小的值。表1-4中的SetDataFormat方法非常简单。
表1-4 SetDataFormat方法
HRESULT IDirectInputDevice::SetDataFormat(
LPCDIDATAFORMAT lpdf
);
参数 说明
LPCDIDATAFORMAT lpdf DIDATAFORMAT结构的地址
如果读者看一下DIDATAFORMAT的文档,会看到错综复杂的说明:其中给出了有关DIOBJECTDATAFORMAT结构的一个数组的信息,说明了设备上每一按钮的数据是如何安排的。
为什么要如此复杂呢?因为DirectInput为目前还未设计出的设备留有余地,这种设备返回的数据可能与惯用结构不符。设置自定义的数据格式是相当复杂的。但幸运的是, DirectInput开发者提供了四种预定义数据格式,可用于任何标准输入设备。它们都是全局变量,都为设备描述了一种数据结构。用户所要记住的是,当设置一个预定义的数据格式时,立即会有数据返回到表1-5中所示的标准结构中。
表1-5 预定义的全局数据格式变量
全局DIDAFORMAT 数据结构
c_dfDIMouse DIMOUSESTATE
c_dfDIKeyboard char[256]
c_dfDIJoystick(适用于多数游戏控制器) DIJOYSTATE
c_dfDIJoystick2(适用于标准游戏控制器) DIJOYSTATE2
设置游戏杆的数据很简单,见下面的例子:
// lpdid is the IDirectInputDevice interface.
lpdid->SetDataFormat( &c_dfDIJoystick );
注意:即使不打算调用IDirectInputDevice::GetDeviceState也要为设备设置数据格式,DirectInput对这些数据格式有其它用途。
1.4.3 获取设备信息
现在有必要对从不同设备和“设备对象实例”即设备物中提取信息的各种方法以及设备支持的力反馈效果做一概述,见表1-6。
表1-6 用于获取各种信息的DirectInput方法
方法· 父接口 目的
GetDeviceStatus IDirectInput 用户传来设备的实例GUID,如果该设备连接到系统上,则此方法返回DI_OK(注意该设备不一定非得创建为DirectInput设备)
EnumObjects IdirectInputDevice 列举设备中的按钮,轴和视点帽
GetCapabilities IdirectInputDevice 结构中,包括设备是否已连接,是否支持某种力反馈参数,以及按钮、轴、视点帽的数量
GetDeviceData IdirectInputDevice 取得源于设备的缓冲区数据
GetDeviceInfo IdirectInputDevice 把多种信息与写DIDEVICEINSTANCE结构中,包括设备的产品和实例GUID类型、昵称及标题等
GetDeviceState IdirectInputDevice 取得直接从设备而来的输入数据——即各按钮、轴、视点帽的当前状态
GetObjectInfo IdirectInputDevice 把特定按钮或轴的信息写入DIDEVICEOBJECTINFO结构中,包括类型、在数据格式中的位置、昵称、对输入数据某些更秘密条目的支持等
GetProperty IdirectInputDevice 取得设备属性 — 即可用SetProperty改变的行为,如怎样说明轴的数据等
EnumCreatedEffectObjects IdirectInputDevice2 列举已创建的力反馈效果
EnumEffects IdirectInputDevice2 列举设备支持的
GetEffectInfo IdirectInputDevice2 将所支持的一种力反馈效果的各方面信息写入DIEFFECTINFO结构中
GetforceFeedbackState IdirectInputDevice2 取得力反馈设备的信息,包括是否已加电,是否处于暂停状态等
(·)所有IDirectInputDevice方法对IDiretInputDevice2和接口同样有效。
列举设备物。创建设备后的第一步一般应该是找出有哪些可用的按钮,轴或视点帽。在示例程序DInput中,每当用户选定新设备后即进行这一操作,以使用户能选择发射键。表面上看EnumObjects方法是用于取得可用设备物清单的方法,但究竟是不是这样呢?先看一看表1-7中此方法的语法。
表1-7 EnumObjects方法
HRESULT IDirectInputDevice::EnumObjects(
LPDIENUMDEVICEOBJECTSCALLBACK lpCallback,
LPVOID pvRef,
DWORD dwFlags
);
参数 说明
LPDIENUMDEVICEOBJECTSCALLBACK lpCallback 回调函数地址
LPVOID pvRef 可为任何值,典型值为一指向数据的指针,可在回调函数中使用或修改。此值是传给回调函数的第二个参数
DWORD dwFlags DIDFT_* 标志的组合,以使列举局限于某种类型的设备,如可产生力反馈效果的按钮
下例的调用将列举所有按钮,包括键盘上的键:
g_1pdid2->EnumObjects(EnumDeviceObjectsProc,NULL,DIDFT_BUTTON) 一般列举方法中的第一个参数是指向回调函数的指针,每次DirectInput发现匹配设备时都将调回此函数。中间的参数是可任意使用的pvRef,一般用作指向数据的指针,且此数据需要在回调函数中使用或修改。
回调函数照例必须接收一种标准序列的参数并返回一个BOOL值(DIENUM_CONITINUE或DIENUM_STOP),但在其它方面则完全由开发者决定其功能,下面是一个回调函数的例子:
BOOL CALLBACK EnumDeviceObjectsProc( LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef )
{
int i;
i = SendDlgItemMessage( g_hOptionsWnd,
IDC_COMBO_BUTTONS,
CB_ADDSTRING,
0,
( LPARAM )( lpddoi->tszName ) );
SendDlgItemMessage( g_hOptionsWnd,
IDC_COMBO_BUTTONS,
CB_SETITEMDATA,
i,
( DWORD )lpddoi->dwType )
return DIENUM_CONTINUE; // = TRUE.
}
IDirectInput::EnumDevices调用回调函数后,DirectInput将其列举的特定设备物的信息写入到一个结构中。在上述情况下,此结构是DIDEVICEOBJECTINSTANCE,见表1-8。
表1-8 DIDEVICEOBJECTINSTANCE 结构
typedef struct DIDEVICEOBJECTINSTANCE {
DWORD dwSize;
GUID guidType;
DWORD dwOfs;
DWORD dwType;
DWORD dwFlags;
TCHAR tszName[MAX_PATH];
DWORD dwFFMaxForce;
DWORD dwFFForceResolution;
WORD wCollectionNumber;
WORD wDesignatorIndex;
WORD wUsagePage;
WORD wUsage;
DWORD dwDimension;
WORD wExponent;
WORD wReportId;
} DIDEVICEOBJECTINSTANCE, *LPDIDEVICEOBJECTINSTANCE;
typedef const DIDEVICEOBJECTINSTANCE *LPCDIDEVICEOBJECTINSTANCE;
成员 说明
DWORD dwSize 如果结构传给IDirectInputDevice::GetObjectInfo,则此值必须初始化为sizeof(DIDEVICEOBJECTINSTANCE)
GUID guidType 区分对象是键、按钮还是轴等
DWORD dwOfs 数据格式中的首选(不必是实际的)偏移量,对象数据可在此处取得。只有使用自定义数据格式的程序才需考虑这一点
DWORD dwType 更多有关对象类型和对象实例标识的信息。(参见本章后部的“标识设备物”)
DWORD dwFlags 对象的其它信息,主要用于处理对力反馈的支持
TCHAR tszName[MAX_PATH] 对象的呢称 — 如“Dial”
DWORD dwFFMaxForce 对象可输出的实际作用力,以牛顿为单位
DWORD dwFFForceResolution 输出作用力的分级间隔,以万分之几表示
WORD wCollectionNumber 保留以支持HID
WORD wDsignatorIndex 保留以支持HID
WORD wUsagePage 保留以支持HID
WORD wUsage 保留以支持HID
DWORD dwDimension 对像值所采用的尺寸单位
WORD wExponent 与尺寸相关的指数(如果已知的话)
WORD wExponent 保留
稍后将用到此结构,因为它很有用 — 但不是因为EnumObjets,为什么呢?回忆一下如何给设备设置数据格式。设置完成后DirectInput对这种设备的印象就被锁定住了。例如,如果把数据格式关联到DIJOYSTATE结构,则DirectInput将准备好从多达32个按钮,6条轴,2个滑块,4个视点帽获取数据。一般说来设备上拥有的对象远少于此,但它表示的是设备可以多拥有一些。
必须要理解实际设备与DirectInput对它的印象之间的差别。EnumObjects列举设备上的所有东西而不管DirectInput是否准备从其中获取数据。实际上,DirectInput甚至可能连此设备的数据格式都没有;即使有,也无法从数据结构中的某个特定区域识别出所列举的对象。DIDEVICEOBJECTINSTANCE中的dwOfs成员指明了DirectInput会首选地把数据放在什么地方(以避免直接对驱动器提供的原始数据包进行拼揍和排序),对标准数据格式而言就是其结束的位置,但并不能确保是这样的。
只有那些需要不停地创建自定义的数据格式的程序才会正规地调用EnumObjects。比如现在想列举真正在于数据结构中的游戏杆按钮,那么应该象下面这样进行:
void CountJoyButtons( HWND hwnd )
{
DIDEVICEOBJECTINSTANCE
DWORD i, X;
DWORD MaxButtons = 32; // if DIJOYSTATE is our format.
DWORD dwOfs;
didoi.dwSize = sizeof( didoi );
for( x = 0; x
{
dwOfs = DIJOFS_BUTTON( x );
if ( SUCCEEDED( g_lpdid2->GetObjectInfo( &didoi,
dwOfs,
DIPH_BYOFFSET ) ) )
{
i = SendDlgItemMessage ( hwnd, IDC_COMBO_BUTTONS,
CB_ADDSTRING, 0,
( LPARAM ) didoi.tszName );
SendDlgItemMessage ( hwnd, IDC_COMBO_BUTTONS,
CB_SETITEMDATA, i, dwOfs );
}
}
}// CountJoyButtons().
此函数使用DINPUT.H中的一个宏(根据合适的数据格式)循环检测可能存在的按钮并确定其偏移值(下一节将讲到偏移量标识问题)。然后调用IDirectInputDevice::GetObjectInfo,如果按钮存在将返回DI_OK,否则返回DIERR_OBJECTNOTFOUND。
如果按钮存在,DirectInput将返回相关信息并存入DIDEVICEOBJECTINSTANCE结构中 — 与EnumObjects填充的结构一样。接下来CountJoyButtons获取按钮的昵称并将其填入对话框的下拉列表框中,此对话框所属的窗口的句柄就是传给上面函数那个句柄。同时它还将偏移值作为列表项的相关数据而存储,用索引使得在列表中的列表项增加或存储后能方便地取得这一偏移值。此偏移值用于标识用户选定的按钮。
1.4.4 标识设备物
上一节讲过按钮或其它对象可以在调用GetObjectInfo时用设备数据结构中的偏移量来标识,这是标识设备物的两种方法之一,另一种方法是由实例号即ID标识。
由偏移量标识。给设备设定数据格式后,每一对象都关联到此设备数据格式的特定位置处,此位置的字节偏移量是一种方便地标识对应设备物的方法。
现在来考虑一种最简单的情况:键盘。每次读键盘状态时都会返回一个256字节的数组(称之为KeyArray)。每一键都在数组中有一个索引值,对应于从数组起点开始的偏移量,这些索引值在DINPUT.H中以DIK_* 的宏定义形式给出。如ESC键对应的字节数据在KeyArray[DIK_ESCAPE]中(第4章键盘输入中有DIK_*值的完整列表)。但DIK_ESCAPE也可以在另外的情况下标识ESC键,如现在要从源于键盘的缓冲区数据中获取一些事件的信息,这些事件各自与一单独的键有关。如果ESC键被按下,则数据包中的标识为DIK_ESCAPE(稍后再讨论有关获取数据的内容,上面只是为了说明如何用数据偏移量标识键)。
鼠标和游戏杆数据也类似,只不过数据格式更加复杂。对鼠标而言,数据格式是DIMOUSESTATE结构,包括三个LONG(对应于每一可能的轴)和四个字节(对应于每一可能的按钮)。对应于主按钮的数据偏移量是从DIMOUSESTATE结构起点开始的rgbButtons[0]的偏移量。要获得帮助可以查DINPUT.H中的宏。
鼠标偏移量。对游戏杆而言,根据设定的数据格式,偏移量既可以是相对于DIJOYSTATE结构起点也可以相对于DIJOYSTATE2结构起点,DINPUT.H中提供了从任一游戏杆数据结构中获取设备物偏移量的宏定义。
DIMOFS_BUTTON0
DIMOFS_BUTTON1
DIMOFS_BUTTON2
DIMOFS_BUTTON3
DIMOFS_X
DIMOFS_Y
DIMOFS_Z 游戏杆偏移量
DIJOFS_BUTTON0到DIJOFS_BUTTON31
DIJOFS_BUTTON(n)
DIJOFS_POV(n)
DIJOFS_RX
DIJOFS_RY
DIFOFS_RZ
DIJOFS_X
DIJOFS_Y
DIJOFS_Z
DIJOFS_SLIDER(n) 注意:游戏杆按钮既可以由常量标识也可以由索引标识。如按钮0可以对应DIJOFS_BUTTON0或DIJOFS_BUTTON(0)。与键盘的情况类似,鼠标或游戏杆上按钮或轴的数据偏移量用于把存于缓冲区的输入项关联到产生这些输入的设备物上,在获取或设置属性时也可以用这些偏移量把其中的一条轴单列出来。
由ID标识标识。设备物的另一种方法是通过实例号(或ID)进行,它只是分配给各设备物一个序列,但不是GUID。回忆一下前面讲过的应分清DirectInput对设备的印象与由驱动器表示的设备实际组成之间的差别。设备上的每一对象都有实例号,即使数据结构中设有此对象的位置也是这样,甚至在根本未给此设备设置数据结构时也是这样。当从DIDEVICEOBJECTINSTANCE结构中获取设备物的信息时,不论是通过列举还是通过调用IDirectInputDevice::GetObjectInfo方法进行,标识信息将返到dwType成员中。此DWORD实际包含有两条信息,低字节存储的是对象的类型(如轴或按钮),中间两个字节为实例号,可以用DIDFT_GETTYPE和DIDFT_GETINSTANCE来提取所需的信息。
偏移量与ID:哪个更好。多数情况下,选择偏移量还是实例号来识别设备物是无关紧要的。用诸如GetObjectInfo及SetProperty等方法时可以使用其中任一种,但必须要在适当位置设置DIPH_BYOFFSET或DIPH_BYID标志以使DirectInput知道选用的是哪一种数值。但要获取输入数据,则应用偏移量来标识设备物。在程序中使用偏移量系统是最简单的,只有在访问特定作用返馈设备上的只输出对象的属性时才绝对要使用ID号,因为只输出设备不能提供输入数据,因此也就没有数据偏移量。
1.4.5 设置和获取设备属性
设备属性可包括以下内容:
轴如何提供数据(相对或绝对)。
存入缓冲区的输入数据对应的缓冲区大小。
轴的物理范围和它提供的数据之间的关系(范围、饱和度及盲区)。
力反馈杆是否模拟自居中弹性。
力反馈设备的增益(类似于音量控制)。 另外,制造商可为特定设备定义其它属性。表1-9介绍了SetProperty及GetProperty属性。
表1-9 SetProperty和GepProperty属性
HRESULT GetProperty(
REFGUID rguidProp,
LPDIPROPHEADER pdiph
);
及
HRESULT GetProperty(
REFGUID rguidProp,
LPDIPROPHEADER pdiph
);
参数 说明
REFGUID rguidProp 要设置或获取属性的标识,可以为DINPUT.H中的DIPROP_* 预定义值,也可以是制造者提供的GUID
LPDIPROPHEADER pdiph 主数据结构中DIPROPHEADER结构的地址,通常是一个DIPROPDWORD或DIPROPRANGE。主结构中的其它成员含有要被设置的数据
和别处一样,DirecX灵活的有远见的天性使得它看起来比想象得要复杂得多。由于SetProperty可用于人们目前根本没有想到的设备,所以不能让它接受标准数据结构。实际上,它可以接受任何针对要访问的属性而预定义的结构。对标准属性,可以是DIPROPDWORD(任何需要一个DWORD的属性),或DIPROPRANGE(任何需要两个LONG的属性)。但制造商可以声明任何其它类型的结构,仅需以一个DIPROPHEADER结构(见表1-10)作为其数据的第一部分,DirectInput能知道主结构的大小并标识出要设置或获取其属性的对象。
表1-10 DIPROPHEADER结构
typedef struct DIPROPHEADER {
DWORD dwSize;
DWORD dwHeaderSize;
DWORD dwObj;
DWORD dwHow;
} DIPROPHEADER, *LPDIPROPHEADER;
typedef const DIPROPHEADER *LPCDIPROPHEADER;
成员 说明
DWORD dwSize 必须初始化为外围结构的大小,如sizeof(DIPROPDWORD)
DWORD dwHeaderSize 必须初始化为sizeof(DIPROPHEADER)
DWORD dwObj 属性被访问的设备物的标识,如果包含整个设备则为O(对设定缓冲区大小而言)
DWORD dwHow 如果包含整个设备则为DIPH_DEVIC,否则为DIPH_BYOFFSET或DIPH_BYID,说明采用哪种系统标识设备物
表1-11与1-12为经常与SetProperty和GetProperty一同使用的两个标准外围结构。DIPROPDWORD用于设置或获取任何需要一个DWORD数据的属性;DIPROPRANGE用于设置或获取绝对轴的范围,也可以用于访问任何使用两个LONG数据的属性。关于轴的范围将在第4章游戏杆输入中详细讨论。
表1-11 DIPROPDWORD结构
typedef struct DIPROPDWORD {
DIPROPHEADER diph;
DWORD dwData;
} DIPROPDWORD, *LPDIPROPDWORD;
typedef const DIPROPDWORD *LPCDIPROPDWORD;
成员 说明
DIPROPHEADER diph 前面进过的结构头
DWORD dwData 数据 — 由人设置并通过SetProperty传给设备,或由Getproperty填充。典型的数据项为缓冲区大小和轴模式等,数据的意义由方法的rguidProp参数决定
表1-12 DIPROPRANGE结构
typedef struct DIPROPRANGE {
DIPROPHEADER diph;
LONG lMin; LONG lMax;
} DIPROPRANGE, *LPDIPROPRANGE;
typedef const DIPROPRANGE *LPCDIPROPRANGE;
成员 说明
DIPROPHEADER diph 结构头
LONG lMin 最小范围值
LONG lMax 最大范围值
下面是用SetProperty设置X轴范围的一个例子:
#define JOYMIN -1000
#define JOYMAX 1000
DIPROPRANGE diprg;
diprg.diph.dwSize = sizeof( diprg );
diprg.diph.dwHeaderSize = sizeof( diprg.diph )
diprg.diph.dwObj = DIJOFS_X
diprg.diph.dwHow = DIPH_BYOFFSET;
diprg.lMin = JOYMIN;
diprg.lMax = JOYMAX;
if ( FAILED( g_lpdid2->SetProperty( DIPROP_RANG, &diprg.diph ) ) )
retrurn FALSE;
接下来要获取设备的缓冲区大小,这里DIPROPDWORD结构在声明中初始化,但其思想与上一例基本一样。
DIPROPDWORD dipdw =
{ // The header, which we initialize.
{
sizeof( DIPROPDWORD ), // diph.dwSize sizeof( DIPROPHEADER ), // diph.dwHeaderSize 0, // diph.dwObj - no particular object DIPH_DEVICE, // diph.dwHow - entire device };
// The data will go here.
0 // dwData
};
HRESULT hr = g_lpdid2->GetProperty( DIPROP_BUFFERSIZE, &dipdw.diph );
1.5.6 协作级别
象每次使用结构时都要填充dwSize成员一样,设置协作级别也是一种令人费解的工作 — 为什么非得由用户做这项工作?难道DirectInput不能做得更好吗?事实上,DirectInput非常希望把事情做得更好,但有时也需要一些帮助。设置设备的协作级别旨在让DirectInput知道用户所希望的应用程序与系统以及其它程序之间相互联系时有什么样的表现。在深入讲解这一问题之前,先介绍一下程序调用IDirectInputDevice::SetCooperativeLevel时能使用的有效标志组合,见表1-13。
表1-13 IDirectInputDevice::SetCooperativeLevel使用的标志组合
标志 含义 适用于
DISCL_NONEXCLUSIVE|DISCL_ABCKGROUND 其他人可以以独占或非独占模式获得设备;当前程序可以在任何时刻访问数据 除力反馈外的所有设备
DISCL_NONEXCLUSIVE|DISCL_FOREGROUND 其他人可以以独占或非独占模式获得设备;当前程序只有在位于前台时才能访问数据 除力反馈外的所有设备
DISCL_EXCLUSIVE|DISCL_BACKGROUND 其他人可以以非独占模式获得设备;当前程序可以在任何时刻访问数据游戏杆和力反馈设备
DISCL_EXCLUSIVE|DISC_FOREGROUND 其他人可以以非独占模式获得设备;当前程序只有在位于前台时才能访问数据除键盘外的所有设备,对鼠标有效但禁止Windows显示光标
需要指出的第一点是,同一时刻不能有两个程序或一个程序的两个实例以独占模式获得同一设备。这主要是出于安全考虑,它可以防止输入给某一程序的值被传给同时运行的另一程序。对力反馈设备而言此时只能使用独占模式,上述这种特点可以防止两个程序同时输出作用效果,而如果同时输出则将产生混淆和警告。独占模式针对程序与系统间的相互作用不同而有多个分支:
由于Windows要求在任何时刻以同于独占的模式访问键盘,因此程序只能以非独占模式获得键盘。
由于Windows要求在任何时刻以等同于独占的模式使用鼠标,所以当程序以独占模式获得鼠标时,Windows将不能访问鼠标。这就意味着不会再产生任何鼠标消息,光标也将消失。注意这种表现与键盘不同,对键盘而言Windows有优先特权,所以重要的按钮组合如ALT_TAB及CTRL_ALT_DELETE总是有效。 如果考虑其表现则没有太多的理由优先选用独占模式。对鼠标而言选择独占模式为这样可有一点点好处,因以使Windows不再有产生鼠标消息的必要。但对游戏杆而言,选择哪种模式对表现没有影响。以前台模式获得设备意味着只有程序的主窗口位于前台时才能接收输入,对游戏和多数其它程序而言这种设置是自然的。后台模式则意味着只要程序在运行就可以接收输入,哪怕它已被最小化。想象一下一个“Smarthouse”程序,人们可以单击一个远程控制来打开车库门或开始烧烤食物,这种程序可以一直停留在Windows任务栏上,而不必被带到前台进行工作,它是一个很好的后台模式的例子。
另一种使用后台模式的情况是在当一对话框处于打开状态同时要保持获得设备时。因为当属性页位于前台时,用户需要体察其效果,所以设备只能以后台模式获得。如果已设置了前台协作级别,则只要程序转为后台,它将不能访问所用的设备。对鼠标而言,当用户打开菜单或对话框,甚至程序静止在前台时都将失去访问权。这种情况只能准备在设备重新有效时再次获得。下面开始讲获得设备。
1.4.7 获得设备
到这里为止DirectInput设置已包含了简单地准备设备待用的一劳永逸的步骤,但在能得到数据前还必须获得该设备。获得设备即是取得它的使用权并通知DirectInput说明程序想按设定的格式接收数据。但获得设备不是一劳永逸的步骤,由于种种原因,程序可能多次失去对设备的获取,每当这种情况发生时,程序需要重新获得所需设备,但DirectInput不会代劳。
假设DirectInput是一家旅游圣地豪华馆餐厅的服务生,它知道顾客喜欢坐在什么地方,是否愿意和他人用同一张餐桌,喜爱什么样的鲜花摆设等。但并不是任何时候顾客都能径直走入餐厅并坐到自己喜欢的座位上,因为有可能另一个团体已包用了他的餐桌,或者别人刚用完离去后,餐桌未来得及收拾和按喜欢的方式设置。因此,顾客应首先找服务生登记使各种安排都准备妥当。
找服务生登记并获准坐下和获得设备是一样的。每次获得设备时,DirectInput必须把各方面设成用户指定的方式,即清除所有缓冲区内的数据,以最有效的方式为数据传输作安排,同时给出用户选择的那个设备的属性。离开餐厅等同于调用IDirectInputDevice::Unacquire。其目的是让DirectInput知道其它程序或Windows系统现在可以以任何级别访问该设备了。
在结束程序前有时必须有意地释放设备,最常见的情况是当鼠标被以独占模式获得时Windows系统将不能使用鼠标,这在前面已讲过。这时如果想让Windows接管鼠标的使用权进行菜单选择,就必须调用Unacquire以使Windows做它应作的事;当菜单关闭后再重新获得鼠标。DirectX SDK中的示例程序Scrawl提供了这种用法的例子,当用户右击鼠标时Scrawl释放鼠标并打开一个上下文菜单,用户可以在标准的Windows鼠标光标下进行选择。
如果想改变除力反馈增益以外的设备属性则也需要先释放该设备。前面已提到过,程序有时会不自觉地释放设备,发生这种情况通常是由于程序以前台模式获得设备,但使用者已切换到另一窗口,这就好比那位假想的Maitre d’被贿赂或受到威胁而离开了用户的餐桌,当然可能这个类比不很恰当。
无论释放设备是有意的还是不自觉的,在想重新获取数据之前都必须通知DirectInput,这可由调用IDirectInputDevice::Acquire实现。但并没有简单快速的方法来检验设备是否处于未获取状态,所能作的只是检验申请接收数据时的回应是什么,IDirectInputDevice::GetDeviceState和IDirectInputDevice::GetDeviceData 方法在数据流被打断后的第一次调用时会返回DIERR_INPUTLOST。如果没有重新获得设备,接下来调用时会返回DIERR_NOTACQUIRED。收到DIERR_INPUTLOST后可以试着重新获得该设备并再取一次数据,见下面的代码段:
getstate:
hr = g_pMouse->GetDeviceState( sizeof( DIMOUSESTATE ), &dims );
if ( hr == DIERR_INPUTLOST )
{
hr = g_pMouse->Acquire();
if ( SUCCEEDED( hr ) ) goto getstate;
}
但是,想以同样的方法响应DIERR_NOTAQUIERD或其它失败消息值是不可取的。假设程序以前台协作级别使用一种设备,当使用者切换到另一程序的窗口时,此程序就会在每次想取得数据时收到DIERR_NOTACQUIRED消息,这时任何获取该设备的尝试都是对CPU周期的浪费,而且还有产生死循环的危险。这时应响应WM_ACTIVATE消息来重新获取该设备,因为此消息表明程序又切换到前台了。
对输入设备的访问进行管理确实很有好处,但这需要在适当的时候进行,当假想的人走近并开枪射击时,如果用户不能疯狂地按鼠标或拍打游戏杆,那么一切就都没有意义了。所以应总保持安全,试图重新获取已获得的设备不会有什么害处;对Acquire的多余调用不会有效果。
1.5、取得输入数据
在讲如何真正从设备获取数据这个非常重要的问题之前,先来回顾一下在此之前应进行的步骤。
创建设备。如果支持的设备不是鼠标或键盘,则还有需要得到一个IDirectInputDevice接口,因为至少会用到Poll方法。
设置协作级别。
设置数据格式。
设置设备属性。对游戏杆而言这一步是必需的,因为不设置轴的范围就不能继续进行后面的过程。如果想用缓冲区数据,则也需要设置其属性,因为必须把缓冲区大小从0改为某个值。
获得设备。与前面几步不同,这一步不是一劳永逸的。 1.5.1 两类数据
如果读者使用了旧风格的Windows输入编程方法就需要为诸如WM_KEYDOWN、WM_MOUSEMOVE等消息写处理程序。这些消息由Windows产生以响应设备中单个发生的事件,如键或按钮被按下或松开或者鼠标有了足够的移动而产生了一个中断等。
尽管DirectInput不使用Windows消息,但它采用了类似的取得输入信息的系统,这个系统称为缓冲区数据。当设备的缓冲区大小属性被设为大于0的值以后,这一系统就开始运作。每当有输入事件发生时,都会有一个数据包被创建,但这些数据包被放在一个私有缓冲区中,而不是转换成消息的格式。在任何时刻都可以用IDirectInputDevice::GetDeviceData方法从那些缓冲区中取出数据包。
读者或许也已用过类似于joyGetPosEx和GetAsyncKeyState函数,它们返回的不是离散的事件,而是整个设备或单个键的当前状态的“快照”。在DirectInput中这被称为立即数据,它存在于数据包中,数据包的格式即是为设备设置的格式,调用IDirectInputDevice::GetDeviceState方法可以取得这些数据。
读者可以在程序中任意选择使用上述类型的数据,我们的示例程序中使用立即数据来检测箭头键或游戏杆轴的状态,用缓冲区数据搜寻对发射键的敲击。鼠标的移动也使用了缓冲区数据,因为我们程序中的光标响应的是鼠标的移动而不是其位置。
从缓冲区中清除一个条目并不会改变GetDeviceState返回数据,因为它返回的总是设备的当前真实状态。就这一点而言它与标准的WindowsGetKeyboardState函数不同,后者实际上只是跟踪被处理的键盘消息。第4、5、6 章有关鼠标、游戏杆和键盘输入将详细说明立即数据和缓冲区数据。
1.5.2 事件通知
DirectInput提供一种机制以通知程序设备上是否发生某事或程序是否失去了设备,用Win32 CreatEvent函数创建一个事件,然后通过把其句柄传递给IDirectInputDevice::SetEventNotification方法(见表1-14)将其关联到某个设备。
表1-14 SetEventNotification方法
HRESULT SetEventNotification(
HANDLE hEvent
);
参数 说明
CreateEvent 返回的事件句柄,当设备状态改变时此事件将被设置。想关闭某设备的事件通知可通过传NULL实现
事件通知对于实时游戏并不十分有用,这时一般采用的方法是在每次循环时检测设备状态或输入数据缓冲区的内容,也可以同时对这两项进行检测。从另一方面说,程序在鼠标或键盘输入之前不做任何有关的事可以更有效地使用处理器,使之等待事件发生而不是总在轮询各个设备(见DXSDK中的Srawl示例程序)。但对于其它设备,则不得不采用轮询的方法,原因稍后再说明,因此检测信号并不比直接查询设备状态或缓冲区好多少。如果确实发现了使用事件通知的场合,那么在SDK参考中有大量类似于Scrawl中IDirectInputDevice::SetEventNotification的示例代码可以使用。
注意:本章和后续章节中,我们经常使用“事件”一词来说明设备状态或缓冲区里数据项发生改变的事情,如按压按钮或轴的变化等,一般它不表示本章中我们讲到的事件对象,在使用狭义时我们将尽量表达清楚。
1.5.3 轮询以获取数据
当设备状态改变时DirectInput是如何知道的呢?某些情况下,它通过旧风格的方法即硬件中断实现,否则就必须查询驱动器以获得当前设备状态并用一些小把戏来模拟事件(广义和狭义)。所有这些都在幕后静静地运行,但是首先需要做一件事以使轮询操作发生。DirectInput不会自动轮询设备,而只在调用IDirectInputDevice::Poll方法时才这么做。该方法没有任何参数。
调用Poll后会发生什么呢?DirectInput先检查设备是否确实需要轮询。如果不需要 — 即如果DirectInput通过硬件中断获知状态变化 — 则Poll方法立即返回。否则DirectInput检查设备的状态并取得立即数据。如果已为缓冲区数据留出了空间,则DirectInput会把此次设备的状态与上次调用时的状态相比较,并根据发现的变化创建数据包,然后它将数据包存入缓冲区中,就好象这些数据是从中断得到的一样,接下来会针对已创建的事件发出信号。
可以通过检测DIDEVCAPS结构的DIDC_POLLEDDATAFORMAT标志来验证是否设备上的某部分需要轮询。也可以通过检测DIDEVICEOBJECTINSTANCE中的DIDOI_POLLED以获知是否设备上的特定设备物需要轮询。但是更安全经济的做法是不管有无必要都调用Poll,如果设备不需要轮询,则该方法几乎立即就返回。
总之,在每次IDirectInputDevice::GetDeviceState或IdirectInputDevice2::GetDeviceData获取数据之前最好都调用IdirectInputDevice2::Poll,除非确知程序只用到中断驱动的设备。
提示:如果程序在消息循环中寻找有标志的输入事件,则可以调用WaitForSingleObject、WaitForMultipleObjects或其它的一种检测事件的函数,并把超时值(dwMilliseconds)设为0,然后如果该函数返回后未找到事件对象,就调用Poll。在SDKIDirectInputDevice::SetEventNotification参考的最后一个例子中,就可以在DoGame函数内调用Poll。