`
yingyingol
  • 浏览: 747939 次
文章分类
社区版块
存档分类
最新评论

掌握DirectX和DirectInput—力反馈游戏杆 (2)

 
阅读更多

使用DirectInput对象

一旦拥有了DirectInput对象,就可以用它来创建DirectInputDevice对象,来管理系统中特定的设备。创建DirectInputDevice对象要使用CreateDevice函数,它是作为IdirectInput接口一部分的五个函数之一。CreateDevice需要所请求设备的GUID,返回新DirectInputDevice对象的IdirectInputDevice接口指针。

这些内容看起来很熟悉,因为它与CoCreateInstanceDirectInputCreate类似。但是,现在还没有完全准备好开始DirectInputDevice对象,原因是在创建DirectInputDevice对象前需要该设备的GUID

DirectInput库为创建DirectInputDevice对象预定义了两个GUIDGUID_SysKeyboardGUID_SysMouse。将两者之一直接传递给CreateDevice函数,就会得到相应设备的DirectInputDevice对象。

注意,令人感到奇怪的是缺少对游戏杆的预定义GUID。在Windows中,通常都有系统键盘和系统鼠标,另一方面,系统本身并不使用游戏杆。可以安装一个或者多个游戏杆,但系统管理的范围只限于驱动程序级。系统并为这些设备指定特殊的系统状态,也不会在日常事务中使用这些设备。因此,为游戏杆定义GUIDDirectInput来说是不合理的。

那么,如何才能找到与系统连接的游戏杆的GUID呢?要得到它们,必须要列举设备。列举系统设备和性能在DirectX中相当普遍。要列举系统中的输入设备,需要使用EnumDevices函数。EnumDevicesIdirectInput接口的一部分,如下定义:

注意此函数与Windows中其它列举API相同,例如EnumWindows。第二个参数是一个回调函数。第三个参数是程序中定义的32位值。第一个参数是想要列举的设备类型,对游戏杆来说,是DIDEVTYPE_JOYSTICK(全部的设备类型列在表4中)。最后一个参数是详细描述想要列举的设备的标志。现在支持的标志是DIEDFL_ATTACHEDONLYDIEDFL_ALLDEVICES(这两个标志是互斥独占的),此外还有DIEDFL_FORCEFEEDBACK,此标志表示力反馈设备,能够和另两个标志位或操作。

4:定义列举的输入设备

以下定义的值可以传递给EnumDevices来选择列举哪种类型的输入设备。另外也支持子类型,见SDKDIDEVICEINSTANCE结构的文档。

说明

DIDEVTYPE_MOUSE

列举鼠标设备 (标准、轨迹球等)

DIDEVTYPE_KEYBOARD

列举键盘设备 (标准、键区等)

DIDEVTYPE_JOYSTICK

列举游戏杆设备 (操纵杆、操纵轮、方向舵等)

DIDEVTYPE_DEVICE

列举其它设备

EnumDevices列举系统中的输入设备时,反复地调用回调函数。回调函数定义如下:

BOOL CALLBACK EnumProc(LPCDIDEVICEINSTANCE lpddi,LPVOID pvRef) ;

因为回调函数是由用户程序定义并传递给EnumDevices的,所以是调用CreateDevice的最合适地方,直到创建了满足需要的足够DirectInputDevice对象为止。但是回调函数并非一定要如此实现,可以简单的将列举设备的所有GUID保存在一个表中,在以后的代码中使用。

回调函数接受两个参数。第二个参数是程序定义的传递给EnumDevices32位值。更重要的是,第一个参数传递指向一个结构的指针,该结构包含关于能够与列举标准匹配的单个设备的许多信息。这是一个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的定义:

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和实际试图从设备读取信息之间,能够失去对设备的占有。所以需要检查返回值来捕捉这样的错误,并准备好在任何时间重新获得该设备。

关于AcquireUnacquire的决定性要点:当程序获得独占协作级别的设备时,DirectX拥有该设备。例如,如果鼠标被DirectX(独占)获得,那么程序窗口中的按钮就不会对鼠标做出响应。这就是说,如果想让Windows对设备响应,就应该释放该设备。换句话说,如果不想让DirectInput从设备中读取数据,就调用Unacquire

设置完设备的协作级别后,接着应该为设备配置其它设置。获得了设备后,接着就应该开始使用GetDeviceState函数轮流检测输入的数据。当完成与设备对象的操作后,调用Unacquire释放DirectInputDevice对象。设备与设备之间存在细节上的差别;下面讲解游戏杆和键盘,应该能为从其它设备读取输入提供足够的基础知识。

键盘

键盘是到目前为止最容易读取的设备。实际上,设置完数据格式、协作级别、获得设备以后,就可以读取键盘状态了。读取键盘状态要使用IdirectInputDevice接口的GetDeviceState成员。GetDeviceState用关于物理设备的状态信息组装一个结构,所组装结构的类型由前面对SetDataFormat的调用决定。对键盘来说,此数据结构是一个简单的256个字节组成的数组。每个字节对应于键盘上的一个键,如果某个键按下,相应字节的高位就被设置。

DirectInput定义了一套以DIK_XXX为前缀的常量,这些常量可以用来索引字节数组以找到关于特定键的数据。例如,如果要检查右Shif键当前是否按下,可以使用DIK_RSHIFT定义:

CKeyboardData256个字节的缓冲区。几乎就是这么简单,但是要记住,不管GetDeviceState在何时返回DIERR_INPUTLOST,就必须使用Acquire获得设备。这种情况发生在每次用户从程序切换离开的时候。

还有一点很重要,就是能够请求DirectInput缓冲键盘信息。这要求提供一个缓冲区并使用SetProperty为设备设置缓冲区大小。在本文中没有篇幅讨论这一技术,但这一技术在程序不能相当频繁的检查键盘状态时非常有用。用户有可能在程序中两次GetDeviceState调用之间按下又松开了一个键,如果DirectInput不缓冲键盘数据的化,这种击键动作就丢失了。

游戏杆

游戏杆非常好玩。与其好听的名称(Joystick——原意为欢乐杆)相符,这种设备为游戏体验添加了许多乐趣,同时也为程序员的体验添加了一些东西。正常情况下,通过调用IdirectInput接口的CreateDevice成员得到IdirectInputDevice接口(和对象),这对游戏杆也适用。

但是开发人员都希望立即将接口升级到IDirectInputDevice2,那么可以象下面这样使用QueryInterface调用请求CreateDevice返回新的接口:

如果成功,就可以释放原来的接口,开始使用漂亮的新IDirectInputDevice2接口。但是为什么要这么做呢?IDirectInputDevice2接口提供IdirectInputDevice的所有功能,而且还有另外两个重要特性:支持查询设备和支持力反馈设备。

其次,需要设置上的一些考虑。还记得SetDataFormat定义了GetDeviceState返回的数据的类型。对于游戏杆设备,使用c_dfDIJoystickc_dfDIJoystick2两个预定义变量之一,将返回数据的类型设置为DIJOYSTATEDIJOYSTATE2结构。选择哪种主要取决于要使用游戏杆哪种类型的特性。浏览这些结构中的成员应该对弄清这个问题有帮助。

同所有输入设备一样,要为游戏杆设置数据格式和协作级别。游戏杆往往比键盘需要更多一点注意。这是因为现在还几乎没有功能完美的游戏杆,所以程序应该检查以确保控制的设备能满足要求。如果不能,就调整要求或者提醒用户游戏杆太落后!设备的能力可以并且应该调用IdirectInputDevice接口的成员函数GetCapabilities探测。

这就引出了适用于所有DirectX组件的另一个讨论点。DirectX为多种设备提供广泛的支持。软件开发环境和使用环境可能有很大差别,不同的计算机支持不同水平的DirectX功能。编写好使用DirectX的软件,需要检查硬件的能力。最差的情况下,如果某个功能不支持,可以退出程序。最好的情况当然是程序能够聪明地根据缺少的特性调整本身的需求。

在开始从设备得到输入之前,需要设置设备的特性。这些特性包括象返回值的范围、游戏杆的中心点等此类的细节。这一工作由函数SetProperty完成,相当复杂。

SetProperty设置设备的一个特性。首先,必须使用关于要改变的设置的一些信息填写一个数据结构。请参考Platform SDK中的文档,得到所有数据结构。每个结构都以一个DIPROPHEADER结构开始,此结构中填写描述要改变的设置的信息。然后,用特定于所改变的设置的数据填写结构中剩余的部分。最后,调用SetProperty,参数是GUID和指向结构中DIPROPHEADER部分的指针。下面的代码片段将游戏杆的垂直范围设置为–100100


此结构中最难懂的部分是diph.dwObjdiph.dwHowdiph.dwHow描述diph.dwObj中保存何种信息。diph.dwObj实际描述哪个属性被设置。大多数情况下,diph.dwHow的值是DIPH_BYOFFSETdiph.dwObj的值是传递给SetDataFormat的结构中一个预定义的偏移。

应该指出能够列举设备的对象,包括按钮和其它特点。这一工作由EnumObjects函数完成。这样做时,应该提供一个对象标志符。将此标志符传递给diph.dwObj成员,将diph.dwHow成员填写为DIPH_BYID

在从设备读取数据之前,至少要为设备的XY坐标轴设置最小和最大值。设置好设备属性后,就可以获得设备并开始从设备获得数据。从游戏杆获取数据与从键盘或鼠标获取数据不同,因为游戏杆是查询设备。

键盘和鼠标会引发硬件中断,由系统中的驱动程序处理,并用来更新通过调用GetDeviceStateDirectInput返回的数据。查询设备(如大多数游戏杆)不产生硬件中断,因此,DirectInput必须被告知从设备获取状态信息。这一工作通过调用IDirectInputDevice2接口的Poll成员函数完成。此时也是检查 设备是否需要重新获得的适当时机。设备被成功查询后,就可以调用GetDeviceState获取状态信息。

如果调用SetDataFormat时使用c_dfDIJoystick变量,那么GetDeviceState将用游戏杆当前的状态信息填充一个DIJOYSTATE结构。此结构的内容主要取决于物理设备的特性和SetProperty的设置。例如,如果结构中的lY成员等于-50,并且Y轴的范围设置为-100100,那么就是说游戏杆在垂直方向上处于中心和最顶端的中间。程序中应该确保设备的范围设置为能合理满足需求的值。为了从游戏杆设备中获取数据,程序应该定期查询设备。

使用DirectInputEffect

首先,应该解释一些力反馈技术。力反馈设备是能够产生用户可以感觉到的力的设备,这些力叫作效果,例如颠簸效果或者持续的将操纵杆推向右上方的力。这些效果是“播放”出来的,效果由程序控制播放,或者对函数调用响应,或者对用户按键自动反应。

DirectInput目前支持大约一打不同的效果类型(见表5)。这些效果的范围从完全由程序控制的低级持续力效果,到由DirectInput或设备自己控制的高级倾斜或波动效果。效果有四种基本类型:持续力、倾斜效果、周期效果和条件。持续力是单一方向上不改变强度的力。倾斜效果是强度随时间线性变化的持续的力。周期效果是沿着给定的轴重复变化,其量级或者力的强度由周期效果定义。条件是对用户与游戏杆的交互作用做出响应的效果。这种效果可能是象一根弹簧,操纵杆向某个方向推得越远,反弹力就越强。

5DirectInput效果的类型

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结构如下定义:


dwSize成员是此结构的字节数。DwFlags指出效果使用的坐标类型,以及是使用偏移方法还是ID方法描述按钮(就向前面说明的SetProperty)。通常情况下,可以设置为DIEFF_CARTESIAN|DIEFF_OBJECTOFFSETS,即按钮采用偏移描述,坐标使用XYZ坐标形式。

DwDuration说明效果播放多少毫秒。注意dwDuration可以设为INFINITEDwSamplePeriod说明效果播放一个周期花费多少毫秒。不同设备支持不同的周期。实际中,SideWinder游戏杆支持的周期不大于1秒,不小于1/80秒。DwGain可以看作效果的主要量,因为它说明效果多么有力。此值的范围是010000

DwTriggerButtondwTriggerRepeatInterval用来设置触发效果播放的按钮,以及重复频率。当然,可以通过将dwTriggerButton的值设置为DIEB_NOTRIGGER来将效果设置为与按钮无关。否则,dwFlags定义通过ID还是偏移方式描述按钮。因为偏移方式不需要调用EnumObjects,所以一般可以将值指定为DIJOFS_ BUTTON0DIJOFS_BUTTON1

CAxes成员说明效果将影响几个轴。RgdwAxes指向一个描述所包含的轴的DWORD数组,数组中每个轴是一个成员。同按钮一样,轴也是用偏移或者ID来指明。一般的偏移值包括DIJOFS_XDIJOFS_Y

同样,rglDirection成员指向一个long型数组,每个轴是一个成员。在笛卡儿坐标中,(Y=-1X=1)与(Y=-10X=10)描述的是同一个方向。这就是说,如果想得到一个不是45度整数倍方向上的斜的力,就应该调整两个值的相对大小。例如,(Y=-10X=1)描述与上面例子在同一象限的方向,但却明显靠近Y轴。

效果也可以有描述它们的包。填充一个DIENVELOPE结构,并将其地址填写到lpEnvelope成员就可以完成。包可以在一段时间内影响效果的数量或力量。其中,起动水平是效果的开始变化点,启动时间说明效果达到力量保持阶段花费多少毫秒。衰减水平是效果在包最后达到的水平,衰减时间是衰减用掉了多少豪秒。包可以用来制造初始状态较强,然后慢慢衰减的力效果。图1中描绘了包如何改变效果。

1:包效果

DIEFFECT结构的最后两个成员是cbTypeSpecificParamslpvTypeSpecificParams。它们保存特定于所创建效果类型的结构的字节数和地址。特定类型的效果使用何种结构的信息见表5

填写完这个结构并调用CreateEffect后,就会获得指向IdirectInputEffect接口的指针,现在可以使用此接口播放效果,改变效果等。如果没有将效果联系到按钮,就必须用IdirectInputEffect接口的StartStop成员播放和停止效果。如果效果与按钮关联,那么在创建时下传到设备;否则,效果在播放时自动下传到设备。如果程序必须重新获得设备,那么所有与按钮相关的效果必须通过明确的调用Download成员才能下传到设备。

效果能够用Unload成员卸载,也能够通过向SetParameters成员函数传递新的DIEFFECT结构重新设置参数。当程序用完效果后,必须调用接口的Release成员。

演示例子

2:演示程序

首先,应该建立演示代码并运行,应该能看到一个游戏杆配置窗口(见图2)。使用游戏杆可以移动中间的人,在窗口的左上角是坐标和输入状态信息。如果有力反馈游戏杆,那么通过按下按钮12应该能感觉到一对不同的力。如果将小人撞到窗口的边缘,应该能感到碰撞效果。

这个例子说明了DirectInput的使用。这里仍然有相当数量的代码与DirectInput没有直接关系。这些代码根据功能划分成模块。Main.cpp是基本的WinMain样板文件和窗口创建代码。除了调用初始化函数外,这部分代码基本上与本文的其它部分没有关系。它创建窗口,进入消息循环。WndProc.cpp包含程序窗口的窗口过程。

Demo.cpp开始了有意义的代码。不论何时提到“demo”,都是指程序游戏。例如,InitDemo函数为图形设置状态数据并创建一些所需的时间和线程。除了初始化,此演示程序运行在第二个线程中。该线程尝试读取输入并刷新状态数据,每秒进行32次。然后使窗口无效,从而让主线程重新绘制窗口。这就是说,输入和状态变化的一个反复,或者说一个演示周期,大约有1/32秒。所以,不管显示刷新得多么频繁,输入响应速度都会保持一致。

DX.cpp包含DirectX需要的非常小的初始化和结束处理,然后调用完成特殊DirectInput工作的函数。除了CoInitializeCoUninitialize外,DXInput模块包含本文中提到的所有内容。函数按照演示程序中用到的顺序列出,每个只列一次。注意,DirectInput的大部分工作在初始化中完成。冗长的任务划分成几个函数列在表6中。

6DXInput.cpp的函数

成员函数

说明

InitDirectInput

为系统键盘初始化DirectInput对象和DirectInputDevice对象。

EnumJoy

列举设备的回调函数。此函数为系统中安装的第一个游戏杆创建DirectInputDevice

InitForceFeedback

如果找到游戏杆是适应力反馈的,此函数就为力反馈效果进行一些设置。

InitRampEffect, InitBumpEffects, InitWavyEffect

这些函数每个都设置一个效果。这些效果演示了DirectInput中几种不同的效果,并且应该对创建新效果有用。

这个模块中的另一个要点是演示程序重复调用的函数。ForceEffect播放一个存在的效果,GetKeyboardInput获得键盘输入,GetJoystickInput获得游戏杆输入。最后UnInitDirectInput结束所有的一切。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics