原文地址:
http://www.microsoft.com/china/MSDN/library/archives/technic/develop/tsother/0314c.aspJason Clark 不知不觉中,Windows下的游戏和多媒体程序已经开始流行。硬件变得越来越快,Windows也变得更加灵活。自从Microsoft发布了DirectX,游戏开发人员对其它平台已经越来越不感兴趣了。许多游戏开发者也已经将他们的开发工作完全移植到了Windows下。
为PC开发游戏从来就没有轻松过。从无数种显示卡和声卡中,开发者学会了在功能性和兼容性之间平衡的艺术。他们不得不处理象页面切换、段内存结构和位操作这样令人讨厌的问题。并且随着多人游戏的流行,开发者必须同时处理象网络和通信等事项。DirectX引入后,游戏开发者变得轻松了。通过为开发者提供的DirectX对象,绝大多数讨厌的工作已经被简化了。
基于DirectX的程序是普通的Windows程序吗?必须懂得COM吗?为简单的程序值得使用DirectX吗?必须使用DirectX的全部组件吗?这样的问题肯定还有更多。
本文将首先介绍DirectX,然后介绍DirectX的一个组件DirectInput的使用。演示程序说明了DirectInput的用法,着重介绍了其强大的反馈功能。
DirectX揭密
DirectX是一套为Windows程序提供对系统硬件更亲密控制的组件。(表1列出了DirectX 5.0的组件及其作用)。那么,亲密控制是什么意思呢?
表1:DirectX 5.0的组件
组件 | 用途 |
DirectDraw | 高速2D图象 |
DirectSound | 短响应时间声音输出 |
Direct3D | 高速3D图象 |
DirectInput | 面向游戏的对游戏杆和其它输入设备的访问 |
DirectSetup | 方便的安装DirectX组件 |
DirectPlay | 面向游戏的通信和网络支持 |
DirectShow | 视频流支持 |
DirectAnimation | 动画录放支持 |
DirectX提供的硬件控制常常被描述成底层控制,这会使人联想起位操作和其它讨厌的事情。实际上,DirectX组件包含许多高层API,使得象复制位图和播放声音等复杂的工作变得相当简单。用“为程序提供比过去更好的对硬件的控制”来形容DirectX更准确。这在Windows中是一个显著的特性,因为在Windows中,资源是共享的,并由操作系统控制。
DirectX组件遵守称为COM的二进制对象的工业标准。
开始DirectX
下面从DirectX的安装开始讲起。大多数情况下,某个好玩的游戏就会为系统安装DirectX。为得到最新的版本,应该从最新的Microsoft Platform SDK中将DirectX安装到系统中。可以在http://www.microsoft.com/msdn站点或者MSDN光盘中找到platform SDK。缺省情况下,Microsoft Platform SDK被安装到缺省驱动器根目录下的\MSSDK目录中。DirectX的头文件安装在\MSSDK\INCLUDE目录中,Lib文件安装在\MSSDK\LIB目录中。
Platform SDK包含了一些非常好的DirectX例子和文档。早期发布的DirectX文档非常粗略而且有些是错误的,现在的版本已经极大地改正了这一问题。最好要熟悉这些文档。
现在已经为安装利用DirectX的程序做好了准备。所幸的是,不必一次就处理DirectX的全部功能。DirectX是一套可以分别使用的组件。实际上,在编程概念中,DirectX的不同部分互相没有联系。它们仅仅是具有相同的设计风格和目标:使Windows的游戏编程变得容易。
使用DirectX组件的程序有什么特殊的地方吗?根本没有。使用DirectX组件的程序是基于Win32的程序,它们使用普通Win32 API集,并且可以访问所有可以获得的操作系统工具。实际上,DirectX既可以用于GUI程序,也可以用于控制台程序。可以直接用Petzold-style SDK编程开发程序,也可以用基本类库,如MFC。总的说,唯一的要求是大多数DirectX组件在程序中需要HWND,所以至少要有一个窗口。
虽然DirectX组件是分离的,但是每个组件的实现风格和使用都是相同的。DirectInput是学习DirectX的非常好的出发点,原因是DirectInput是最简单的组件之一。
用力
以后在游戏中要“用力”,这是电影《星球大战》中的说法,因为DirectInput中加入了相当令人陶醉的力反馈支持。DirectX 5.0以前,DirectInput支持从鼠标和键盘读取输入,这是一个有用但却令人厌烦的特性。DirectX 5.0中,DirectInput被扩充到支持具有以物理力的形式向用户传播反馈的能力的设备。
如果不能立即理解上面的内容,下面就用一个游戏进行解释。假设你刚启动了你最喜欢的超现实3D越野赛车游戏,正手握力反馈游戏杆。在起跑线上,你可以听到赛车引擎的空转声,同时也能够通过游戏杆感觉到赛车引擎的空转!比赛开始后,你可以感觉到引擎高速旋转的嗡嗡震动。当行驶到赛程中崎岖的地段时,你将会不停的感觉到电子碰撞。赛车在整个赛场上撞来撞去,你的游戏杆也会如此。赛车车轮卡在车辙中导致赛车被拉向左边,游戏杆也会被拉向左边!整个过程中你可以感觉到每次颠簸、刮擦、撞击和撞毁。
现在,带有支持DirectInput的Windows驱动程序的唯一的力反馈设备是Microsoft的SideWinder Force Feedback Pro。这一现状不会持续太久,新设备以及现有设备的新驱动程序很快就会进入市场。
剖析DirectInput
DirectInput由三个对象组成:DirectInput, DirectInputDevice, 和DirectInputEffect (见表2)。DirectInput是一个高层的对象,通过DirectInput对象可以对相关的输入设备进行基本的初始化和查找。DirectInput对象最终用来创建低层的DirectInputDevice对象。DirectX中的每个主要组件都采用相同的方法,首先创建高层对象,如DirectInput或DirectSound对象,然后创建低层对象与硬件进行实际的通信。
表2: DirectInput对象
对象 | 说明 |
DirectInput | 封装高层DirectInput功能,列举设备并用来创建DirectInputDevice对象。 |
DirectInputDevice | 与物理输入设备的接口,例如游戏杆,包括收集和设置设备状态信息的接口,并且用来创建DirectInputEffect对象 (对于力反馈设备)。 |
DirectInputEffect | 封装能够在力反馈设备上“播放”的简单效果,提供启动、停止和设置力反馈效果等功能。 |
DirectInput对象是三个对象中最容易理解的。实际上,它在一个接口形式IDirectInput (见表3)中只提供五个函数。这是DirectInput的一个非常重要的部分,因为这是出发点。
表3:IdirectInput接口
成员函数 | 说明 |
CreateDevice | 创建一个DirectInputDevice对象并返回一个指向其IdirectInputDevice接口的指针。 |
EnumDevices | 为找到的与给定标准匹配的每个设备调用一个回调函数,每个回调函数提供一个GUID,可以用在CreateDevice中创建DirectInputDevice对象。 |
GetDeviceStatus | 测试物理设备是否连接到系统。 |
Initialize | 如果DirectInput对象是使用CoCreateInstance创建的,那么在使用前必须调用Initialize成员。如果DirectInput对象是使用DirectInputCreate创建的,那么就已经初始化过了。 |
RunControlPanel | 为设备运行Windows Control Panel程序,让用户安装新设备或者更改已有设备的配置。游戏杆校准可以在此处做。 |
创建DirectInput对象
为了创建DirectInput对象并得到其IdirectInput接口指针,应该在程序初始化阶段使用两种方法之一完成。
第一种方法相当简单。DirectX提供了一个助手函数DirectInputCreate来创建并初始化DirectInput对象。它与所有DirectInput的函数、接口和宏定义都在头文件DINPUT.H中声明。实际的函数体在DINPUT.LIB文件中。
DirectInputCreate如下定义:
HRESULT WINAPI DirectInputCreate(
HINSTANCE hinst,
DWORD dwVersion,
LPDIRECTINPUT * lplpDirectInput,
LPUNKNOWN punkOuter
);
第一个参数是应用程序的实例。第二个参数是程序需要的DirectInput版本,通常使用DIRECTINPUT_VERSION宏,定义为当前版本。第三个参数最重要,如果对COM非常陌生的化就很难理解,它是指向IdirectInput接口的指针的地址。程序中应该定义一个LPDIRECTINPUT类型的变量(可以是全局的)并将其地址作为第三个参数传递给DirectInputCreate。
最后一个参数叫作punkOuter,与COM技术中的聚合有关,可以用NULL安全的忽略。返回值是一个HRESULT,是COM的标准返回类型,可以将返回值与可能的返回值比较,也可以使用COM宏定义SUCCESS或FAILED来检查。
使用DirectInputCreate能够容易地创建高层对象并得到其主接口指针。这是DirectX的又一个设计方法,每个DirectX组件都提供助手函数来创建高层对象,例如DirectInputCreate或DirectDrawCreate。在程序中可以用这些助手函数创建DirectX对象,然而,这些函数实际上创建的是COM对象。这个工作也可以用叫作CoCreateInstance的标准Win32 API函数来完成。这就引出了创建DirectInput对象的第二中方法。
在Win32中用CoCreateInstance创建COM对象非常普遍。如果程序中已经使用CoCreateInstance创建了其他COM对象,开发者可能就会希望也用它来创建DirectX对象。因为COM对象在安装时就在系统中注册过,所以唯一需要知道的就是对象的GUID,用它来创建一个实例。创建DirectX对象需要的全部GUID都在头文件中声明,并在库文件DXGUID.LIB中定义。可以将一个预定义的GUID传递给CoCreateInstance,让Windows为你创建对象。
CoCreateInstance定义如下:
STDAPI CoCreateInstance(
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID * ppv
);
第一个参数是要创建对象的GUID,DirectX定义的GUID是叫作CLSID_DirectInput的GUID结构变量。第二个参数是熟悉的pUnkOuter,同样可以用NULL忽略。第三个参数dwClsContext定义COM对象在何处创建,DirectX只支持进程内服务器,所以必须使用CLSCTX_INPROC_SERVER。
第四个参数是两种方法真正的不同之处。记住COM对象对外提供接口,与对象本身一样,接口也用GUID识别。使用第一种方法,不能选择得到的接口,总是得到IdirectInput。使用CoCreateInstance可以请求对象所支持的任何接口,方法是使用为接口预定义的GUID。但是在DirectInput这是没有意义的,因为DirectInput对象的唯一有用的接口就是IdirectInput。其它DirectX组件支持多个有用的接口。(例如,DirectDraw对象可以用IdirectDraw或IDirectDraw2接口操作。)
最后一个参数是程序中接口指针变量的实际地址。
现在就拥有了对象和对象的一个接口。CoCreateInstance方法还需要另外一步:必须要首先调用一个接口函数初始化对象。DirectInputCreate提供的是一个已经初始化过的DirectInput对象,但CoCreateInstance没有特定于DirectInput的认识,因此必须调用IdirectInput接口的初始化成员函数。假设如下定义IdirectInput接口指针变量:
LPDIRECTINPUT g_lpDI
可以如下调用初始化函数:
g_lpDI->Initialize( hInstance, DIRECTINPUT_VERSION);
既然选择采取这种标准方法创建对象,就不得不注意COM需要的其他标准,例如需要调用CoInitialize和CoUninitialize。
使用DirectInput对象
一旦拥有了DirectInput对象,就可以用它来创建DirectInputDevice对象,来管理系统中特定的设备。创建DirectInputDevice对象要使用CreateDevice函数,它是作为IdirectInput接口一部分的五个函数之一。CreateDevice需要所请求设备的GUID,返回新DirectInputDevice对象的IdirectInputDevice接口指针。
HRESULT CreateDevice(
REFGUID rguid,
LPDIRECTINPUTDEVICE *lplpDirectInputDevice,
LPUNKNOWN pUnkOuter
);
这些内容看起来很熟悉,因为它与CoCreateInstance和DirectInputCreate类似。但是,现在还没有完全准备好开始DirectInputDevice对象,原因是在创建DirectInputDevice对象前需要该设备的GUID。
DirectInput库为创建DirectInputDevice对象预定义了两个GUID:GUID_SysKeyboard和GUID_SysMouse。将两者之一直接传递给CreateDevice函数,就会得到相应设备的DirectInputDevice对象。
注意,令人感到奇怪的是缺少对游戏杆的预定义GUID。在Windows中,通常都有系统键盘和系统鼠标,另一方面,系统本身并不使用游戏杆。可以安装一个或者多个游戏杆,但系统管理的范围只限于驱动程序级。系统并为这些设备指定特殊的系统状态,也不会在日常事务中使用这些设备。因此,为游戏杆定义GUID对DirectInput来说是不合理的。
那么,如何才能找到与系统连接的游戏杆的GUID呢?要得到它们,必须要列举设备。列举系统设备和性能在DirectX中相当普遍。要列举系统中的输入设备,需要使用EnumDevices函数。EnumDevices是IdirectInput接口的一部分,如下定义:
HRESULT EnumDevices(
DWORD dwDevType,
LPDIENUMCALLBACK lpCallback,
LPVOID pvRef,
DWORD dwFlags
);
注意此函数与Windows中其它列举API相同,例如EnumWindows。第二个参数是一个回调函数。第三个参数是程序中定义的32位值。第一个参数是想要列举的设备类型,对游戏杆来说,是DIDEVTYPE_JOYSTICK(全部的设备类型列在表4中)。最后一个参数是详细描述想要列举的设备的标志。现在支持的标志是DIEDFL_ATTACHEDONLY和DIEDFL_ALLDEVICES(这两个标志是互斥独占的),此外还有DIEDFL_FORCEFEEDBACK,此标志表示力反馈设备,能够和另两个标志位或操作。
图4:定义列举的输入设备
以下定义的值可以传递给EnumDevices来选择列举哪种类型的输入设备。另外也支持子类型,见SDK中DIDEVICEINSTANCE结构的文档。
值 | 说明 |
DIDEVTYPE_MOUSE | 列举鼠标设备 (标准、轨迹球等) |
DIDEVTYPE_KEYBOARD | 列举键盘设备 (标准、键区等) |
DIDEVTYPE_JOYSTICK | 列举游戏杆设备 (操纵杆、操纵轮、方向舵等) |
DIDEVTYPE_DEVICE | 列举其它设备 |
当EnumDevices列举系统中的输入设备时,反复地调用回调函数。回调函数定义如下:
BOOL CALLBACK EnumProc(LPCDIDEVICEINSTANCE lpddi,LPVOID pvRef) ;
因为回调函数是由用户程序定义并传递给EnumDevices的,所以是调用CreateDevice的最合适地方,直到创建了满足需要的足够DirectInputDevice对象为止。但是回调函数并非一定要如此实现,可以简单的将列举设备的所有GUID保存在一个表中,在以后的代码中使用。
回调函数接受两个参数。第二个参数是程序定义的传递给EnumDevices的32位值。更重要的是,第一个参数传递指向一个结构的指针,该结构包含关于能够与列举标准匹配的单个设备的许多信息。这是一个DIDEVICEINSTANCE结构。此结构中最重要的一条信息是设备的GUID,保存在结构的guidInstance成员中。
当程序中完全完成DirectInput有关的工作后,就应该调用IdirectInput接口的Release成员。这就告诉DirectInput对象可以释放自己了。在DirectX中,最好养成释放对象的习惯,从低层对象开始,到高层对象结束。正常情况下程序会作为清除或者关闭的例行公事的一部分调用Release。这是使用每个DirectX组件的必要步骤,也是使用每个COM组件的必要步骤。
现在已经用CreateDevice成员函数获得了DirectInputDevice对象的一个接口,为开始处理与系统连接的实际物理设备做好了准备。
使用DirectInputDevice对象
DirectInputDevice对象的每个实例都与系统中的特定设备相关。此对象提供了对系统硬件更多的控制和能力,从而使DirectX的允诺实现。下面讨论拥有了DirectInputDevice对象后下一步干什么。
拥有了IdirectInputDevice接口的一个接口指针,现在干什么?首先,设置设备的数据格式。通过调用SetDataFormat来完成,该函数是一个接口成员函数。设置数据格式包括无数可能的决定,包括轴信息、相对或绝对坐标信息、等等。所有这些细节通过一个叫作DIDATAFORMAT的结构传递给此函数。实际上,SetDataFormat唯一的参数就是指向此结构的指针。
填写这个结构的细节会使人发憷。值得感谢的是这一工作并不是必须的,因为DirectInput已经定义了几个DIDATAFORMAT结构变量,可以用于比较普通的输入设备:c_dfDIKeyboard, c_dfDIMouse, c_dfDIJoystick, 和c_dfDIJoystick2。为普通的力反馈游戏杆设置数据格式,可以使用下面的调用形式:
lpdid->SetDataFormat( &c_dfDIJoystick ) ;
在此例中,lpdid是指向IdirectInputDevice接口的指针。
设置完设备对象的数据格式后,就需要设置设备的协作级别。因为协作级别在整个DirectX中很常见,所以这里要做一下说明。大多数直接处理系统硬件的DirectX对象在接口的成员中都有一个叫作SetCooperativeLevel函数。这个函数很重要,因为它定义了程序操纵与系统中其它进程有关的硬件的控制级别。同其它DirectX对象一样,只有设置了协作级别才能使DirectInputDevice对象工作。要理解协作级别,就需要熟悉Acquire函数。调用此函数是为了获得对物理设备的实际访问(不要和逻辑上的DirectInputDevice对象混了)。相反的,Unacquire函数释放对物理设备的访问。
下面是函数SetCooperativeLevel的定义:
HRESULT SetCooperativeLevel(
HWND hwnd,
DWORD dwFlags
);
hwnd是程序的主窗口。标志是下面一些值的或操作的结合: DISCL_BACKGROUND, DISCL_FOREGROUND, DISCL_EXCLUSIVE, DISCL_ NONEXCLUSIVE。
如果标志参数中或上了DISCL_EXCLUSIVE,则当获得设备后本程序就成为唯一允许访问该物理设备的进程。另一方面,如果选择了DISCL_NONEXCLUSIVE,那么系统中可以有多个进程同时协作获得和使用该设备。如果或上了DISCL_BACKGROUND,程序将不会失去物理设备。然而,象Ctrl+Alt+Del组合键被按下这样的系统事件仍然能够隐含地“unacquire”程序中的设备。如果使用了DISCL_ FOREGROUND,当不是活动窗口时,程序将会自动释放物理设备。这就是将程序主窗口句柄传递给SetCooperativeLevel的意义。DirectX根据窗口是否是系统当前活动窗口自动调整设备共享。
那么所有这些值的意义是什么呢?下面举个例子说明。如果力反馈游戏杆的协作模式是DISCL_FOREGROUND | DISCL_EXCLUSIVE,那么只要程序处于活动状态,就能够从游戏杆读数据并播放力反馈效果(力反馈需要exclusive-level协作)。只要用户一选择其它程序,程序就失去对物理设备的控制,新激活的程序就能够访问该设备。这意味着在调试程序时,如果切换到调试器窗口,程序就会因为窗口变为非活动的而失去对游戏杆的控制。
如果将同一游戏杆的协作级别设为DISCL_BACKGROUND | DISCL_EXCLUSIVE将会是什么情况呢?程序将会所有时间都能访问游戏杆,不管窗口的状态。但是现在系统中其它进程就不能获得游戏杆,除非程序释放了游戏杆,不管用户在做什么!
非常明显,在正式发布的产品中应该使用DISCL_FOREGROUND | DISCL_EXCLUSIVE,而在调试版本中应该使用DISCL_BACKGROUND|DISCL_EXCLUSIVE。但是也不总是这样选择。例如,如果设备是系统键盘,那么DirectInputDevice想独占使用而调用SetCooperativeLevel将会失败。这是因为操作系统想要允许用户自由地从一个程序切换到另一个程序。类似的,DirectInputDevice不会允许以协作级别DISCL_BACKGROUND|DISCL_EXCLUSIVE请求系统鼠标。Windows不希望一个程序能够完全将用户与操作系统的联系切断。
在能够从物理设备读取信息或向物理设备发送信息之前,必须要用Acquire获得设备。在临时或永久结束设备使用时要明确地使用Unacquire函数释放设备。但Unacquire并不是失去设备控制的唯一方法。
如果设置协作级别时使用DISCL_FOREGROUND标志,那么程序的主窗口不再是系统中的活动窗口时设备将被明确释放。这就是说,在程序调用Acquire和实际试图从设备读取信息之间,能够失去对设备的占有。所以需要检查返回值来捕捉这样的错误,并准备好在任何时间重新获得该设备。
关于Acquire和Unacquire的决定性要点:当程序获得独占协作级别的设备时,DirectX拥有该设备。例如,如果鼠标被DirectX(独占)获得,那么程序窗口中的按钮就不会对鼠标做出响应。这就是说,如果想让Windows对设备响应,就应该释放该设备。换句话说,如果不想让DirectInput从设备中读取数据,就调用Unacquire。
设置完设备的协作级别后,接着应该为设备配置其它设置。获得了设备后,接着就应该开始使用GetDeviceState函数轮流检测输入的数据。当完成与设备对象的操作后,调用Unacquire释放DirectInputDevice对象。设备与设备之间存在细节上的差别;下面讲解游戏杆和键盘,应该能为从其它设备读取输入提供足够的基础知识。
键盘
键盘是到目前为止最容易读取的设备。实际上,设置完数据格式、协作级别、获得设备以后,就可以读取键盘状态了。读取键盘状态要使用IdirectInputDevice接口的GetDeviceState成员。GetDeviceState用关于物理设备的状态信息组装一个结构,所组装结构的类型由前面对SetDataFormat的调用决定。对键盘来说,此数据结构是一个简单的256个字节组成的数组。每个字节对应于键盘上的一个键,如果某个键按下,相应字节的高位就被设置。
DirectInput定义了一套以DIK_XXX为前缀的常量,这些常量可以用来索引字节数组以找到关于特定键的数据。例如,如果要检查右Shif键当前是否按下,可以使用DIK_RSHIFT定义:
GetDeviceState(256,(LPVOID) cKeyboardData) ;
if(cKeyboardData[DIK_ RSHIFT]&0x80)
DoWhatever() ;
CKeyboardData是256个字节的缓冲区。几乎就是这么简单,但是要记住,不管GetDeviceState在何时返回DIERR_INPUTLOST,就必须使用Acquire获得设备。这种情况发生在每次用户从程序切换离开的时候。
还有一点很重要,就是能够请求DirectInput缓冲键盘信息。这要求提供一个缓冲区并使用SetProperty为设备设置缓冲区大小。在本文中没有篇幅讨论这一技术,但这一技术在程序不能相当频繁的检查键盘状态时非常有用。用户有可能在程序中两次GetDeviceState调用之间按下又松开了一个键,如果DirectInput不缓冲键盘数据的化,这种击键动作就丢失了。
游戏杆
游戏杆非常好玩。与其好听的名称(Joystick——原意为欢乐杆)相符,这种设备为游戏体验添加了许多乐趣,同时也为程序员的体验添加了一些东西。正常情况下,通过调用IdirectInput接口的CreateDevice成员得到IdirectInputDevice接口(和对象),这对游戏杆也适用。
但是开发人员都希望立即将接口升级到IDirectInputDevice2,那么可以象下面这样使用QueryInterface调用请求CreateDevice返回新的接口:
hr = lpDIDeviceJoystickTemp->QueryInterface( IID_IDirectInputDevice2,
(void **) &g_lpDIDeviceJoystick);
如果成功,就可以释放原来的接口,开始使用漂亮的新IDirectInputDevice2接口。但是为什么要这么做呢?IDirectInputDevice2接口提供IdirectInputDevice的所有功能,而且还有另外两个重要特性:支持查询设备和支持力反馈设备。
其次,需要设置上的一些考虑。还记得SetDataFormat定义了GetDeviceState返回的数据的类型。对于游戏杆设备,使用c_dfDIJoystick或c_dfDIJoystick2两个预定义变量之一,将返回数据的类型设置为DIJOYSTATE或DIJOYSTATE2结构。选择哪种主要取决于要使用游戏杆哪种类型的特性。浏览这些结构中的成员应该对弄清这个问题有帮助。
同所有输入设备一样,要为游戏杆设置数据格式和协作级别。游戏杆往往比键盘需要更多一点注意。这是因为现在还几乎没有功能完美的游戏杆,所以程序应该检查以确保控制的设备能满足要求。如果不能,就调整要求或者提醒用户游戏杆太落后!设备的能力可以并且应该调用IdirectInputDevice接口的成员函数GetCapabilities探测。
这就引出了适用于所有DirectX组件的另一个讨论点。DirectX为多种设备提供广泛的支持。软件开发环境和使用环境可能有很大差别,不同的计算机支持不同水平的DirectX功能。编写好使用DirectX的软件,需要检查硬件的能力。最差的情况下,如果某个功能不支持,可以退出程序。最好的情况当然是程序能够聪明地根据缺少的特性调整本身的需求。
在开始从设备得到输入之前,需要设置设备的特性。这些特性包括象返回值的范围、游戏杆的中心点等此类的细节。这一工作由函数SetProperty完成,相当复杂。
SetProperty设置设备的一个特性。首先,必须使用关于要改变的设置的一些信息填写一个数据结构。请参考Platform SDK中的文档,得到所有数据结构。每个结构都以一个DIPROPHEADER结构开始,此结构中填写描述要改变的设置的信息。然后,用特定于所改变的设置的数据填写结构中剩余的部分。最后,调用SetProperty,参数是GUID和指向结构中DIPROPHEADER部分的指针。下面的代码片段将游戏杆的垂直范围设置为–100到100:
DIPROPRANGE dipRange ;
dipRange.diph.dwSize = sizeof(dipRange);
dipRange.diph.dwHeaderSize = sizeof(dipRange.diph);
dipRange.diph.dwObj = DIJOFS_Y;
dipRange.diph.dwHow = DIPH_BYOFFSET;
dipRange.lMin = -100;
dipRange.lMax = +100;
g_lpDIDeviceJoystick->SetProperty( DIPROP_RANGE, &dipRange.diph) ;
此结构中最难懂的部分是diph.dwObj和diph.dwHow。diph.dwHow描述diph.dwObj中保存何种信息。diph.dwObj实际描述哪个属性被设置。大多数情况下,diph.dwHow的值是DIPH_BYOFFSET,diph.dwObj的值是传递给SetDataFormat的结构中一个预定义的偏移。
应该指出能够列举设备的对象,包括按钮和其它特点。这一工作由EnumObjects函数完成。这样做时,应该提供一个对象标志符。将此标志符传递给diph.dwObj成员,将diph.dwHow成员填写为DIPH_BYID。
在从设备读取数据之前,至少要为设备的X和Y坐标轴设置最小和最大值。设置好设备属性后,就可以获得设备并开始从设备获得数据。从游戏杆获取数据与从键盘或鼠标获取数据不同,因为游戏杆是查询设备。
键盘和鼠标会引发硬件中断,由系统中的驱动程序处理,并用来更新通过调用GetDeviceState由DirectInput返回的数据。查询设备(如大多数游戏杆)不产生硬件中断,因此,DirectInput必须被告知从设备获取状态信息。这一工作通过调用IDirectInputDevice2接口的Poll成员函数完成。此时也是检查 设备是否需要重新获得的适当时机。设备被成功查询后,就可以调用GetDeviceState获取状态信息。
如果调用SetDataFormat时使用c_dfDIJoystick变量,那么GetDeviceState将用游戏杆当前的状态信息填充一个DIJOYSTATE结构。此结构的内容主要取决于物理设备的特性和SetProperty的设置。例如,如果结构中的lY成员等于-50,并且Y轴的范围设置为-100到100,那么就是说游戏杆在垂直方向上处于中心和最顶端的中间。程序中应该确保设备的范围设置为能合理满足需求的值。为了从游戏杆设备中获取数据,程序应该定期查询设备。
使用DirectInputEffect
首先,应该解释一些力反馈技术。力反馈设备是能够产生用户可以感觉到的力的设备,这些力叫作效果,例如颠簸效果或者持续的将操纵杆推向右上方的力。这些效果是“播放”出来的,效果由程序控制播放,或者对函数调用响应,或者对用户按键自动反应。
DirectInput目前支持大约一打不同的效果类型(见表5)。这些效果的范围从完全由程序控制的低级持续力效果,到由DirectInput或设备自己控制的高级倾斜或波动效果。效果有四种基本类型:持续力、倾斜效果、周期效果和条件。持续力是单一方向上不改变强度的力。倾斜效果是强度随时间线性变化的持续的力。周期效果是沿着给定的轴重复变化,其量级或者力的强度由周期效果定义。条件是对用户与游戏杆的交互作用做出响应的效果。这种效果可能是象一根弹簧,操纵杆向某个方向推得越远,反弹力就越强。
表5:DirectInput效果的类型
GUID | 说明 | 使用方法注解 |
GUID_ConstantForce | 固定强度、特定方向的持续拉力。 | 使用DICONSTANT力结构作为DIEFFECT结构的一部分实现持续力。 |
GUID_CustomForce | 一序列持续力下传到设备,按顺序播放。 | DICUSTOMFORCE结构被用来定义力。 |
GUID_Damper | 随沿坐标轴的移动增加的条件效果。 | 实现这种效果的特定类型结构是DICONDITION结构。条件效果通常不支持包。 |
GUID_Friction | 阻碍沿坐标轴移动的条件效果。 | 实现这种效果的特定类型结构是DICONDITION结构。条件效果通常不支持包。 |
GUID_Inertia | 随沿坐标轴移动的加速度增加的条件效果。 | 实现这种效果的特定类型结构是DICONDITION结构。条件效果通常不支持包。 |
GUID_RampForce | 特定方向上大小线性增加或减小的拉力。 | DIRAMPFORCE结构被用来作为DIEFFECT结构中的类型相关部分。 |
GUID_SawtoothDown | 力瞬间达到最大然后线性减小到最小的周期效果。 | 需要的特定类型结构是DIPERIODIC结构。 |
GUID_SawtoothUp | 力从最小线性增加到最大然后瞬间降到最小的周期效果 | 需要的特定类型结构是DIPERIODIC结构。 |
GUID_Sine | 力正弦变化的周期效果。 | 需要的特定类型结构是DIPERIODIC结构。 |
GUID_Spring | 力随到某个中点的相对距离而增大的条件效果。 | 实现这种效果的特定类型结构是DICONDITION结构。条件效果通常不支持包。 |
GUID_Square | 力瞬时在最大与最小之间转变的周期效果。 | 需要的特定类型结构是DIPERIODIC结构。 |
GUID_Triangle | 力在最大与最小之间线性变化的周期效果。 | 需要的特定类型结构是DIPERIODIC结构。 |
下面所有与力反馈游戏杆有关的工作都是针对Microsoft SideWinder Force Feedback Pro游戏杆,这就是说,本文中的某些细节对其它设备可能多少会产生一些问题。
在创建力反馈效果以前先获得设备是一个不错的想法。虽然这不是必须的,但是在效果能够被下传到设备前必须要获得设备。这一点对于播放对用户按下按钮做出反应的力效果尤其重要。
要创建效果,首先要为每个打算使用的效果创建DirectInputEffect对象的实例。这一工作通过调用IDirectInputDevice2接口的CreateEffect成员函数完成。此函数需要效果的GUID,以及指向DIEFFECT结构的指针,该结构中填写的是效果的细节。最后,CreateEffect返回一个指向IdirectInputEffect接口的指针,该指针的地址是CreateEffect的一个参数。这个调用的核心部分集中在DIEFFECT结构的填充。
DIEFFECT结构如下定义:
typedef struct {
DWORD dwSize;
DWORD dwFlags;
DWORD dwDuration;
DWORD dwSamplePeriod;
DWORD dwGain;
DWORD dwTriggerButton;
DWORD dwTriggerRepeatInterval;
DWORD cAxes;
LPDWORD rgdwAxes;
LPLONG rglDirection;
LPDIENVELOPE lpEnvelope;
DWORD cbTypeSpecificParams;
LPVOID lpvTypeSpecificParams;
} DIEFFECT, *LPDIEFFECT;
dwSize成员是此结构的字节数。DwFlags指出效果使用的坐标类型,以及是使用偏移方法还是ID方法描述按钮(就向前面说明的SetProperty)。通常情况下,可以设置为DIEFF_CARTESIAN|DIEFF_OBJECTOFFSETS,即按钮采用偏移描述,坐标使用XYZ坐标形式。
DwDuration说明效果播放多少毫秒。注意dwDuration可以设为INFINITE。DwSamplePeriod说明效果播放一个周期花费多少毫秒。不同设备支持不同的周期。实际中,SideWinder游戏杆支持的周期不大于1秒,不小于1/80秒。DwGain可以看作效果的主要量,因为它说明效果多么有力。此值的范围是0到10000。
DwTriggerButton和dwTriggerRepeatInterval用来设置触发效果播放的按钮,以及重复频率。当然,可以通过将dwTriggerButton的值设置为DIEB_NOTRIGGER来将效果设置为与按钮无关。否则,dwFlags定义通过ID还是偏移方式描述按钮。因为偏移方式不需要调用EnumObjects,所以一般可以将值指定为DIJOFS_ BUTTON0和DIJOFS_BUTTON1。
CAxes成员说明效果将影响几个轴。RgdwAxes指向一个描述所包含的轴的DWORD数组,数组中每个轴是一个成员。同按钮一样,轴也是用偏移或者ID来指明。一般的偏移值包括DIJOFS_X和DIJOFS_Y。
同样,rglDirection成员指向一个long型数组,每个轴是一个成员。在笛卡儿坐标中,(Y=-1,X=1)与(Y=-10,X=10)描述的是同一个方向。这就是说,如果想得到一个不是45度整数倍方向上的斜的力,就应该调整两个值的相对大小。例如,(Y=-10,X=1)描述与上面例子在同一象限的方向,但却明显靠近Y轴。
效果也可以有描述它们的包。填充一个DIENVELOPE结构,并将其地址填写到lpEnvelope成员就可以完成。包可以在一段时间内影响效果的数量或力量。其中,起动水平是效果的开始变化点,启动时间说明效果达到力量保持阶段花费多少毫秒。衰减水平是效果在包最后达到的水平,衰减时间是衰减用掉了多少豪秒。包可以用来制造初始状态较强,然后慢慢衰减的力效果。图1中描绘了包如何改变效果。
图1:包效果
DIEFFECT结构的最后两个成员是cbTypeSpecificParams和lpvTypeSpecificParams。它们保存特定于所创建效果类型的结构的字节数和地址。特定类型的效果使用何种结构的信息见表5。
填写完这个结构并调用CreateEffect后,就会获得指向IdirectInputEffect接口的指针,现在可以使用此接口播放效果,改变效果等。如果没有将效果联系到按钮,就必须用IdirectInputEffect接口的Start和Stop成员播放和停止效果。如果效果与按钮关联,那么在创建时下传到设备;否则,效果在播放时自动下传到设备。如果程序必须重新获得设备,那么所有与按钮相关的效果必须通过明确的调用Download成员才能下传到设备。
效果能够用Unload成员卸载,也能够通过向SetParameters成员函数传递新的DIEFFECT结构重新设置参数。当程序用完效果后,必须调用接口的Release成员。
演示例子
图2:演示程序
首先,应该建立演示代码并运行,应该能看到一个游戏杆配置窗口(见图2)。使用游戏杆可以移动中间的人,在窗口的左上角是坐标和输入状态信息。如果有力反馈游戏杆,那么通过按下按钮1和2应该能感觉到一对不同的力。如果将小人撞到窗口的边缘,应该能感到碰撞效果。
这个例子说明了DirectInput的使用。这里仍然有相当数量的代码与DirectInput没有直接关系。这些代码根据功能划分成模块。Main.cpp是基本的WinMain样板文件和窗口创建代码。除了调用初始化函数外,这部分代码基本上与本文的其它部分没有关系。它创建窗口,进入消息循环。WndProc.cpp包含程序窗口的窗口过程。
Demo.cpp开始了有意义的代码。不论何时提到“demo”,都是指程序游戏。例如,InitDemo函数为图形设置状态数据并创建一些所需的时间和线程。除了初始化,此演示程序运行在第二个线程中。该线程尝试读取输入并刷新状态数据,每秒进行32次。然后使窗口无效,从而让主线程重新绘制窗口。这就是说,输入和状态变化的一个反复,或者说一个演示周期,大约有1/32秒。所以,不管显示刷新得多么频繁,输入响应速度都会保持一致。
DX.cpp包含DirectX需要的非常小的初始化和结束处理,然后调用完成特殊DirectInput工作的函数。除了CoInitialize和CoUninitialize外,DXInput模块包含本文中提到的所有内容。函数按照演示程序中用到的顺序列出,每个只列一次。注意,DirectInput的大部分工作在初始化中完成。冗长的任务划分成几个函数列在表6中。
表6:DXInput.cpp的函数
成员函数 | 说明 |
InitDirectInput | 为系统键盘初始化DirectInput对象和DirectInputDevice对象。 |
EnumJoy | 列举设备的回调函数。此函数为系统中安装的第一个游戏杆创建DirectInputDevice。 |
InitForceFeedback | 如果找到游戏杆是适应力反馈的,此函数就为力反馈效果进行一些设置。 |
InitRampEffect, InitBumpEffects, InitWavyEffect | 这些函数每个都设置一个效果。这些效果演示了DirectInput中几种不同的效果,并且应该对创建新效果有用。 |
这个模块中的另一个要点是演示程序重复调用的函数。ForceEffect播放一个存在的效果,GetKeyboardInput获得键盘输入,GetJoystickInput获得游戏杆输入。最后UnInitDirectInput结束所有的一切。
要获得完整的源代码,请访问MSJ的Web站点http://www.microsoft.com/msj.