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

掌握DirectXDirectInput ——力反馈游戏杆

Jason 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 的组件及其作用)。那么,亲密控制是什么意思呢?

1DirectX 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 越 野赛车游戏,正手握力反馈游戏杆。在起跑线上,你可以听到赛车引擎的空转声,同时也能够通过游戏杆感觉到赛车引擎的空转!比赛开始后,你可以感觉到引擎高 速旋转的嗡嗡震动。当行驶到赛程中崎岖的地段时,你将会不停的感觉到电子碰撞。赛车在整个赛场上撞来撞去,你的游戏杆也会如此。赛车车轮卡在车辙中导致赛 车被拉向左边,游戏杆也会被拉向左边!整个过程中你可以感觉到每次颠簸、刮擦、撞击和撞毁。

     现在,带有支持DirectInputWindows 驱动程序的唯一的力反馈设备是MicrosoftSideWinder Force Feedback Pro 。这一现状不会持续太久,新设备以及现有设备的新驱动程序很快就会进入市场。

剖析DirectInput

     DirectInput 由三个对象组成:DirectInput, DirectInputDevice, DirectInputEffect ( 见表2)DirectInput 是一个高层的对象,通过DirectInput 对象可以对相关的输入设备进行基本的初始化和查找。DirectInput 对象最终用来创建低层的DirectInputDevice 对象。DirectX 中的每个主要组件都采用相同的方法,首先创建高层对象,如DirectInputDirectSound 对象,然后创建低层对象与硬件进行实际的通信。

2: DirectInput 对象

对象

说明

DirectInput

封装高层DirectInput 功能,列举设备并用来创建DirectInputDevice 对象。

DirectInputDevice

与物理输入设备的接口,例如游戏杆,包括收集和设置设备状态信息的接口,并且用来创建DirectInputEffect 对象 ( 对于力反馈设备)

DirectInputEffect

封装能够在力反馈设备上“播放”的简单效果,提供启动、停止和设置力反馈效果等功能。

     DirectInput 对象是三个对象中最容易理解的。实际上,它在一个接口形式IDirectInput ( 见表3) 中只提供五个函数。这是DirectInput 的一个非常重要的部分,因为这是出发点。

3IdirectInput 接口

成员函数

说明

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 宏定义SUCCESSFAILED 来检查。

     使用DirectInputCreate 能够容易地创建高层对象并得到其主接口指针。这是DirectX 的又一个设计方法,每个DirectX 组件都提供助手函数来创建高层对象,例如DirectInputCreateDirectDrawCreate 。在程序中可以用这些助手函数创建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

);

      第一个参数是要创建对象的GUIDDirectX 定义的GUID 是叫作CLSID_DirectInputGUID 结构变量。第二个参数是熟悉的pUnkOuter ,同样可以用NULL 忽略。第三个参数dwClsContext 定义COM 对象在何处创建,DirectX 只支持进程内服务器,所以必须使用CLSCTX_INPROC_SERVER

     第四个参数是两种方法真正的不同之处。记住COM 对象对外提供接口,与对象本身一样,接口也用GUID 识别。使用第一种方法,不能选择得到的接口,总是得到IdirectInput 。使用CoCreateInstance 可以请求对象所支持的任何接口,方法是使用为接口预定义的GUID 。但是在DirectInput 这是没有意义的,因为DirectInput 对象的唯一有用的接口就是IdirectInput 。其它DirectX 组件支持多个有用的接口。(例如,DirectDraw 对象可以用IdirectDrawIDirectDraw2 接口操作。)

最后一个参数是程序中接口指针变量的实际地址。

现在就拥有了对象和对象的一个接口。CoCreateInstance 方法还需要另外一步:必须要首先调用一个接口函数初始化对象。DirectInputCreate 提供的是一个已经初始化过的DirectInput 对象,但CoCreateInstance 没有特定于DirectInput 的认识,因此必须调用IdirectInput 接口的初始化成员函数。假设如下定义IdirectInput 接口指针变量:

LPDIRECTINPUT g_lpDI

     可以如下调用初始化函数:

