DirectInput编程基础

导读:
  DirectInput编程基础 - 简介 出 处: 中国游戏开发者
  [ 2001-09-09] 做 者
  目 录
  1.1 DirectInput概念
  1.2 设置DirectInput
  1.3 列举设备
  1.4 设置设备
  1.5 取得输入数据
  绪言
  输入相对于图形和声音而言从未成为游戏开发中的很是重要的论题。读取键盘按键、鼠标移动和游戏杆位置彷佛并无什么困难,但随着新输入设备对市场的强烈冲击以及DirectX的发行,这一问题变得日益重要了。
  若是用户是DirectInput编程的新手,那么应先排除掉一些旧观念。应记住的重要一点是DirectInput的得名是由于它直接与设备驱动器通信,对鼠标和键盘也就意味着当即响应硬件中断,而不是等待Windows发送消息证实已发生输入事件。固然,DirectInput放弃了Windows的消息队列,在虚构WM_KEYDOWN或WM_MOVSEMOVE等消息的同时也就再也不享受Windows提供的一些服务。这种服务大都不是对游戏输入特别有用,但它有助于理解缺乏这些服务时所产生输入信息的含义,后面有关鼠标和键盘输入的章节中,将深刻地讨论这一问题。
  固然,DirectInput放弃了Windows的消息队列,在虚构WM_KEYDOWN及WM_MOVSEMOVE等消息的同时也就再也不享受Windows提供的一些服务。这种服务大都不是对游戏输入特别有用,但它有助于理解缺乏这些服务时所产生输入信息的含义,后面有关鼠标和键盘输入的章节中,将深刻地讨论这一问题。
  1.一、DirectInput概念
  下面看一下DirectInput所关心的内容:输入设备及其部件。
  1.1.1 设备
  DirectInput可识别三种基本设备类型:
  键盘,标准系统键盘。
  鼠标,这一类中包括全部相似于鼠标的设备,如触摸板、跟踪球以及相应的按键。
  游戏杆,相对于其它类型的控制器和力反馈设备而言,这是易于产生误解的名称,其范围从简单的游戏杆直至虚拟现实的复杂设备。本书中,咱们使用这一名称是为了与DirectInput术语保持一致,但应记住,当谈到“游戏杆”时,这一说法通常是“特殊游戏控制器”的简称。  只要讲到DirectInput,那么设备就是指由设备驱动器表明的螺丝、螺母集合, 带有内置跟踪球或触摸板的膝上键盘算做两个设备,但带有力反馈激励器的游戏杆倒是一个设备。DirectInput API和文档中把返回数据或产生力反馈效果的设备的不一样部分都称为设备对象实例或简称对象,这两种叫法都不是最佳的。由于“对象”一词(在其它状况下)指代码对象,“实例”批COM接口的例示。在本文中,咱们尽可能避免使用这种叫法,而用更准确的“设备物”取代。咱们认为,设备物能够是任何的键、按钮、视点帽或轴等。
  另外一个更易混淆的问题是“设备”能够指物理对象(如其驱动器)也能够指DirectInput建立的DirectInputDevice对象。当讲到建立一个设备时,咱们指得到一个指向IDirectInputDevice接口的指针,以即可以用相应方法与设备驱动器通信。
  1.1.2 按钮和轴
  按钮是任何具备开和关状态的设备。DirectInput在概念上不对按钮加以区分,它们能够位于鼠标上、游戏杆上或游戏板上,还能够是键盘上的键。
  轴是设备中的一种控制而不是按钮 — 有位置而不是开、关状态的事物。严格地说,轴不是物理对象,而是由物理对象向某一方向的移动产生的一个值。游戏杆、跟踪球、鼠标球和触摸板等一个物理对象控制两个轴的值,分别称为X轴和Y轴,由于它们通常控制的是二维计算机屏幕上的指针或游戏元素。游戏杆上的减速把手或滑块只控制一个轴,它与MicrosoftIntelliMouse鼠标中的转轮是同样的。游戏杆上也可能有其它的轴,如用于控制舵方向的扭转动做等,用于驾驶、飞行摸拟等的特殊控制器依靠操纵轮、轭、踏板等操纵多个轴。
  视点帽是例外的一种状况,它不是有任何按钮或轴,后面有关游戏杆的章节中将详细讨论这一设备。应记住的重要一点是,轴与物理空间或其它事件之间不存在固定关系。
  程序上下文中设备的Z轴(尽管与三维坐标系统中一个轴的名称相同)可能与空间的第三维毫无关系,而其做用却多是控制翻页、音量或其它非空间属性。虽然已有一些成形的约定,如把X 轴与控制器的左右运动和屏幕上的水平方向相关联,但映射设备中的物理量与DirectInput中逻辑轴的关系是由设备驱动器实现的,而把这些轴与它们发挥做用的游戏概念关联起来是由程序设计者完成的 — 或给使用者以选择的机会并由他们完成。
  轴能够是绝对的或相对的。游戏杆中的X轴和Y轴天生是绝对的,由于在任何方向它们都只能移动有限的距离,他们的位置也一般是以与某一固定中心点的距离而测量的。相反,鼠标天生是一种相对的设备,由于它能够在任何方向上无限地移动,并且也设有“原点”,对它所关心的只是在上一次检测以后它沿每一个轴运行的距离。
  DirectInput能够违反天性而把任何轴看成绝对的或相对的来处理。但多数状况下把鼠标(包括转轮)处理为绝对的轴,把其它设备 — 杆、滑块、拨号盘看成相对轴处理,也就是说,对鼠标要考虑的是其运动,而其它轴考虑的则是位置。
  1.二、设置DirectInput
  简单地调用DirectInputCreate函数(见表1-1)就能够初始化DirectInput系统,此函数将返回一个指向IDirectInput接口的指针,而后能够用此接口的方法列举设备并建立设备对象,这些设备对象是DirectInput的工做部分。
  表1-1 DirectInputCreate函数
  HRESULT WINAPI DirectInputCreate(
  HINSTANCE hinst,
  DWORD dwVersion,
  LPDIRECTINPUT * lplpDirectInput,
  LPUNKNOWN punkOuter
  );
  参数 说明
  HINSTANCE hinst 建立DirectInput对象的程序或DLL的实例句柄
  DWORD dwVersin 设计程序所用的DirectInput版本号,此值一般为DIRECTINPUT_VERSION,但看到的是文本
  LPDIRECTINPUT *lplpDirectInput 新DirectInput对象的指针地址
  LPUNKNOWN pUnkOuter 必须为NULL
  注意:DirectX很是灵活地容许用户用之前的版本(实际即为版本3)进行设计,只要给DirectInputCreate函数传递一个非DIRECTINPUT_VERSION值就好了。若是传递的是OXO300,则程序在装有DirectX 3运行文件的系统中会很好地运行。但必须确保在所用结构与DirectX与定义的不一样时,使用与DirectX3兼容的结构,如DIDEVCAPS_DX3。
  简单有效的方法是在包含DINPUT.H文件前给出本身对DIRECTINPUT_VERSION的定义,如:如今不用担忧使用旧结构了,由于DINPUT.H被解释过之后,所存的新结构都奇迹般地转换成对应的早期结构,其中DIDEVCAPS与DIDEVCAPS_DX3一致,如此类推。
  提示:即便是用新版DirectInput进行设计,也可使用旧版本中的结构,用途之一是IDirectInputDevice:: GetCapabilities方法,经过传递传来DIDEVCAPS_DX3结构将省去DirectInput查询力反馈驱动器的一步,可以使速度略有提升。固然只有在对力反馈不感兴趣时才可这样作,同时不要忘记初始化dwSize成员为sizeof(DIDEVCAPS_DX3)。
  1.三、列举设备
  你通常不须要列举鼠标和键盘。即便与用户系统链接的这两种设备都不止一个(固然这种可能很小),咱们也能够经过在IDirectInput::CreateDevice中使用预约义的变量GUID_SYSMOUSE 和GUID_SYSKEYBOARD来解决。
  游戏杆则属于另外一种状况,由于不存在所谓的“系统游戏杆”。一些用户会安装多种游戏控制器的驱动程序,甚至把多个这种设备链接到系统。至少,为了得到了一个合适的设备或多个设备的GUID,要对其进行列举,或许为了使用户能进行选择还须要列举全部可用的设备。后面有关游戏杆的章节中将详细讨论这一问题。
  要列举设备须要先创建一个回调函数,而后调用EnumDevices方法,见表1-2。
  表1-2 EnumDevices方法
  HRESULT IDirectInput::EnumDevices(
  DWORD dwDevType,
  LPDIENUMCALLBACK lpCallback,
  LPVOID pvRef,
  DWORD dwFlags
  );
  参数 说明
  DWORD dwDevType DIDEVTYPE_* 值,指定要查找的设备类型,如为0则查找全部类型
  LPDIENUMCALLBACK lpCallback 回调函数地址
  LPVOID pvRef 可为任何值,典型值为一指向数据的指针,可在回调函数中使用或修改。这是传给回调函数的第二个参数
  DWORD dwFlags 对列举的更严格的限制标志。能够为(a)DIEDFL_ALLDEVICES(=0)或(b)DIEDFL_ATTACHEDONLY和DIEDFL_FORCEFEEDBACK之一或二者皆有
  EnumDevices用到的回调函数能够有许多用途。被列举的特定设备的许多信息都在DIDVICEINSTANCE结构中,此结构由DirectInput自动创建并传给回调函数,下面是回调函数如何使用这些信息的几个例子:
  1)建立此设备。此设备的实例GUID已位于DIDEVICEINSTANCE结构中,所以回调函数能够很方便地获取IDirectInputDevice接口并进行其它初始化工做。
  2)检查设备的子类型,例如,能够检查键盘子类型并对游戏功能对应的键作必要的修改(可是,若是列举键盘没有别的重要目的,则能够经过调用IDirectInputDevice::GetDeviceInfo简单地实现)。下面的例子说明了如何在回调函数中更改对键功能的分配。这里假定调用EnumDevices时已限定了列举的是键盘。
  BOOL CALLBACK DIEnumKbdProc( LPCDIDEVICEINSTANCE lpddi,
  LPVOID pvRef )
  {
  switch ( GET_DIDEVICE_SUBTYPE( GET_DIDEVICE_SUBTYPE( lpddi->dwDevType ))
  {
  case DIDEVTYPEKEYBOARD_PCXT;
  case DIDEVTYPEKEYBOARD_PCAT;         // No F11 and F12 keys. so reassign here.
  ...         break;
  // And so on. ALL the subtypes are listed in the
  // reference for the DIDEVICEINSTANCE structure.
  ...    }
  return DIENUM_CONTINUE;
  }  3)检测设备性能。假设如今正在查找有至少8个按钮的游戏杆,对列举的每种游戏杆均可以建立一个临时的IDirectInputDevice接口并调用其GetCapabilities方法,所需的信息将由此方法填入DIDEVCAPS结构的dwButtons成员中。
  4)产生一个列表框,用户可从中选择一种设备。
  1.四、设置设备
  准备用DirectInput使用设备有时有点象飞行前的检查清单,但不要把这当成杂事 — 应把它当作一种机会,一种避免错误发生,使DirectInput正常工做的机会。
  1.4.1 建立设备
  选定一种输入设备或由用户选出一种后,接下来能够为其获取一个接口。继续工做以前所需的一条基本信息是设备的实例GUID,若是没有在列举时从DirectInput传给回调函数的DIDEVICEINSTANCE结构的guidInstance成员中获取这一GUID,则须要使用预约义变量GUID_SysKeyboard或GUID_SysMouse。表1-3为 CreateDevice的方法。
  表1-3 CreateDevice方法
  HRESULT CreateDevice(
  REFGUID rguid,
  LPDIRECTINPUTDEVICE *lplpDirectInputDevice,
  LPUNKNOWN pUnkOuter
  );
  参数 说明
  REFGUID rguid 设备的实例GUID
  LPDIRECTINPUTDEVICE *lplpDirectInputDevice 指向新IDirectInputDevice接口的指针地址
  LPUNKNOWN pUnkOuter 必须为NULL
  注意:GUID是按引用传递的,这也许是由于GUID_SysKeyboard和GUID_SysMouse是全局变量而不是宏。在C中,不容许按引用传递,所以只能传递GUID的指针。
  得到IDirectInput Device接口后,一般应当即查询IDirectInputDevice2接口并代替IDirectInput接口。这一步对于力反馈编程很是重要,但即便不须要力反馈,也还可能想用IDirectInputDevice2::Poll方法,其缘由稍后再解释。下面的程序先建立设备,而后为其获取IDirectInputDevice2接口。读者能够在其中加入本身的错误处理语句。
  LPDIRECTINPUTDEVICE2 CreateDevice2( LPDIRECTINPUT lpdi, GUID* pguid )
  {
  HRESULT hr, hr2;
  LPDIRECTINPUTDEVICE lpdid1; // Temporay.
  LPDIRECTINPUTDEVICE2 lpdid2; // The keeper.
  hr = lpdi->CreateDevice( *pguid, &lpdid1, NULL );
  if ( SUCCEEDED( hr ) )
  {
  hr2 = lpdid1->QueryInterface( IID_IDirectInputDevice2,
  ( void ** )&lpdid2 );
  lpdid1->Release();
  }
  else
  {
  OutputDebugString(
  "Could not create IDirectInputDevice device" );
  return NULL;
  }
  if ( FAILED( hr2 ) )
  {
  OutputDebugString(
  "Could not create IDirectInputDevice2 device" );
  return NULL;
  }
  return lpdid2;
  }// CreateDevice2.
  1.4.2 设置数据格式
  从设备获取数据以前,必须先设置其数据格式。这只是一个在设备的状态改变后获取数据包并与已知结构进行匹配的问题。例如,从游戏杆获得的数据包显然和从鼠标那里得来的不同:格式的大小不一样,而且按照按钮和轴组合的不一样有不一样格式大小的值。表1-4中的SetDataFormat方法很是简单。
  表1-4 SetDataFormat方法
  HRESULT IDirectInputDevice::SetDataFormat(
  LPCDIDATAFORMAT lpdf
  );
  参数 说明
  LPCDIDATAFORMAT lpdf DIDATAFORMAT结构的地址
  若是读者看一下DIDATAFORMAT的文档,会看到错综复杂的说明:其中给出了有关DIOBJECTDATAFORMAT结构的一个数组的信息,说明了设备上每一按钮的数据是如何安排的。
  为何要如此复杂呢?由于DirectInput为目前还未设计出的设备留有余地,这种设备返回的数据可能与惯用结构不符。设置自定义的数据格式是至关复杂的。但幸运的是, DirectInput开发者提供了四种预约义数据格式,可用于任何标准输入设备。它们都是全局变量,都为设备描述了一种数据结构。用户所要记住的是,当设置一个预约义的数据格式时,当即会有数据返回到表1-5中所示的标准结构中。
  表1-5 预约义的全局数据格式变量
  全局DIDAFORMAT 数据结构
  c_dfDIMouse DIMOUSESTATE
  c_dfDIKeyboard char[256]
  c_dfDIJoystick(适用于多数游戏控制器) DIJOYSTATE
  c_dfDIJoystick2(适用于标准游戏控制器) DIJOYSTATE2
  设置游戏杆的数据很简单,见下面的例子:
  // lpdid is the IDirectInputDevice interface.
  lpdid->SetDataFormat( &c_dfDIJoystick );
  注意:即便不打算调用IDirectInputDevice::GetDeviceState也要为设备设置数据格式,DirectInput对这些数据格式有其它用途。
  1.4.3 获取设备信息
  如今有必要对从不一样设备和“设备对象实例”即设备物中提取信息的各类方法以及设备支持的力反馈效果作一律述,见表1-6。
  表1-6 用于获取各类信息的DirectInput方法
  方法· 父接口 目的
  GetDeviceStatus IDirectInput 用户传来设备的实例GUID,若是该设备链接到系统上,则此方法返回DI_OK(注意该设备不必定非得建立为DirectInput设备)
  EnumObjects IdirectInputDevice 列举设备中的按钮,轴和视点帽
  GetCapabilities IdirectInputDevice 结构中,包括设备是否已链接,是否支持某种力反馈参数,以及按钮、轴、视点帽的数量
  GetDeviceData IdirectInputDevice 取得源于设备的缓冲区数据
  GetDeviceInfo IdirectInputDevice 把多种信息与写DIDEVICEINSTANCE结构中,包括设备的产品和实例GUID类型、昵称及标题等
  GetDeviceState IdirectInputDevice 取得直接从设备而来的输入数据——即各按钮、轴、视点帽的当前状态
  GetObjectInfo IdirectInputDevice 把特定按钮或轴的信息写入DIDEVICEOBJECTINFO结构中,包括类型、在数据格式中的位置、昵称、对输入数据某些更秘密条目的支持等
  GetProperty IdirectInputDevice 取得设备属性 — 便可用SetProperty改变的行为,如怎样说明轴的数据等
  EnumCreatedEffectObjects IdirectInputDevice2 列举已建立的力反馈效果
  EnumEffects IdirectInputDevice2 列举设备支持的
  GetEffectInfo IdirectInputDevice2 将所支持的一种力反馈效果的各方面信息写入DIEFFECTINFO结构中
  GetforceFeedbackState IdirectInputDevice2 取得力反馈设备的信息,包括是否已加电,是否处于暂停状态等
  
  (·)全部IDirectInputDevice方法对IDiretInputDevice2和接口一样有效。
  列举设备物。建立设备后的第一步通常应该是找出有哪些可用的按钮,轴或视点帽。在示例程序DInput中,每当用户选定新设备后即进行这一操做,以使用户能选择发射键。表面上看EnumObjects方法是用于取得可用设备物清单的方法,但到底是不是这样呢?先看一看表1-7中此方法的语法。
  表1-7 EnumObjects方法
  HRESULT IDirectInputDevice::EnumObjects(
  LPDIENUMDEVICEOBJECTSCALLBACK lpCallback,
  LPVOID pvRef,
  DWORD dwFlags
  );
  参数 说明
  LPDIENUMDEVICEOBJECTSCALLBACK lpCallback 回调函数地址
  LPVOID pvRef 可为任何值,典型值为一指向数据的指针,可在回调函数中使用或修改。此值是传给回调函数的第二个参数
  DWORD dwFlags DIDFT_* 标志的组合,以使列举局限于某种类型的设备,如可产生力反馈效果的按钮
  下例的调用将列举全部按钮,包括键盘上的键:
  g_1pdid2->EnumObjects(EnumDeviceObjectsProc,NULL,DIDFT_BUTTON)  通常列举方法中的第一个参数是指向回调函数的指针,每次DirectInput发现匹配设备时都将调回此函数。中间的参数是可任意使用的pvRef,通常用做指向数据的指针,且此数据须要在回调函数中使用或修改。
  回调函数照例必须接收一种标准序列的参数并返回一个BOOL值(DIENUM_CONITINUE或DIENUM_STOP),但在其它方面则彻底由开发者决定其功能,下面是一个回调函数的例子:
  BOOL CALLBACK EnumDeviceObjectsProc( LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef )
  {
  int i;
  i = SendDlgItemMessage( g_hOptionsWnd,
  IDC_COMBO_BUTTONS,
  CB_ADDSTRING,
  0,
  ( LPARAM )( lpddoi->tszName ) );
  SendDlgItemMessage( g_hOptionsWnd,
  IDC_COMBO_BUTTONS,
  CB_SETITEMDATA,
  i,
  ( DWORD )lpddoi->dwType )
  return DIENUM_CONTINUE; // = TRUE.
  }
  IDirectInput::EnumDevices调用回调函数后,DirectInput将其列举的特定设备物的信息写入到一个结构中。在上述状况下,此结构是DIDEVICEOBJECTINSTANCE,见表1-8。
  表1-8 DIDEVICEOBJECTINSTANCE 结构
  typedef struct DIDEVICEOBJECTINSTANCE {
  DWORD dwSize;
  GUID guidType;
  DWORD dwOfs;
  DWORD dwType;
  DWORD dwFlags;
  TCHAR tszName[MAX_PATH];
  DWORD dwFFMaxForce;
  DWORD dwFFForceResolution;
  WORD wCollectionNumber;
  WORD wDesignatorIndex;
  WORD wUsagePage;
  WORD wUsage;
  DWORD dwDimension;
  WORD wExponent;
  WORD wReportId;
  } DIDEVICEOBJECTINSTANCE, *LPDIDEVICEOBJECTINSTANCE;
  typedef const DIDEVICEOBJECTINSTANCE *LPCDIDEVICEOBJECTINSTANCE;
  成员 说明
  DWORD dwSize 若是结构传给IDirectInputDevice::GetObjectInfo,则此值必须初始化为sizeof(DIDEVICEOBJECTINSTANCE)
  GUID guidType 区分对象是键、按钮仍是轴等
  DWORD dwOfs 数据格式中的首选(没必要是实际的)偏移量,对象数据可在此处取得。只有使用自定义数据格式的程序才需考虑这一点
  DWORD dwType 更多有关对象类型和对象实例标识的信息。(参见本章后部的“标识设备物”)
  DWORD dwFlags 对象的其它信息,主要用于处理对力反馈的支持
  TCHAR tszName[MAX_PATH] 对象的呢称 — 如“Dial”
  DWORD dwFFMaxForce 对象可输出的实际做用力,以牛顿为单位
  DWORD dwFFForceResolution 输出做用力的分级间隔,以万分之几表示
  WORD wCollectionNumber 保留以支持HID
  WORD wDsignatorIndex 保留以支持HID
  WORD wUsagePage 保留以支持HID
  WORD wUsage 保留以支持HID
  DWORD dwDimension 对像值所采用的尺寸单位
  WORD wExponent 与尺寸相关的指数(若是已知的话)
  WORD wExponent 保留
  稍后将用到此结构,由于它颇有用 — 但不是由于EnumObjets,为何呢?回忆一下如何给设备设置数据格式。设置完成后DirectInput对这种设备的印象就被锁定住了。例如,若是把数据格式关联到DIJOYSTATE结构,则DirectInput将准备好从多达32个按钮,6条轴,2个滑块,4个视点帽获取数据。通常说来设备上拥有的对象远少于此,但它表示的是设备能够多拥有一些。
  必需要理解实际设备与DirectInput对它的印象之间的差异。EnumObjects列举设备上的全部东西而无论DirectInput是否准备从其中获取数据。实际上,DirectInput甚至可能连此设备的数据格式都没有;即便有,也没法从数据结构中的某个特定区域识别出所列举的对象。DIDEVICEOBJECTINSTANCE中的dwOfs成员指明了DirectInput会首选地把数据放在什么地方(以免直接对驱动器提供的原始数据包进行拼揍和排序),对标准数据格式而言就是其结束的位置,但并不能确保是这样的。
  只有那些须要不停地建立自定义的数据格式的程序才会正规地调用EnumObjects。好比如今想列举真正在于数据结构中的游戏杆按钮,那么应该象下面这样进行:
  void CountJoyButtons( HWND hwnd )
  {
  DIDEVICEOBJECTINSTANCE
  DWORD i, X;
  DWORD MaxButtons = 32; // if DIJOYSTATE is our format.
  DWORD dwOfs;
  didoi.dwSize = sizeof( didoi );
  for( x = 0; x
  {
  dwOfs = DIJOFS_BUTTON( x );
  if ( SUCCEEDED( g_lpdid2->GetObjectInfo( &didoi,
  dwOfs,
  DIPH_BYOFFSET ) ) )
  {        
  i = SendDlgItemMessage ( hwnd, IDC_COMBO_BUTTONS,
  CB_ADDSTRING, 0,
  ( LPARAM ) didoi.tszName );
  SendDlgItemMessage ( hwnd, IDC_COMBO_BUTTONS,
  CB_SETITEMDATA, i, dwOfs );
  }
  }
  }// CountJoyButtons().
  此函数使用DINPUT.H中的一个宏(根据合适的数据格式)循环检测可能存在的按钮并肯定其偏移值(下一节将讲到偏移量标识问题)。而后调用IDirectInputDevice::GetObjectInfo,若是按钮存在将返回DI_OK,不然返回DIERR_OBJECTNOTFOUND。
  若是按钮存在,DirectInput将返回相关信息并存入DIDEVICEOBJECTINSTANCE结构中 — 与EnumObjects填充的结构同样。接下来CountJoyButtons获取按钮的昵称并将其填入对话框的下拉列表框中,此对话框所属的窗口的句柄就是传给上面函数那个句柄。同时它还将偏移值做为列表项的相关数据而存储,用索引使得在列表中的列表项增长或存储后能方便地取得这一偏移值。此偏移值用于标识用户选定的按钮。
  1.4.4 标识设备物
  上一节讲过按钮或其它对象能够在调用GetObjectInfo时用设备数据结构中的偏移量来标识,这是标识设备物的两种方法之一,另外一种方法是由实例号即ID标识。
  由偏移量标识。给设备设定数据格式后,每一对象都关联到此设备数据格式的特定位置处,此位置的字节偏移量是一种方便地标识对应设备物的方法。
  如今来考虑一种最简单的状况:键盘。每次读键盘状态时都会返回一个256字节的数组(称之为KeyArray)。每一键都在数组中有一个索引值,对应于从数组起点开始的偏移量,这些索引值在DINPUT.H中以DIK_* 的宏定义形式给出。如ESC键对应的字节数据在KeyArray[DIK_ESCAPE]中(第4章键盘输入中有DIK_*值的完整列表)。但DIK_ESCAPE也能够在另外的状况下标识ESC键,如如今要从源于键盘的缓冲区数据中获取一些事件的信息,这些事件各自与一单独的键有关。若是ESC键被按下,则数据包中的标识为DIK_ESCAPE(稍后再讨论有关获取数据的内容,上面只是为了说明如何用数据偏移量标识键)。
  鼠标和游戏杆数据也相似,只不过数据格式更加复杂。对鼠标而言,数据格式是DIMOUSESTATE结构,包括三个LONG(对应于每一可能的轴)和四个字节(对应于每一可能的按钮)。对应于主按钮的数据偏移量是从DIMOUSESTATE结构起点开始的rgbButtons[0]的偏移量。要得到帮助能够查DINPUT.H中的宏。
  鼠标偏移量。对游戏杆而言,根据设定的数据格式,偏移量既能够是相对于DIJOYSTATE结构起点也能够相对于DIJOYSTATE2结构起点,DINPUT.H中提供了从任一游戏杆数据结构中获取设备物偏移量的宏定义。
  DIMOFS_BUTTON0
  DIMOFS_BUTTON1
  DIMOFS_BUTTON2
  DIMOFS_BUTTON3
  DIMOFS_X
  DIMOFS_Y
  DIMOFS_Z   游戏杆偏移量
  DIJOFS_BUTTON0到DIJOFS_BUTTON31
  DIJOFS_BUTTON(n)
  DIJOFS_POV(n)
  DIJOFS_RX
  DIJOFS_RY
  DIFOFS_RZ
  DIJOFS_X
  DIJOFS_Y
  DIJOFS_Z
  DIJOFS_SLIDER(n)  注意:游戏杆按钮既能够由常量标识也能够由索引标识。如按钮0能够对应DIJOFS_BUTTON0或DIJOFS_BUTTON(0)。与键盘的状况相似,鼠标或游戏杆上按钮或轴的数据偏移量用于把存于缓冲区的输入项关联到产生这些输入的设备物上,在获取或设置属性时也能够用这些偏移量把其中的一条轴单列出来。
  由ID标识标识。设备物的另外一种方法是经过实例号(或ID)进行,它只是分配给各设备物一个序列,但不是GUID。回忆一下前面讲过的应分清DirectInput对设备的印象与由驱动器表示的设备实际组成之间的差异。设备上的每一对象都有实例号,即便数据结构中设有此对象的位置也是这样,甚至在根本未给此设备设置数据结构时也是这样。当从DIDEVICEOBJECTINSTANCE结构中获取设备物的信息时,不管是经过列举仍是经过调用IDirectInputDevice::GetObjectInfo方法进行,标识信息将返到dwType成员中。此DWORD实际包含有两条信息,低字节存储的是对象的类型(如轴或按钮),中间两个字节为实例号,能够用DIDFT_GETTYPE和DIDFT_GETINSTANCE来提取所需的信息。
  偏移量与ID:哪一个更好。多数状况下,选择偏移量仍是实例号来识别设备物是可有可无的。用诸如GetObjectInfo及SetProperty等方法时可使用其中任一种,但必需要在适当位置设置DIPH_BYOFFSET或DIPH_BYID标志以使DirectInput知道选用的是哪种数值。但要获取输入数据,则应用偏移量来标识设备物。在程序中使用偏移量系统是最简单的,只有在访问特定做用返馈设备上的只输出对象的属性时才绝对要使用ID号,由于只输出设备不能提供输入数据,所以也就没有数据偏移量。
  1.4.5 设置和获取设备属性
  设备属性可包括如下内容:
  轴如何提供数据(相对或绝对)。
  存入缓冲区的输入数据对应的缓冲区大小。
  轴的物理范围和它提供的数据之间的关系(范围、饱和度及盲区)。
  力反馈杆是否模拟自居中弹性。
  力反馈设备的增益(相似于音量控制)。  另外,制造商可为特定设备定义其它属性。表1-9介绍了SetProperty及GetProperty属性。
  表1-9 SetProperty和GepProperty属性
  HRESULT GetProperty(
  REFGUID rguidProp,
  LPDIPROPHEADER pdiph
  );
  及
  HRESULT GetProperty(
  REFGUID rguidProp,
  LPDIPROPHEADER pdiph
  );
  参数 说明
  REFGUID rguidProp 要设置或获取属性的标识,能够为DINPUT.H中的DIPROP_* 预约义值,也能够是制造者提供的GUID
  LPDIPROPHEADER pdiph 主数据结构中DIPROPHEADER结构的地址,一般是一个DIPROPDWORD或DIPROPRANGE。主结构中的其它成员含有要被设置的数据
  和别处同样,DirecX灵活的有远见的天性使得它看起来比想象得要复杂得多。因为SetProperty可用于人们目前根本没有想到的设备,因此不能让它接受标准数据结构。实际上,它能够接受任何针对要访问的属性而预约义的结构。对标准属性,能够是DIPROPDWORD(任何须要一个DWORD的属性),或DIPROPRANGE(任何须要两个LONG的属性)。但制造商能够声明任何其它类型的结构,仅需以一个DIPROPHEADER结构(见表1-10)做为其数据的第一部分,DirectInput能知道主结构的大小并标识出要设置或获取其属性的对象。
  表1-10 DIPROPHEADER结构
  typedef struct DIPROPHEADER {
  DWORD dwSize;
  DWORD dwHeaderSize;
  DWORD dwObj;
  DWORD dwHow;
  } DIPROPHEADER, *LPDIPROPHEADER;
  typedef const DIPROPHEADER *LPCDIPROPHEADER;
  成员 说明
  DWORD dwSize 必须初始化为外围结构的大小,如sizeof(DIPROPDWORD)
  DWORD dwHeaderSize 必须初始化为sizeof(DIPROPHEADER)
  DWORD dwObj 属性被访问的设备物的标识,若是包含整个设备则为O(对设定缓冲区大小而言)
  DWORD dwHow 若是包含整个设备则为DIPH_DEVIC,不然为DIPH_BYOFFSET或DIPH_BYID,说明采用哪一种系统标识设备物
  表1-11与1-12为常常与SetProperty和GetProperty一同使用的两个标准外围结构。DIPROPDWORD用于设置或获取任何须要一个DWORD数据的属性;DIPROPRANGE用于设置或获取绝对轴的范围,也能够用于访问任何使用两个LONG数据的属性。关于轴的范围将在第4章游戏杆输入中详细讨论。
  表1-11 DIPROPDWORD结构
  typedef struct DIPROPDWORD {
  DIPROPHEADER diph;
  DWORD dwData;
  } DIPROPDWORD, *LPDIPROPDWORD;
  typedef const DIPROPDWORD *LPCDIPROPDWORD;
  成员 说明
  DIPROPHEADER diph 前面进过的结构头
  DWORD dwData 数据 — 由人设置并经过SetProperty传给设备,或由Getproperty填充。典型的数据项为缓冲区大小和轴模式等,数据的意义由方法的rguidProp参数决定
  表1-12 DIPROPRANGE结构
  typedef struct DIPROPRANGE {
  DIPROPHEADER diph;
  LONG lMin; LONG lMax;
  } DIPROPRANGE, *LPDIPROPRANGE;
  typedef const DIPROPRANGE *LPCDIPROPRANGE;
  成员 说明
  DIPROPHEADER diph 结构头
  LONG lMin 最小范围值
  LONG lMax 最大范围值
  下面是用SetProperty设置X轴范围的一个例子:
  #define JOYMIN -1000
  #define JOYMAX 1000
  DIPROPRANGE diprg;
  diprg.diph.dwSize     = sizeof( diprg );
  diprg.diph.dwHeaderSize  = sizeof( diprg.diph )
  diprg.diph.dwObj     = DIJOFS_X
  diprg.diph.dwHow     = DIPH_BYOFFSET;
  diprg.lMin        = JOYMIN;
  diprg.lMax        = JOYMAX;
  if ( FAILED( g_lpdid2->SetProperty( DIPROP_RANG, &diprg.diph ) ) )
  retrurn FALSE;
  接下来要获取设备的缓冲区大小,这里DIPROPDWORD结构在声明中初始化,但其思想与上一例基本同样。
  DIPROPDWORD dipdw =
  {    // The header, which we initialize.
  {
  sizeof( DIPROPDWORD ),  // diph.dwSize      sizeof( DIPROPHEADER ),  // diph.dwHeaderSize      0,            // diph.dwObj - no particular object      DIPH_DEVICE,       // diph.dwHow - entire device    };
  // The data will go here.
  0               // dwData
  };
  HRESULT hr = g_lpdid2->GetProperty( DIPROP_BUFFERSIZE, &dipdw.diph );
  1.5.6 协做级别
  象每次使用结构时都要填充dwSize成员同样,设置协做级别也是一种使人费解的工做 — 为何非得由用户作这项工做?难道DirectInput不能作得更好吗?事实上,DirectInput很是但愿把事情作得更好,但有时也须要一些帮助。设置设备的协做级别旨在让DirectInput知道用户所但愿的应用程序与系统以及其它程序之间相互联系时有什么样的表现。在深刻讲解这一问题以前,先介绍一下程序调用IDirectInputDevice::SetCooperativeLevel时能使用的有效标志组合,见表1-13。
  表1-13 IDirectInputDevice::SetCooperativeLevel使用的标志组合
  标志 含义 适用于
  DISCL_NONEXCLUSIVE|DISCL_ABCKGROUND 其余人能够以独占或非独占模式得到设备;当前程序能够在任什么时候刻访问数据 除力反馈外的全部设备
  DISCL_NONEXCLUSIVE|DISCL_FOREGROUND 其余人能够以独占或非独占模式得到设备;当前程序只有在位于前台时才能访问数据 除力反馈外的全部设备
  DISCL_EXCLUSIVE|DISCL_BACKGROUND 其余人能够以非独占模式得到设备;当前程序能够在任什么时候刻访问数据游戏杆和力反馈设备
  DISCL_EXCLUSIVE|DISC_FOREGROUND 其余人能够以非独占模式得到设备;当前程序只有在位于前台时才能访问数据除键盘外的全部设备,对鼠标有效但禁止Windows显示光标
  须要指出的第一点是,同一时刻不能有两个程序或一个程序的两个实例以独占模式得到同一设备。这主要是出于安全考虑,它能够防止输入给某一程序的值被传给同时运行的另外一程序。对力反馈设备而言此时只能使用独占模式,上述这种特色能够防止两个程序同时输出做用效果,而若是同时输出则将产生混淆和警告。独占模式针对程序与系统间的相互做用不一样而有多个分支:
  因为Windows要求在任什么时候刻以同于独占的模式访问键盘,所以程序只能以非独占模式得到键盘。
  因为Windows要求在任什么时候刻以等同于独占的模式使用鼠标,因此当程序以独占模式得到鼠标时,Windows将不能访问鼠标。这就意味着不会再产生任何鼠标消息,光标也将消失。注意这种表现与键盘不一样,对键盘而言Windows有优先特权,因此重要的按钮组合如ALT_TAB及CTRL_ALT_DELETE老是有效。  若是考虑其表现则没有太多的理由优先选用独占模式。对鼠标而言选择独占模式为这样可有一点点好处,因以使Windows再也不有产生鼠标消息的必要。但对游戏杆而言,选择哪一种模式对表现没有影响。之前台模式得到设备意味着只有程序的主窗口位于前台时才能接收输入,对游戏和多数其它程序而言这种设置是天然的。后台模式则意味着只要程序在运行就能够接收输入,哪怕它已被最小化。想象一下一个“Smarthouse”程序,人们能够单击一个远程控制来打开车库门或开始烧烤食物,这种程序能够一直停留在Windows任务栏上,而没必要被带到前台进行工做,它是一个很好的后台模式的例子。
  另外一种使用后台模式的状况是在当一对话框处于打开状态同时要保持得到设备时。由于当属性页位于前台时,用户须要体察其效果,因此设备只能之后台模式得到。若是已设置了前台协做级别,则只要程序转为后台,它将不能访问所用的设备。对鼠标而言,当用户打开菜单或对话框,甚至程序静止在前台时都将失去访问权。这种状况只能准备在设备从新有效时再次得到。下面开始讲得到设备。
  1.4.7 得到设备
  到这里为止DirectInput设置已包含了简单地准备设备待用的一劳永逸的步骤,但在能获得数据前还必须得到该设备。得到设备便是取得它的使用权并通知DirectInput说明程序想按设定的格式接收数据。但得到设备不是一劳永逸的步骤,因为种种缘由,程序可能屡次失去对设备的获取,每当这种状况发生时,程序须要从新得到所需设备,但DirectInput不会代劳。
  假设DirectInput是一家旅游圣地豪华馆餐厅的服务生,它知道顾客喜欢坐在什么地方,是否愿意和他人用同一张餐桌,喜好什么样的鲜花摆设等。但并非任什么时候候顾客都能径直走入餐厅并坐到本身喜欢的座位上,由于有可能另外一个团体已包用了他的餐桌,或者别人刚用完离去后,餐桌将来得及收拾和按喜欢的方式设置。所以,顾客应首先找服务生登记使各类安排都准备稳当。
  找服务生登记并获准坐下和得到设备是同样的。每次得到设备时,DirectInput必须把各方面设成用户指定的方式,即清除全部缓冲区内的数据,以最有效的方式为数据传输做安排,同时给出用户选择的那个设备的属性。离开餐厅等同于调用IDirectInputDevice::Unacquire。其目的是让DirectInput知道其它程序或Windows系统如今能够以任何级别访问该设备了。
  在结束程序前有时必须有意地释放设备,最多见的状况是当鼠标被以独占模式得到时Windows系统将不能使用鼠标,这在前面已讲过。这时若是想让Windows接管鼠标的使用权进行菜单选择,就必须调用Unacquire以使Windows作它应做的事;当菜单关闭后再从新得到鼠标。DirectX SDK中的示例程序Scrawl提供了这种用法的例子,当用户右击鼠标时Scrawl释放鼠标并打开一个上下文菜单,用户能够在标准的Windows鼠标光标下进行选择。
  若是想改变除力反馈增益之外的设备属性则也须要先释放该设备。前面已提到过,程序有时会不自觉地释放设备,发生这种状况一般是因为程序之前台模式得到设备,但使用者已切换到另外一窗口,这就比如那位假想的Maitre d’被贿赂或受到威胁而离开了用户的餐桌,固然可能这个类比不很恰当。
  不管释放设备是有意的仍是不自觉的,在想从新获取数据以前都必须通知DirectInput,这可由调用IDirectInputDevice::Acquire实现。但并无简单快速的方法来检验设备是否处于未获取状态,所能做的只是检验申请接收数据时的回应是什么,IDirectInputDevice::GetDeviceState和IDirectInputDevice::GetDeviceData 方法在数据流被打断后的第一次调用时会返回DIERR_INPUTLOST。若是没有从新得到设备,接下来调用时会返回DIERR_NOTACQUIRED。收到DIERR_INPUTLOST后能够试着从新得到该设备并再取一次数据,见下面的代码段:
  getstate:
  hr = g_pMouse->GetDeviceState( sizeof( DIMOUSESTATE ), &dims );
  if ( hr == DIERR_INPUTLOST )
  {
  hr = g_pMouse->Acquire();
  if ( SUCCEEDED( hr ) ) goto getstate;
  }
  可是,想以一样的方法响应DIERR_NOTAQUIERD或其它失败消息值是不可取的。假设程序之前台协做级别使用一种设备,当使用者切换到另外一程序的窗口时,此程序就会在每次想取得数据时收到DIERR_NOTACQUIRED消息,这时任何获取该设备的尝试都是对CPU周期的浪费,并且还有产生死循环的危险。这时应响应WM_ACTIVATE消息来从新获取该设备,由于此消息代表程序又切换到前台了。
  对输入设备的访问进行管理确实颇有好处,但这须要在适当的时候进行,当假想的人走近并开枪射击时,若是用户不能疯狂地按鼠标或拍打游戏杆,那么一切就都没有意义了。因此应总保持安全,试图从新获取已得到的设备不会有什么害处;对Acquire的多余调用不会有效果。
  1.五、取得输入数据
  在讲如何真正从设备获取数据这个很是重要的问题以前,先来回顾一下在此以前应进行的步骤。
  建立设备。若是支持的设备不是鼠标或键盘,则还有须要获得一个IDirectInputDevice接口,由于至少会用到Poll方法。
  设置协做级别。
  设置数据格式。
  设置设备属性。对游戏杆而言这一步是必需的,由于不设置轴的范围就不能继续进行后面的过程。若是想用缓冲区数据,则也须要设置其属性,由于必须把缓冲区大小从0改成某个值。
  得到设备。与前面几步不一样,这一步不是一劳永逸的。   1.5.1 两类数据
  若是读者使用了旧风格的Windows输入编程方法就须要为诸如WM_KEYDOWN、WM_MOUSEMOVE等消息写处理程序。这些消息由Windows产生以响应设备中单个发生的事件,如键或按钮被按下或松开或者鼠标有了足够的移动而产生了一个中断等。
  尽管DirectInput不使用Windows消息,但它采用了相似的取得输入信息的系统,这个系统称为缓冲区数据。当设备的缓冲区大小属性被设为大于0的值之后,这一系统就开始运做。每当有输入事件发生时,都会有一个数据包被建立,但这些数据包被放在一个私有缓冲区中,而不是转换成消息的格式。在任什么时候刻均可以用IDirectInputDevice::GetDeviceData方法从那些缓冲区中取出数据包。
  读者或许也已用过相似于joyGetPosEx和GetAsyncKeyState函数,它们返回的不是离散的事件,而是整个设备或单个键的当前状态的“快照”。在DirectInput中这被称为当即数据,它存在于数据包中,数据包的格式便是为设备设置的格式,调用IDirectInputDevice::GetDeviceState方法能够取得这些数据。
  读者能够在程序中任意选择使用上述类型的数据,咱们的示例程序中使用当即数据来检测箭头键或游戏杆轴的状态,用缓冲区数据搜寻对发射键的敲击。鼠标的移动也使用了缓冲区数据,由于咱们程序中的光标响应的是鼠标的移动而不是其位置。
  从缓冲区中清除一个条目并不会改变GetDeviceState返回数据,由于它返回的老是设备的当前真实状态。就这一点而言它与标准的WindowsGetKeyboardState函数不一样,后者实际上只是跟踪被处理的键盘消息。第四、五、6 章有关鼠标、游戏杆和键盘输入将详细说明当即数据和缓冲区数据。
  1.5.2 事件通知
  DirectInput提供一种机制以通知程序设备上是否发生某事或程序是否失去了设备,用Win32 CreatEvent函数建立一个事件,而后经过把其句柄传递给IDirectInputDevice::SetEventNotification方法(见表1-14)将其关联到某个设备。
  表1-14 SetEventNotification方法
  HRESULT SetEventNotification(
  HANDLE hEvent
  );
  参数 说明
  CreateEvent 返回的事件句柄,当设备状态改变时此事件将被设置。想关闭某设备的事件通知可经过传NULL实现
  事件通知对于实时游戏并不十分有用,这时通常采用的方法是在每次循环时检测设备状态或输入数据缓冲区的内容,也能够同时对这两项进行检测。从另外一方面说,程序在鼠标或键盘输入以前不作任何有关的事能够更有效地使用处理器,使之等待事件发生而不是总在轮询各个设备(见DXSDK中的Srawl示例程序)。但对于其它设备,则不得不采用轮询的方法,缘由稍后再说明,所以检测信号并不比直接查询设备状态或缓冲区好多少。若是确实发现了使用事件通知的场合,那么在SDK参考中有大量相似于Scrawl中IDirectInputDevice::SetEventNotification的示例代码可使用。
  注意:本章和后续章节中,咱们常用“事件”一词来讲明设备状态或缓冲区里数据项发生改变的事情,如按压按钮或轴的变化等,通常它不表示本章中咱们讲到的事件对象,在使用狭义时咱们将尽可能表达清楚。
  1.5.3 轮询以获取数据
  当设备状态改变时DirectInput是如何知道的呢?某些状况下,它经过旧风格的方法即硬件中断实现,不然就必须查询驱动器以得到当前设备状态并用一些小把戏来模拟事件(广义和狭义)。全部这些都在幕后静静地运行,可是首先须要作一件事以使轮询操做发生。DirectInput不会自动轮询设备,而只在调用IDirectInputDevice::Poll方法时才这么作。该方法没有任何参数。
  调用Poll后会发生什么呢?DirectInput先检查设备是否确实须要轮询。若是不须要 — 即若是DirectInput经过硬件中断获知状态变化 — 则Poll方法当即返回。不然DirectInput检查设备的状态并取得当即数据。若是已为缓冲区数据留出了空间,则DirectInput会把这次设备的状态与上次调用时的状态相比较,并根据发现的变化建立数据包,而后它将数据包存入缓冲区中,就好象这些数据是从中断获得的同样,接下来会针对已建立的事件发出信号。
  能够经过检测DIDEVCAPS结构的DIDC_POLLEDDATAFORMAT标志来验证是否设备上的某部分须要轮询。也能够经过检测DIDEVICEOBJECTINSTANCE中的DIDOI_POLLED以获知是否设备上的特定设备物须要轮询。可是更安全经济的作法是无论有无必要都调用Poll,若是设备不须要轮询,则该方法几乎当即就返回。
  总之,在每次IDirectInputDevice::GetDeviceState或IdirectInputDevice2::GetDeviceData获取数据以前最好都调用IdirectInputDevice2::Poll,除非确知程序只用到中断驱动的设备。
  提示:若是程序在消息循环中寻找有标志的输入事件,则能够调用WaitForSingleObject、WaitForMultipleObjects或其它的一种检测事件的函数,并把超时值(dwMilliseconds)设为0,而后若是该函数返回后未找到事件对象,就调用Poll。在SDKIDirectInputDevice::SetEventNotification参考的最后一个例子中,就能够在DoGame函数内调用Poll。编程

相关文章
相关标签/搜索