掌握DirectX 和DirectInput ——力反馈游戏杆
Jason Clark
不知不觉中,Windows 下的游戏和多媒体程序已经开始流行。硬件变得越来越快,Windows 也变得更加灵活。自从Microsoft 发布了DirectX ,游戏开发人员对其它平台已经越来越不感兴趣了。许多游戏开发者也已经将他们的开发工作完全移植到了Windows 下。
为PC 开发游戏从来就没有轻松过。从无数种显示卡和声卡中,开发者学会了在功能性和兼容性之间平衡的艺术。他们不得不处理象页面切换、段内存结构和位操作这样令人讨厌的问题。并且随着多人游戏的流行,开发者必须同时处理象 络和通信等事项。DirectX 引入后,游戏开发者变得轻松了。通过为开发者提供的DirectX 对象,绝大多数讨厌的工作已经被简化了。
基于DirectX 的程序是普通的Windows 程序吗须懂得COM 吗简单的程序值得使用DirectX 吗须使用DirectX 的全部组件吗样的问题肯定还有更多。
DirectX 揭密
DirectX 是一套为Windows 程序提供对系统硬件更亲密控制的组件。(表1 列出了DirectX 5.0 的组件及其作用)。那么,亲密控制是什么意思呢/span>
表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 获得设备。这种情况发生在每次用户从程序切换离开的时候。
游戏杆
游戏杆非常好玩。与其好听的名称(Joystick ——原意为欢乐杆)相符,这种设备为游戏体验添加了许多乐趣,同时也为程序员的体验添加了一些东西。正常情况下,通过调用IdirectInput 接口的CreateDevice 成员得到IdirectInputDevice 接口(和对象),这对游戏杆也适用。
但是开发人员都希望立即将接口升级到IDirectInputDevice2 ,那么可以象下面这样使用QueryInterface 调用请求CreateDevice 返回新的接口:
hr = lpDIDeviceJoystickTemp->QueryInterface( IID_IDirectInputDevice2,
(void **) &g_lpDIDeviceJoystick);
如果成功,就可以释放原来的接口,开始使用漂亮的新ID
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!