-- 作者:卷积内核
-- 发布时间:2/7/2006 9:12:00 AM
-- DirectX详解
DirectX是一套为Windows程序提供对系统硬件更紧密控制的组件。(表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 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中描绘了包如何改变效果。 DIEFFECT结构的最后两个成员是cbTypeSpecificParams和lpvTypeSpecificParams。它们 保存特定于所创建效果类型的结构的字节数和地址。特定类型的效果使用何种结构的信息见 表5。 填写完这个结构并调用CreateEffect后,就会获得指向IdirectInputEffect接口的指针, 现在可以使用此接口播放效果,改变效果等。如果没有将效果联系到按钮,就必须用 IdirectInputEffect接口的Start和Stop成员播放和停止效果。如果效果与按钮关联,那 么在创建时下传到设备;否则,效果在播放时自动下传到设备。如果程序必须重新获得设备, 那么所有与按钮相关的效果必须通过明确的调用Download成员才能下传到设备。 效果能够用Unload成员卸载,也能够通过向SetParameters成员函数传递新的DIEFFECT结 构重新设置参数。当程序用完效果后,必须调用接口的Release成员。
|