g_lpDI->Initialize( hInstance, DIRECTINPUT_VERSION);

     既然选择采取这种标准方法创建对象,就不得不注意COM 需要的其他标准,例如需要调用CoInitializeCoUninitialize

使用DirectInput 对象

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

HRESULT CreateDevice(

  REFGUID rguid,

  LPDIRECTINPUTDEVICE *lplpDirectInputDevice,

  LPUNKNOWN pUnkOuter

);

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

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

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

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

HRESULT EnumDevices(

  DWORD dwDevType,

  LPDIENUMCALLBACK lpCallback,

  LPVOID pvRef,

  DWORD dwFlags

);

     注意此函数与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 的定义:

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

     关于AcquireUnacquire 的决定性要点:当程序获得独占协作级别的设备时,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() ;

     CKeyboardData256 个字节的缓冲区。几乎就是这么简单,但是要记住,不管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_dfDIJoystickc_dfDIJoystick2 两个预定义变量之一,将返回数据的类型设置为DIJOYSTATEDIJOYSTATE2 结构。选择哪种主要取决于要使用游戏杆哪种类型的特性。浏览这些结构中的成员应该对弄清这个问题有帮助。

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

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

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

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

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

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 可以设为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 结束所有的一切。

     要获得完整的源代码,请访问MSJWeb 站点http://www.microsoft.com/msj .


掌握DirectXDirectInput ——力反馈游戏杆

Jason 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 的组件及其作用)。那么,亲密控制是什么意思呢?

1DirectX 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 越 野赛车游戏,正手握力反馈游戏杆。在起跑线上,你可以听到赛车引擎的空转声,同时也能够通过游戏杆感觉到赛车引擎的空转!比赛开始后,你可以感觉到引擎高 速旋转的嗡嗡震动。当行驶到赛程中崎岖的地段时,你将会不停的感觉到电子碰撞。赛车在整个赛场上撞来撞去,你的游戏杆也会如此。赛车车轮卡在车辙中导致赛 车被拉向左边,游戏杆也会被拉向左边!整个过程中你可以感觉到每次颠簸、刮擦、撞击和撞毁。

     现在,带有支持DirectInputWindows 驱动程序的唯一的力反馈设备是MicrosoftSideWinder Force Feedback Pro 。这一现状不会持续太久,新设备以及现有设备的新驱动程序很快就会进入市场。

剖析DirectInput

     DirectInput 由三个对象组成:DirectInput, DirectInputDevice, DirectInputEffect ( 见表2)DirectInput 是一个高层的对象,通过DirectInput 对象可以对相关的输入设备进行基本的初始化和查找。DirectInput 对象最终用来创建低层的DirectInputDevice 对象。DirectX 中的每个主要组件都采用相同的方法,首先创建高层对象,如DirectInputDirectSound 对象,然后创建低层对象与硬件进行实际的通信。

2: DirectInput 对象

对象

说明

DirectInput

封装高层DirectInput 功能,列举设备并用来创建DirectInputDevice 对象。

DirectInputDevice

与物理输入设备的接口,例如游戏杆,包括收集和设置设备状态信息的接口,并且用来创建DirectInputEffect 对象 ( 对于力反馈设备)

DirectInputEffect

封装能够在力反馈设备上“播放”的简单效果,提供启动、停止和设置力反馈效果等功能。

     DirectInput 对象是三个对象中最容易理解的。实际上,它在一个接口形式IDirectInput ( 见表3) 中只提供五个函数。这是DirectInput 的一个非常重要的部分,因为这是出发点。

3IdirectInput 接口

成员函数

说明

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 宏定义SUCCESSFAILED 来检查。

     使用DirectInputCreate 能够容易地创建高层对象并得到其主接口指针。这是DirectX 的又一个设计方法,每个DirectX 组件都提供助手函数来创建高层对象,例如DirectInputCreateDirectDrawCreate 。在程序中可以用这些助手函数创建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

);

      第一个参数是要创建对象的GUIDDirectX 定义的GUID 是叫作CLSID_DirectInputGUID 结构变量。第二个参数是熟悉的pUnkOuter ,同样可以用NULL 忽略。第三个参数dwClsContext 定义COM 对象在何处创建,DirectX 只支持进程内服务器,所以必须使用CLSCTX_INPROC_SERVER

     第四个参数是两种方法真正的不同之处。记住COM 对象对外提供接口,与对象本身一样,接口也用GUID 识别。使用第一种方法,不能选择得到的接口,总是得到IdirectInput 。使用CoCreateInstance 可以请求对象所支持的任何接口,方法是使用为接口预定义的GUID 。但是在DirectInput 这是没有意义的,因为DirectInput 对象的唯一有用的接口就是IdirectInput 。其它DirectX 组件支持多个有用的接口。(例如,DirectDraw 对象可以用IdirectDrawIDirectDraw2 接口操作。)

最后一个参数是程序中接口指针变量的实际地址。

现在就拥有了对象和对象的一个接口。CoCreateInstance 方法还需要另外一步:必须要首先调用一个接口函数初始化对象。DirectInputCreate 提供的是一个已经初始化过的DirectInput 对象,但CoCreateInstance 没有特定于DirectInput 的认识,因此必须调用IdirectInput 接口的初始化成员函数。假设如下定义IdirectInput 接口指针变量:

LPDIRECTINPUT g_lpDI

     可以如下调用初始化函数:

g_lpDI->Initialize( hInstance, DIRECTINPUT_VERSION);

     既然选择采取这种标准方法创建对象,就不得不注意COM 需要的其他标准,例如需要调用CoInitializeCoUninitialize

使用DirectInput 对象

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

HRESULT CreateDevice(

  REFGUID rguid,

  LPDIRECTINPUTDEVICE *lplpDirectInputDevice,

  LPUNKNOWN pUnkOuter

);

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

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

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

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

HRESULT EnumDevices(

  DWORD dwDevType,

  LPDIENUMCALLBACK lpCallback,

  LPVOID pvRef,

  DWORD dwFlags

);

     注意此函数与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 的定义:

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

     关于AcquireUnacquire 的决定性要点:当程序获得独占协作级别的设备时,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() ;

     CKeyboardData256 个字节的缓冲区。几乎就是这么简单,但是要记住,不管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_dfDIJoystickc_dfDIJoystick2 两个预定义变量之一,将返回数据的类型设置为DIJOYSTATEDIJOYSTATE2 结构。选择哪种主要取决于要使用游戏杆哪种类型的特性。浏览这些结构中的成员应该对弄清这个问题有帮助。

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

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

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

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

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

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 可以设为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 结束所有的一切。

     要获得完整的源代码,请访问MSJWeb 站点http://www.microsoft.com/msj .




掌握DirectXDirectInput ——力反馈游戏杆

Jason 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 的组件及其作用)。那么,亲密控制是什么意思呢?

1DirectX 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 越 野赛车游戏,正手握力反馈游戏杆。在起跑线上,你可以听到赛车引擎的空转声,同时也能够通过游戏杆感觉到赛车引擎的空转!比赛开始后,你可以感觉到引擎高 速旋转的嗡嗡震动。当行驶到赛程中崎岖的地段时,你将会不停的感觉到电子碰撞。赛车在整个赛场上撞来撞去,你的游戏杆也会如此。赛车车轮卡在车辙中导致赛 车被拉向左边,游戏杆也会被拉向左边!整个过程中你可以感觉到每次颠簸、刮擦、撞击和撞毁。

     现在,带有支持DirectInputWindows 驱动程序的唯一的力反馈设备是MicrosoftSideWinder Force Feedback Pro 。这一现状不会持续太久,新设备以及现有设备的新驱动程序很快就会进入市场。

剖析DirectInput

     DirectInput 由三个对象组成:DirectInput, DirectInputDevice, DirectInputEffect ( 见表2)DirectInput 是一个高层的对象,通过DirectInput 对象可以对相关的输入设备进行基本的初始化和查找。DirectInput 对象最终用来创建低层的DirectInputDevice 对象。DirectX 中的每个主要组件都采用相同的方法,首先创建高层对象,如DirectInputDirectSound 对象,然后创建低层对象与硬件进行实际的通信。

2: DirectInput 对象

对象

说明

DirectInput

封装高层DirectInput 功能,列举设备并用来创建DirectInputDevice 对象。

DirectInputDevice

与物理输入设备的接口,例如游戏杆,包括收集和设置设备状态信息的接口,并且用来创建DirectInputEffect 对象 ( 对于力反馈设备)

DirectInputEffect

封装能够在力反馈设备上“播放”的简单效果,提供启动、停止和设置力反馈效果等功能。

     DirectInput 对象是三个对象中最容易理解的。实际上,它在一个接口形式IDirectInput ( 见表3) 中只提供五个函数。这是DirectInput 的一个非常重要的部分,因为这是出发点。

3IdirectInput 接口

成员函数

说明

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 宏定义SUCCESSFAILED 来检查。

     使用DirectInputCreate 能够容易地创建高层对象并得到其主接口指针。这是DirectX 的又一个设计方法,每个DirectX 组件都提供助手函数来创建高层对象,例如DirectInputCreateDirectDrawCreate 。在程序中可以用这些助手函数创建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

);

      第一个参数是要创建对象的GUIDDirectX 定义的GUID 是叫作CLSID_DirectInputGUID 结构变量。第二个参数是熟悉的pUnkOuter ,同样可以用NULL 忽略。第三个参数dwClsContext 定义COM 对象在何处创建,DirectX 只支持进程内服务器,所以必须使用CLSCTX_INPROC_SERVER

     第四个参数是两种方法真正的不同之处。记住COM 对象对外提供接口,与对象本身一样,接口也用GUID 识别。使用第一种方法,不能选择得到的接口,总是得到IdirectInput 。使用CoCreateInstance 可以请求对象所支持的任何接口,方法是使用为接口预定义的GUID 。但是在DirectInput 这是没有意义的,因为DirectInput 对象的唯一有用的接口就是IdirectInput 。其它DirectX 组件支持多个有用的接口。(例如,DirectDraw 对象可以用IdirectDrawIDirectDraw2 接口操作。)

最后一个参数是程序中接口指针变量的实际地址。

现在就拥有了对象和对象的一个接口。CoCreateInstance 方法还需要另外一步:必须要首先调用一个接口函数初始化对象。DirectInputCreate 提供的是一个已经初始化过的DirectInput 对象,但CoCreateInstance 没有特定于DirectInput 的认识,因此必须调用IdirectInput 接口的初始化成员函数。假设如下定义IdirectInput 接口指针变量:

LPDIRECTINPUT g_lpDI

     可以如下调用初始化函数:

g_lpDI->Initialize( hInstance, DIRECTINPUT_VERSION);

     既然选择采取这种标准方法创建对象,就不得不注意COM 需要的其他标准,例如需要调用CoInitializeCoUninitialize

使用DirectInput 对象

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

HRESULT CreateDevice(

  REFGUID rguid,

  LPDIRECTINPUTDEVICE *lplpDirectInputDevice,

  LPUNKNOWN pUnkOuter

);

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

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

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

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

HRESULT EnumDevices(

  DWORD dwDevType,

  LPDIENUMCALLBACK lpCallback,

  LPVOID pvRef,

  DWORD dwFlags

);

     注意此函数与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 的定义:

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

     关于AcquireUnacquire 的决定性要点:当程序获得独占协作级别的设备时,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() ;

     CKeyboardData256 个字节的缓冲区。几乎就是这么简单,但是要记住,不管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_dfDIJoystickc_dfDIJoystick2 两个预定义变量之一,将返回数据的类型设置为DIJOYSTATEDIJOYSTATE2 结构。选择哪种主要取决于要使用游戏杆哪种类型的特性。浏览这些结构中的成员应该对弄清这个问题有帮助。

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

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

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

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

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

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 可以设为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 结束所有的一切。

     要获得完整的源代码,请访问MSJWeb 站点http://www.microsoft.com/msj .

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值