输出控制利器Directlnput详解

发表于2017-11-03
评论0 1.6k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594

15.1 引言

众所周知,在普通的Windows 程序中, 用户通过键盘或者鼠标输入的消息并不是应用程序直接处理的,而是通过Windows 的消息机制转发给Windows 操作系统的。Windows 操作系统对这些消息进行响应后,再通过回调应用程序的窗口过程函数进行相应的消息处理。
这显然满足不了对于性能要求比较苛刻的游戏程序。在DirectX 中,微软为我们提供了名为Directlnput 接口对象来实现用户输入的。Directlnput 直接和硬件驱动打交道,因此处理起用户的输入来说非常迅速。
首先需要给大家说明的是,Directlnput 这套API 自Direct8 更新以来,功能己经足够完善了。所以尽管当前DirectX 的最新版本上升到了DirectX 11, Directlnput 还是DirectX 8 那个版本时代的老样子,API 的内容和功能随着最近几个版本的更迭却原封不动,名称上也保留了8 这个版本号,依然叫Directlnput 8。即目前最新版本的Directlnput,依旧是Directlnput 8 。

15.2 Directlnput 接口概述

Directlnput 作为DirectX 的组件之一,依然是一些COM 对象的集合。Directlnput 由IDirectlnput8 、IDirectlnputDevice8 IDirectlnputEffect 这3 个接口组成,这3 个接口中又分别含有各自的方法。
总的来说, 当前版本的DirectlnputAPI 中, 3 个接口, 47 个方法,组成了这个在电脑游戏开发中不可或缺的组件。
由于IDirectInput8 API 整体来说规模不大,说白了也就是3 个接口, 47 个方法, 不妨我们在文章中将它们一一列举出来,也让大家窥一窥D让ectlnput API 的全貌。

1. IDirectlnput8 接口函数一览



2. IDirectlnputDevice8 接口函数一览


3. IDirectlnputEffect 接口函数一览


其中, IDirectlnput8 作为Directlnput API 中最主要的接口, 用于初始化系统以及创建输入设备接口,Directlnput 中其他的所有的接口都需要依赖于我们的IDirectlnput8 之上,都是通过这个接口进行查询的。而DirectlnputDevice8 接口用于表示各种输入设备(如键盘、鼠标和游戏杆〉,并提供了相同的访问和控制方法。对于某些输入设备( 如游戏杆和鼠标) ,都能通过查询各自的
IDirectlnputDevice8 接口对象,得到另一个接口IDirectlnputEffect8 。而IDirectlnputEffect8 接口则用于控制设备的力反馈效果。



15.3 Directlnput 使用步骤详解

15.3.1 头文件和库文件的包含

我们首先需要注意的是,在使用Directlnput 时,需要保证我们包含了DInput.h 头文件,并且在项目属性页中已经链接了Dlnput8.lib 库文件,以防止“未解析的外部命令”系列错误。另外,dxguid.lib 也可能需要包含上。
当然,库文件我们也可以用代码动态添加:
  1. #pragma comment(lib, "dinput8.lib")     // 使用DirectInput必须包含的头文件,注意这里有8  
  2. #pragma comment(lib,"dxguid.lib")  

15.3.2 创建Directlnput 接口和设备

在Directlnput 中我们通过调用DirectlnputCreate 函数创建并初始化IDirectlnput 接口,我们可以在MSDN 中查到该函数的声明如下:
  1. HRESULT DirectInput8Create(  
  2.          HINSTANCE hinst,  
  3.          DWORD dwVersion,  
  4.          REFIID riidltf,  
  5.          LPVOID * ppvOut,  
  6.          LPUNKNOWN punkOuter  
  7. )  

  •  第一个参数, HINSTANCE 类型的hinst ,表示我们当前创建的Directlnput 的Windows 程序句柄,这个值填我们在WinMain 函数的参数中的实例句柄就可以了。
  •  第二个参数, DWORD 类型的dwVersion ,表示我们当前使用的Directlnput 版本号, 通常可以取DIRECTINPUT_VERSION 或者DIRECTINPUT_HEADER_VERSION,这两个值对应的是同一个值,为0x0800 。所以我们在这里还可以直接填0x0800 。
归根结底的话,可以通过【转到定义】大法在dinput.h 中查到有如下代码:
  1. #define DIRECTINPUT_HEADER_VERSION  0x0800  
  2. #ifndef DIRECTINPUT_VERSION  
  3. #define DIRECTINPUT_VERSION         DIRECTINPUT_HEADER_VERSION  
大体意思很清楚了吧,就是先定义一下DIRECTINPUT_HEADER_VERSION=0x0800 ,然后再说如果没有定义

DIRECTINPUT_VERSION 的话, 就定义一个DIRECT_INPUT_VERSION=DIRECT_INPUT_HEADER_VERSION。

  •  第三个参数,REFIID 类型的riidltf, 表示接口的标志,通常取IID_IDirectlnput8 就可以了。
  •  第四个参数,LPVOID 类型的 *ppvOut,用于返回我们新创建的IDirectlnput8 接口对象的指针。
  •  第五个参数, LPUNKNOWN 类型的punkOuter, 一个和COM 对象接口相关的参数,通常我们设为NULL 就可以了。
这个函数执行成功的话TINPUTVER 会返回HRESULT 类型的DI_OK,而失败的话根据不同的调用失败原因, 会返回

DIERR_BETADIRECSION, DIERR_INVALIDPARAM, DIERR_OLDDIRECTINPUTVERSION, DIERR_OUTOFMEMORY 中的一个。所以我们可以根据FAILED 宏来判断我们IDirectlnput8 接口对象是否创建成功了。
下面是一个调用的例子:

  1. //创建DirectInput接口  
  2. LPDIRECTINPUT8  g_pDirectInput      = NULL;  
  3. if (FAILED(DirectInput8Create(hInstance, 0x0800, IID_IDirectInput8, (void**)&g_pDirectInput, NULL)))  
  4.     return E_FAIL;  

这步完成之后,咱们定义的DIRECTINPUT8 接口对象g_pDirectlnput 就有了权利,新官上任了。

在IDirectlnput8 接口中包含了很多用于初始化输入设备及获得设备接口的方法。其中,常用的方法为EnumDevices 和CreateDevices 。前者EnurnDevices 用于获得输入设备的类型,而后者CreateDevices 用于为输入设备创建IDirectlnputDevice8 接口对象。
需要注意的是,系统中每一个已安装的设备都有一个系统分配的全局唯一标示符(GUID,Global Unique Identification ),从英文单词意义上就可以知道,系统中的每个设备都有着独一无二的GUID,这个GUID 又唯一标识了系统中的某某设备。就像我们每个人都有着独一无二的的身份证号码。
要使用某个设备的话, 首先我们就需要知道它的GUID。
鼠标和键盘作为我们电脑中最为重要的外设,Directlnput 对它们做了特殊对待, 给了后门, 定义了它们的GUID 分别为

GUID_Keyboard 和GUID_ SysMouse 。而对于其他的输入设备,我们就用上面提到过的EnumDevices 方法枚举出这些设备,以得到它们的GUID , 我们可以在MSDN 中查到这个方法有如下声明:

  1. HRESULT EnumDevices(  
  2.          DWORD dwDevType,  
  3.          LPDIENUMDEVICESCALLBACK lpCallback,  
  4.          LPVOID pvRef,  
  5.          DWORD dwFlags  
  6. )   
第一个参数, DWORD 类型的dwDevType ,指定我们需要枚举的设备类型。
可取的值为DI8DEVCLASS_ALL, DI8DEVCLASS_DEVICE, DI8DEVCLASS_GAMECTRL, DI8DEVCLASS_KEYBOARD, DI8DEVCLASS_POINTER 中的一个。

  •  第二个参数, LPDIENUMDEVICESCALLBACK 类型的lpCallback,用于指定一个回调函数的地址,当系统中每找到一个匹配的设备时,就会自动调用这个回调函数。
  •  第三个参数, LPVOID 类型的pvRef, 返回我们当前匹配设备的GUID 值。
  •  第四个参数, DWORD 类型的dwFlags , 指定我们枚举设备的方式。取值可以是下面的一个或者多个值: DIEDFL_ALLDEVICES, DIEDFL_ATTACHEDONLY,  DIEDFL_FORCEFEEDBACK, DIEDFL_INCLUDEALIASES, DIEDFL_INCLUDEHIDDEN,  DIEDFL_INCLUDEPHANTOMS 。

取得我们需要使用的设备的GUID 后,就可以根据这个GUID 来调用IDirectlnput8 接口的CreateDevice 方法,进而来创建设备的IDirectlnputDevice8 接口对象了。
我们可以在MSDN 中查到IDirectlnput8: : CreateDevice 方法的声明如下:
 copy

  1. HRESULT CreateDevice(  
  2.          REFGUID rguid,  
  3.          LPDIRECTINPUTDEVICE * lplpDirectInputDevice,  
  4.          LPUNKNOWN pUnkOuter  
  5. )  

  • 第一个参数,REFGUID 类型的rguid , 就是填我们上面讲到的输出设备的GUID。系统中当前使用的键盘对应GUID_SysKeyboard , 当前使用的鼠标对应GUID_SysMouse 。其他设备的话,就用我们刚刚讲过的EnurnDevices 获取一下就行了。
  • 第二个参数, LPDIRECTINPUTDEVICE 类型的 *lplpDirectlnputDevice,表示我们所创建的输入设备对象的指针地址, 可以说调用这个CreateDevice 参数就是在初始化这个参数。
  • 第三个参数, LPUNKNOWN 类型的pUnkOuter 和COM 对象的IUnknown 接口相关的一个参数, 一般我们不去管它,设为NULL 就可以了。
讲解完了, 当然得看一个调用实例。下面的代码中CreateDevice 方法的第二个参数我们填的是GUID_SysMouse , 所以我们再为系统鼠标创建一个Directlnput 设备接口对象:
  1. LPDIRECTINPUTDEVICE8    g_pKeyboardDevice   = NULL;  
  2. if (FAILED(g_pDirectInput->CreateDevice(GUID_SysKeyboard, &g_pKeyboardDevice, NULL)))  
  3.     return E_FAIL;  

15.3.3 设置数据格式

数据格式用于表示设备状态信息的存储方式, 每种设备都有一种用于读取对应数据的特定数据格式, 所以对每种设备都要区别对待。所以要使程序从设备读入数据的话, 首先我们需要告诉Directlnput 读取这种数据所采用的格式。
设置数据格式通常我们都是通过IDirectlnputDevice8 接口的SetDataFonnat 方法来做到的, 这个方法可以把设备的数据格式填充到一个DIDATAFORMAT 接口类型的对象。该方法的声明如下:
  1. HRESULT SetDataFormat(  
  2.          LPCDIDATAFORMAT lpdf  
  3. )  
  4.    
SetDataFormat 方法唯一的变量就是LPCDIDATAFORMAT 类型的lpdf, Directlnput 已经为我们准备好了一些备选的参数,下面是一个列表:


依然是一个调用实例, 设置鼠标的数据格式:

  1. g_pMouseDevice->SetDataFormat(&c_dfDIMouse);  

15.3.4 设置协作级别

在Windows 操作系统中,系统中的每个应用程序都通常会使用多个输入设备,并且同一输入设备也可能被多个应用程序同时使用。因此,需要一种方式来共享和协调应用程序对设备的访问。
在Directlnput 中,祭出的是协作级别( Cooperative Level )这套处理方式。
协作级别定义了进程与其他应用程序和操作系统共享设备的方式。设备一旦创建就需要设置它的协作级别,协作级别表示了应用程序对设备的控制权。Directlnput 的协作级别可以以两套方案来分类: 前台、后台模式和共享、独占模式。

(1 )前台模式与后台模式
其中,前台模式表示只有当窗口处于激活状态时, 才能获得设备的控制权。而当处于非激活状态时,会自动失去设备的控制权;后台模式表示可以在任何状态下获取设备,即使是在窗口处于非激活状态时。后台模式可以被任何应用程序在任何时候使用并获取设备数据。
(2 )共享模式与独占模式
共享模式表示多个应用程序可以共同使用该设备,而独占模式表示应用程序是唯一使用该设备的应用程序。这里需要注意一下,独占模式并非意味着其他应用程序不能获取输入设备状态,如果进程同时使用了后台模式与独占模式的话,当其他进程申请了独占模式的话,这个进程就会失去设备的控制权。
我们平常都是通过IDirectlnputDevice8 接口的SetCooperativeLevel 方法来设置设备的协作级别的, 我们可以在MSDN 中查到SetCooperativeLevel 的声明如下:

  1. HRESULT SetCooperativeLevel(  
  2.          HWND hwnd,  
  3.          DWORD dwFlags  
  4. )   

  • 第一个参数,HWND 类型的hwnd,显然就是填想要与当前设备相关联的窗口句柄了,且这个窗口需要属于当前进程的顶级口。
  • 第二个参数, DWORD 类型的dwFlags ,描述了当前设备的协作级别类型,也就是填我们上面讲到的前台、后台模式和共享、独占模式等一些模式的标识符,可取一个值到多个值,我们已经把取值在下表中列出来了:

注意,后台模式和独占模式不能同时选择, 用脚丫子来想都知远它们两个组合起来不符合逻辑,既然都是在后台了,还谈什么独占呢?
下面依旧是一个调用实例,将鼠标设备的协作级别设为前台、独占模式:
 copy

  1. g_pMouseDevice->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_EXCLUSIVE);  

15.3.5 设置特殊属性

设备的特殊属性包含设备的数据模式、缓冲区大小,以及设备的最小最大范围等等。Directlnput为我们提供了SetProperty 方法来设置设备的特殊属性,我们可以在MSDN 中查到这个方法有如下原型:
  1. HRESULT SetProperty(  
  2.          REFGUID rguidProp,  
  3.          LPCDIPROPHEADER pdiph  
  4. )  
这个方法平常用得不算多, 因为篇幅原因暂且先不详细讲了, 需要用的时候大家去查一下文档就可以了。

15.3.6 获取和轮询设备

先来一个常识, 在访问和使用任何输入设备之前, 首先必须获得该输入设备的控制权。其他的程序随时都可能勾心斗角,争夺并抢走对输入设备的控制权。所以我们在使用之前,往往都要重新获取一下设备的控制权,以确保权力在我们手中。
在Directlnput 中,权力的敲门砖为IDirectlnput8 接口的Acquire 方法, 我们可以在MSDN 中查到这个“权力权杖”有如下的原型:
  1. HRESULT Acquire()  
我们可以发现他简简单单, 没有参数,返回值为HRESULT。调用起来当然是非常简单: copy
  1. //获取设备控制权  
  2. g_pMouseDevice->Acquire();  
为了简明起见,我们这里没有用if 和FAILD 宏把它括起来, 进行错误处理。
另外需要注意的是,在获得输入设备的控制权之前,必须先调用IDirectlnputDevice8 接口的SetDataFormat 或者SetActionMap 方法来设置一下数据格式,否则调用Acquire 方法的话,将直接给我们返回DIERR_INVALIDPARAM 错误。
另外需要讲到的是轮询。
轮询可以准备在合适的情况下读取设备数据。因为数据可能具有临界时间的。这个轮询的原型也是非常非常地简单:
  1. HRESULT Poll()  
轮询用起来当然也是非常简单的:
  1. pDIDevice->Poll();  

15.3.7 读取设备信息

在Direct3D 应用程序中,拿到对输入设备的控制权之后,就可调用IDirectlnputDevice8 接口的GetDeviceState 方法来读取设备的数据。而为了存储设备的数据信息, 在调用该方法时,须传递一个数据缓冲区给GetDeviceState 方法,这个GetDeviceState 方法的原型我们可以在MSDN 中查到,如下:
  1. HRESULT GetDeviceState(  
  2.          DWORD cbData,  
  3.          LPVOID lpvData  
  4. )  

  • 第一个参数,DWORD 类型的cbData,指定了我们缓冲区的大小(具体是哪个缓冲区在第二个参数中〉。
  • 第二个参数, LPVOID 类型的lpvData ,表示一个获取当前设备状态的结构体的地址值。
它的数据格式和我们之前调用的IDirectlnputDevice8: :SetDataFormat 方法有着前后呼应的密切联系。下面我们通过一个表格来看看是如何联系的:


比如,我们先调用了SetDataFormat 设置了设备的数据格式为c_dfDIMouse:

  1. g_pMouseDevice->SetDataFormat(&c_dfDIMouse);  
那么我们在读取设备信息的时候调用GetDeviceState就需要把第二个参数填与dfDIMouse对应的DIMOUSESTATE 结构体的一个实例:
  1. DIMOUSESTATE dimouse;  
  2. g_pMouseDevice->GetDeviceState(sizeof(dimouse) , (LPVOID)&dimouse);  
对此,我们可以抽象出一个函数,专门对付疑难杂症,应对各种类型的设备的数据读取,而且还考虑到了设备如果丢失掉了,在合适的时间自动重新获取该设备:
  1. //-----------------------------------【Device_Read( )函数】------------------------------------  
  2. // Desc: 智能读取设备的输入数据  
  3. //--------------------------------------------------------------------------------------------------  
  4. BOOL Device_Read(IDirectInputDevice8 *pDIDevice, void* pBuffer, long lSize)   
  5. {  
  6.     HRESULT hr;  
  7.     while (true)   
  8.     {  
  9.         pDIDevice->Poll();              // 轮询设备  
  10.         pDIDevice->Acquire();           // 获取设备的控制权  
  11.         if (SUCCEEDED(hr = pDIDevice->GetDeviceState(lSize, pBuffer))) break;  
  12.         if (hr != DIERR_INPUTLOST || hr != DIERR_NOTACQUIRED) return FALSE;  
  13.         if (FAILED(pDIDevice->Acquire())) return FALSE;  
  14.     }  
  15.     return TRUE;  
  16. }  
到这一步之后,就是调用一下Device_Read 来读取数据了。调用之后,我们的键位数据其实就存在g_pKeyStateBuffer 之中了,我们接下来要做的就是用if 语句对g_pKeyStateBuffer 数组中对应的键位进行试探,看看这个键是否被按下了。如果按下,就进行相关的处理就可以了,比如:
  1. if (g_pKeyStateBuffer[DIK_A] & 0x80) fPosX -= 0.005f;  
当然,在使用完输入设备后,必须调用IDirectlnputDevice8 接口的Unacquire 方法释放设备的控制权,且需要接着调用Release 方法释放掉设备接口对象。
  1. g_pMouseDevice->Unacquire();  
  2. g_pMouseDevice->Release ();  

15.4 精炼:DirectInput使用五步曲

上面的讲解洋洋洒洒七千字,信息量有些大,为了突出重点,落实到一个字“用”上,让大家有的放矢,快速掌握Directlnput 的使用方法。在这里依旧是来一个使用几步曲的归纳, 主要以代码为载体,把上面讲的知识归纳一下。这回的Directlnput 同样是五步曲。需要说明的是,下面的代码是关于处理键盘消息的,而对于鼠标设备,需要改的地方非常少,也就是在第一步调用CreateDevice 方法时GUID 填GUID_SysKeyboard ,然后在第二步SetDataFormat 中填c_dfDIKeyboard 就可以了(相关知识上面我们已经详细讲过。对于其他设备。依然是改这两个地方,其他设备的GUID 用EnumDevices 枚举一下就知道了,下面就开始Directlnput使用五步曲的讲解;这五步曲分别是:

  • 创键Directlnput 接口和设备,简称创设备。.
  • 设置数据格式和协作级别,简称设格式。
  • 获取设备控制权,简称拿权力。
  • 获取按键情况并做响应,简称取按键。
  • 释放控制权和接口对象,简称释对象。

Directlnput 使用五步曲的载体代码,消化如下这段代码,就能将Directlnput 运用自如:

  1. //  描述:全局变量的声明  
  2. LPDIRECTINPUTDEVICE8    g_pKeyboardDevice   = NULL;  
  3. char    g_pKeyStateBuffer[256] = {0};  
  4.   
  5. // 【Direct Input 使用五步曲之一】,创键Directinput 接口和设备,简称创设备  
  6. DirectInput8Create(hInstance, 0x0800, IID_IDirectInput8, (void**)&g_pDirectInput, NULL);  
  7. g_pDirectInput->CreateDevice(GUID_SysKeyboard, &g_pKeyboardDevice, NULL);  
  8.   
  9.     // 【Direct Input 使用五步曲之二】, 设置数据格式和协作级别, 简称设格式  
  10. g_pKeyboardDevice->SetDataFormat(&c_dfDIKeyboard);  
  11. g_pKeyboardDevice->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);  
  12.   
  13.     //【Directinput 使用五步曲之三1 ,获取设备控制权,简称拿权力  
  14. g_pKeyboardDevice->Acquire();  
  15.   
  16. //【Directinput 使用五步曲之四】, 获取按键情况并做晌应,简称取按键  
  17. // 读取键盘输入  
  18. ::ZeroMemory(g_pKeyStateBuffer, sizeof(g_pKeyStateBuffer));  
  19. Device_Read(g_pKeyboardDevice, (LPVOID)g_pKeyStateBuffer, sizeof(g_pKeyStateBuffer));  
  20.   
  21. //-----------------------------------【Device_Read( )函数】------------------------------------  
  22. // Desc: 智能读取设备的输入数据  
  23. //--------------------------------------------------------------------------------------------------  
  24. BOOL Device_Read(IDirectInputDevice8 *pDIDevice, void* pBuffer, long lSize)   
  25. {  
  26.     HRESULT hr;  
  27.     while (true)   
  28.     {  
  29.         pDIDevice->Poll();              // 轮询设备  
  30.         pDIDevice->Acquire();           // 获取设备的控制权  
  31.         if (SUCCEEDED(hr = pDIDevice->GetDeviceState(lSize, pBuffer))) break;  
  32.         if (hr != DIERR_INPUTLOST || hr != DIERR_NOTACQUIRED) return FALSE;  
  33.         if (FAILED(pDIDevice->Acquire())) return FALSE;  
  34.     }  
  35.     return TRUE;  
  36. }  
  37.   
  38.  //然后就是用if 判断并做响应了,如下面一句代码  
  39. if (g_pKeyStateBuffer[DIK_A] & 0x80) fPosX -= 0.005f;  
  40.   
  41. 【Directlnput 使用五步曲之五】,释放控制权和接口对象,简称释对象  
  42. g_pKeyboardDevice->Unacquire();  
  43. SAFE_RELEASE(g_pKeyboardDevice)  
所以,上述Directlnput 使用五步曲精炼总结起来就十五个字:创设备,设格式,拿权力,取按键,释对象。

15.5 DirectInput键盘按键键值总结

与一般的Windows 应用程序相比, Directinput 处理键盘事件的方式是有很多独特之处的。首先,在我们写的游戏程序中,键盘主要并不是用于文字输入的,而是用于控制3D 世界中人物、对象的运动或者视角的变换等等。且在游戏程序中我们常常只需要知道具体是哪个键被按下,而忽略了该键所对应的字符。所以我们只需读取已按下键的扫描码就可以了。
另外,为了提高程序运行的效率, Directlnput 并非使用Windows 中的消息机制来读取键盘的状态,而是直接读取硬件的状态获取按键的扫描码的。

我们在按照流程创建好和打理好Directlnput 之后,就能在程序中不断获取从键盘输入的那些键盘数据。而在程序中,我们需要定义一个大小为256 字节的数组,其中的每一个字节都存储一个按键的状态,这样就可以保存256 个按键的状态信息了。

微软在Directlnput 中为每个键都设置了一个对应的宏,这些宏都是以DIK_ 为前缀的。例如C 键就定义为DIK_C , 主键盘数字键8 就对应DIK_8 等等,下面就是作者对Directlnput 键码做的一个总结表格, 查起来非常方便:



比如我们要检测左Alt 键是否按下,按下的话就做出响应, 就可以在上表中找到左Alt 键的键码为DIK_LALT,然后就是一句if 语句:

  1. if (g_pKeyStateBuffer(DIK_LALT] & Ox80) fPosX -= O.O5f ;  

15.6 DirectInput 鼠标按键键值总结

在通常的Windows 应用程序中,系统检测鼠标的移动并通过消息处理函数将鼠标的移动作为消息报告给用户,然而这样做的效率非常低下,因为传递给消息处理函数的每个消息首先都要走消息队列这条“官道”,需要慢悠悠地在消息队列中排队,排队完全满足不了我们对游戏即时处理消息的要求。而在Direct3D 中,咱们就可以走后门了,我们可以直接同鼠标的驱动程序进行交互, 而不用走消息队列这条慢悠悠的“官道”。
另外,我们有两种方式来跟踪鼠标的移动为:绝对模式和相对模式。在绝对模式下, 鼠标是基于某个固定点的,这个点通常是屏幕左上角,而此时返回的鼠标坐标是鼠标指针所处位置在屏幕坐标系中的坐标。
而另外一种模式,也就是相对模式下,鼠标坐标则是根据上一个己知位置到当前位置所发生的移动量来得到鼠标的坐标值的。在相对模式下得到的鼠标坐标是一个相对位置,而非绝对位置, 大家需要注意。
好了,回到正题上来。在Directlnput 中, 鼠标的移动信息我们通常都是通过一个名叫DIMOUSESTATE 结构体来记录的,我们可以在MSDN 中查到这个结构体定义如下:
  1. typedef struct DIMOUSESTATE {  
  2.     LONG lX;  
  3.     LONG lY;  
  4.     LONG lZ;  
  5.     BYTE rgbButtons[4];  
  6. } DIMOUSESTATE, *LPDIMOUSESTATE;  
  7.    
这个结构体中, lX, IY , lZ 分别记录了X 轴, Y 轴和Z 轴(鼠标滚轮的相对移动量,鼠标没移动的话,它们的值就是0 ) 。而结构体中的第四个参数rgbButtons[4]记录了四个按钮的状态信息,其中rgbButtons[0]代表鼠标左键, rgbButtons[1] 对应鼠标右键。如果需要处理支持更多按钮的鼠标的话,就去用DIMOUSESTA TE2 结构体吧。
下面我们来看看实例:
  1. DIMOUSESTATE    g_diMouseState  = {0};  
  2. // 读取鼠标输入  
  3. ::ZeroMemory(&g_diMouseState, sizeof(g_diMouseState));  
  4. Device_Read(g_pMouseDevice, (LPVOID)&g_diMouseState, sizeof(g_diMouseState));  
  5.   
  6. // 按住鼠标左键并拖动,为平移操作  
  7. static FLOAT fPosX = 0.0f, fPosY = 30.0f, fPosZ = 0.0f;  
  8. if (g_diMouseState.rgbButtons[0] & 0x80)   
  9. {  
  10.     fPosX  = g_diMouseState.lX *  0.08f;  
  11.     fPosY  = g_diMouseState.lY * -0.08f;  
  12. }  

15.7 示例程序D3Ddemo8

首先需要说明的是,本篇文章配套的程序为了更加有趣,用到了我们目前还未讲到的一点技术,就是X 文件模型的载入。源代码中X 文件模型的载入相关的代码大家如果看不懂没关系,稍后会有专门的章节进行精彩的讲解。
然后这篇文章中的demo 我们对细节部分做了升级,新加了3 个功能,它们分别是:
  •  在窗口左上角智能读取运行的机器使用的显卡名称。
  •  在窗口左下角给出了帮助信息。
  •  在窗口左上角给出了模型当前的三维坐标。
下面我们分别来对这3 个新功能进行讲解:
1. 智能读取显卡名称
第一个新的小功能,在窗口左上角智能读取运行的机器使用的显卡名称。
这个其实很简单,借助一个GetAdapterldentifier 方法就可以了。这个方法可以获取显卡的厂商类型等信息。原型如下:
  1. HRESULT GetAdapterIdentifier(  
  2.   [in]   UINT Adapter,  
  3.   [in]   DWORD Flags,  
  4.   [out]  D3DADAPTER_IDENTIFIER9 *pIdentifier  
  5. );  
注意到第三个参数类型是一个D3DADAPTER_IDENTIFIER9 结构体,这个结构体的第三个参数Description 就保存着显卡的名称的char 类型的字符串。思路也就是围绕着这个GetAdapterldentifier 方法来的,用GetAdapterldentifier 方法取得显卡的名称的char 类型的字符串,然后转换成wchar_t 类型并在显卡名称之前拼接上“ 当前显卡型号: ”字样, 然后把结果存在全局的字符串数组

g_strAdapterName 中,最后在Render 函数中用TextOut 写出来就可以了。另外注意一点,因为IDirect3D9 : :GetAdapteridentifier 是IDirect3D9 中的方法,而在我们的代码中IDirect3D9接口对象仅局部存在于Direct3D_lnit()方法中,所以我们绝大部分实现代码是在这个Direct3D_lnit()方法中完成的。具体做法咱们直接看代码,这可是每行都详细注释的代码。
首先是一个全局变量:

  1. wchar_t g_strAdapterName[60]={0};    //包含显卡名称的字符数组  
然后就是Direct3D_Init()方法中的功能实现代码:
  1. //获取显卡信息到g_strAdapterName中,并在显卡名称之前加上“当前显卡型号:”字符串  
  2.  wchar_t TempName[60]=L"当前显卡型号:";   //定义一个临时字符串,且方便了把"当前显卡型号:"字符串引入我们的目的字符串中  
  3.  D3DADAPTER_IDENTIFIER9 Adapter;  //定义一个D3DADAPTER_IDENTIFIER9结构体,用于存储显卡信息  
  4.  pD3D->GetAdapterIdentifier(0,0,&Adapter);//调用GetAdapterIdentifier,获取显卡信息  
  5.  int len = MultiByteToWideChar(CP_ACP,0, Adapter.Description, -1, NULL, 0);//显卡名称现在已经在Adapter.Description中了,但是其为char类型,我们要将其转为wchar_t类型  
  6.  MultiByteToWideChar(CP_ACP, 0, Adapter.Description, -1, g_strAdapterName, len);//这步操作完成后,g_strAdapterName中就为当前我们的显卡类型名的wchar_t型字符串了  
  7.  wcscat_s(TempName,g_strAdapterName);//把当前我们的显卡名加到“当前显卡型号:”字符串后面,结果存在TempName中  
  8.  wcscpy_s(g_strAdapterName,TempName);//把TempName中的结果拷贝到全局变量g_strAdapterName中,大功告成~  
最后就是在Direct3D_Render()函数中调用一下DrawText 显示出来了:
  1. //显示显卡类型名  
  2. g_pTextAdaperName->DrawText(NULL,g_strAdapterName, -1, &formatRect,   
  3. DT_TOP | DT_LEFT, D3DXCOLOR(1.0f, 0.5f, 0.0f, 1.0f));  

2 . 给出文字帮助信息
第二个新的小功能,在窗口左下角给出帮助信息。
其实非常简单,就是定义一些LPD3DXFONT 接口对象,然后在Objects_Init()函数中用D3DXCreateFont 创建不同的字体,最后在Direct3D_Render 全DrawText 出来就行了。
3 . 输出模型坐标
第三个新的小功能, 在窗口左上角给出了模型当前的三维坐标。

实现方法就是用swprintf_s 把世界矩阵g_matWorld 的几个分量格式化到一个静态的wchar_t类型的字符串中, 然后DrawText 出来就可以了。实现代码如下:

  1. static wchar_t strInfo[256] = {0};  
  2. swprintf_s(strInfo,-1, L"模型坐标: (%.2f, %.2f, %.2f)", g_matWorld._41, g_matWorld._42, g_matWorld._43);  
  3. g_pTextHelper->DrawText(NULL, strInfo, -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(135,239,136,255));  

还有一点,因为考虑到咱们的Direct3D_Render() 函数中的代码随着讲解的不断深入,代码越来越多,越来越杂, 越来越乱。所以我们给它配了一个搭档Direct3D_Update(), 不是即时渲染代码但是需要即时调用的,如按键后坐标的更改,按键后填充模式的更改等等相关的代码, 都放在Direct3D_Update()中了,这样就给Direct3D_ Render()绘制函数减了负,看起来更加清晰。
因为也是即时调用, 所以Direct3D_Update()在消息循环中与Direct3D_Render()平起平坐:
  1.       //消息循环过程  
  2. MSG msg = { 0 };  //初始化msg  
  3. while( msg.message != WM_QUIT )         //使用while循环  
  4. {  
  5.     if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )   //查看应用程序消息队列,有消息时将队列中的消息派发出去。  
  6.     {  
  7.         TranslateMessage( &msg );       //将虚拟键消息转换为字符消息  
  8.         DispatchMessage( &msg );        //该函数分发一个消息给窗口程序。  
  9.     }  
  10.     else  
  11.     {  
  12.         Direct3D_Update(hwnd);          //调用更新函数,进行画面的更新  
  13.         Direct3D_Render(hwnd);          //调用渲染函数,进行画面的渲染      
  14.     }  
  15. }  
最后一点, Directlnput 使用五步曲的第四步, 即获取按键状态并进行响应就是在Direct3D_Update 中实现的:
  1. //-----------------------------------【Direct3D_Update( )函数】--------------------------------  
  2. //  描述:不是即时渲染代码但是需要即时调用的,如按键后的坐标的更改,都放在这里  
  3. //--------------------------------------------------------------------------------------------------  
  4. void    Direct3D_Update( HWND hwnd)  
  5. {  
  6.     // 获取键盘消息并给予设置相应的填充模式    
  7.     if (g_pKeyStateBuffer[DIK_1]    & 0x80)         // 若数字键1被按下,进行实体填充    
  8.         g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_SOLID);  
  9.     if (g_pKeyStateBuffer[DIK_2]    & 0x80)         // 若数字键2被按下,进行线框填充    
  10.         g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);    
  11.   
  12.     // 读取鼠标输入  
  13.     ::ZeroMemory(&g_diMouseState, sizeof(g_diMouseState));  
  14.     Device_Read(g_pMouseDevice, (LPVOID)&g_diMouseState, sizeof(g_diMouseState));  
  15.   
  16.     // 读取键盘输入  
  17.     ::ZeroMemory(g_pKeyStateBuffer, sizeof(g_pKeyStateBuffer));  
  18.     Device_Read(g_pKeyboardDevice, (LPVOID)g_pKeyStateBuffer, sizeof(g_pKeyStateBuffer));  
  19.   
  20.   
  21.     // 按住鼠标左键并拖动,为平移操作  
  22.     static FLOAT fPosX = 0.0f, fPosY = 30.0f, fPosZ = 0.0f;  
  23.     if (g_diMouseState.rgbButtons[0] & 0x80)   
  24.     {  
  25.         fPosX  = g_diMouseState.lX *  0.08f;  
  26.         fPosY  = g_diMouseState.lY * -0.08f;  
  27.     }  
  28.   
  29.     //鼠标滚轮,为观察点收缩操作  
  30.     fPosZ  = g_diMouseState.lZ * 0.02f;  
  31.   
  32.     // 平移物体  
  33.     if (g_pKeyStateBuffer[DIK_A] & 0x80) fPosX -= 0.005f;  
  34.     if (g_pKeyStateBuffer[DIK_D] & 0x80) fPosX  = 0.005f;  
  35.     if (g_pKeyStateBuffer[DIK_W] & 0x80) fPosY  = 0.005f;  
  36.     if (g_pKeyStateBuffer[DIK_S] & 0x80) fPosY -= 0.005f;  
  37.   
  38.   
  39.     D3DXMatrixTranslation(&g_matWorld, fPosX, fPosY, fPosZ);  
  40.   
  41.   
  42.     // 按住鼠标右键并拖动,为旋转操作  
  43.     static float fAngleX = 0.15f, fAngleY = -(float)D3DX_PI ;  
  44.     if (g_diMouseState.rgbButtons[1] & 0x80)   
  45.     {  
  46.         fAngleX  = g_diMouseState.lY * -0.01f;  
  47.         fAngleY  = g_diMouseState.lX * -0.01f;  
  48.     }  
  49.     // 旋转物体  
  50.     if (g_pKeyStateBuffer[DIK_UP]    & 0x80) fAngleX  = 0.005f;  
  51.     if (g_pKeyStateBuffer[DIK_DOWN]  & 0x80) fAngleX -= 0.005f;  
  52.     if (g_pKeyStateBuffer[DIK_LEFT]  & 0x80) fAngleY -= 0.005f;  
  53.     if (g_pKeyStateBuffer[DIK_RIGHT] & 0x80) fAngleY  = 0.005f;  
  54.   
  55.   
  56.     D3DXMATRIX Rx, Ry;  
  57.     D3DXMatrixRotationX(&Rx, fAngleX);  
  58.     D3DXMatrixRotationY(&Ry, fAngleY);  
  59.   
  60.     //得到最终的矩阵并设置  
  61.     g_matWorld = Rx * Ry * g_matWorld;//得到最终的矩阵  
  62.     g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_matWorld);//设置世界矩阵  
  63.     Matrix_Set();  
  64. }  
运行这个程序,我们会得到如下的非常可爱的程序,可以用鼠标和键盘旋转地观察这个可爱的动漫人物。
我们可以通过按住鼠标左键或者右键并拖动鼠标或者按键盘上的W 、S 、A 、D 以及上、下、左、右方向键来调整视角,以各个方位观察这个可爱的动漫人物的三维模型。



15.8 手把手封装DirectInput到类中

首先,希望大家在学习抽象和封装这套Directlnput API的过程中,在这区区一百来行代码的字里行间里,能领悟到那些只能意会不可言传的宝贵编程思想。
对某某内容进行抽象和封装这种思想大家在正式的游戏开发中会经常碰到。其实说白了,游戏引擎就是在做这类工作,把很多内容很多接口都封装在一个个类中,我们要使用游戏引擎的话,直接用已经封装好的类和接口就可以了。
而我们在游戏开发过程中, 经常也是把某些相似度很高或者功能性很明确的内容封装在类中,方便后续使用的调用,也把功能都模块化了,显得步步为营,有章可循。比如后面我们还会介绍把一个摄像机封装在类中,把地形系统封装在一个类中,把天空盒系统封装在一个类中,把粒子系统封装在一个类中,把处理网格模型的代码封装在一个类中等等。
好了,下面开始介绍封装的实现细节吧。作者每次在写一个类的时候,都是先按照这个类的属性,看看有哪些成员变量需要定义,然后围绕着这些成员变量去定义一些函数,最后再检查一下除了围绕着成员变量的函数之外,还有没有额外需要定义的函数,这样函数大体的框架就打好了。最后补充一下构造函数和析构函数中的代码,实现一下函数的具体代码,增增减减相关代码。通过这
个四步曲流程, 一个类就写好了。
而对于本次需要写的DlnputClass 类,先复习一下Directlnput 使用五步曲,再经过如下一番思考:
对于这个DlnputClass 类,首先肯定要一个指向IDirectlnput8 接口的指针m_pDirectlnput , 然后是键盘设备对m_KeyboardDevice,存储键盘内容的字符数组m_ keyBuffer[256],鼠标设备对象m_MouseDevice ,最后是用于存储鼠标坐标和键值相关内容的结构体m_MouseState 。嗯,成员变量就是这么多,然后成员函数方面,构造函数析构函数先显式地写出来, Directlnput 初始化函数要有吧,我们取名为Init;然后不断获取输入消息、设备丢失了重新获取(俗话说“断线重连”)的函数得有吧,我们取名为Getlnput , 然后判断键盘按键按下的函数得有吧, 我们取名为IsKeyDown 。键盘按键判断有了, 鼠标按键的判断自然不能少,lsMouseButtonDown 函数就出来了。然后返回鼠标在X 轴、Y 轴的移动量的函数MouseDX 和MouseDY 也可以有,最后,负责鼠标滚轮滚动的检测的MouseDZ 自然也不能少。好像就不缺什么了,嗯, 敲键盘开始写吧。
于是,我们就可以写出这个DlnputClass 类的轮廓如下:

  1. //DInputClass类定义开始  
  2. class DInputClass  
  3. {  
  4. private:  
  5.     IDirectInput8       * m_pDirectInput;   //IDirectInput8接口对象  
  6.     IDirectInputDevice8 * m_KeyboardDevice;  //键盘设备接口对象  
  7.     char            m_keyBuffer[256];       //用于键盘键值存储的数组  
  8.   
  9.     IDirectInputDevice8 *m_MouseDevice;      //鼠标设备接口对象  
  10.     DIMOUSESTATE        m_MouseState;           //用于鼠标键值存储的一个结构体  
  11.   
  12. public:  
  13.     HRESULT         Init( HWND hWnd,HINSTANCE hInstance,DWORD keyboardCoopFlags, DWORD mouseCoopFlags ); //初始化DirectInput键盘及鼠标输入设备  
  14.     void        GetInput();   //用于获取输入信息的函数  
  15.     bool        IsKeyDown(int iKey);   //判断键盘上某键是否按下  
  16.       
  17.     bool        IsMouseButtonDown(int button);  //判断鼠标按键是否被按下  
  18.     float       MouseDX();   //返回鼠标的X轴坐标值  
  19.     float       MouseDY();  //返回鼠标的Y轴坐标值  
  20.     float       MouseDZ();  //返回鼠标的Z轴坐标值  
  21.   
  22.   
  23. public:  
  24.     DInputClass(void);      //构造函数  
  25.     ~DInputClass(void); //析构函数  
  26. };  

类的轮廓出来了,我们下面就来看看成员函数的具体实现。
首先,构造函数和析构函数的实现代码。推荐大家养成一个好的习惯,在构造函数中对成员变量都附一个初值,而析构函数中记得COM 对象和指针,该释放的都要释放掉。
我们看看DlnputClass 类中有哪些成员变量,然后逐个在构造函数中列出来, int 类型和他的亲戚就赋0 ,指针就赋NULL ,如果是数组就ZeroMemory 一下,构造函数就写好了,代码如下:
  1. //-----------------------------------------------------------------------------  
  2. // Desc: 构造函数  
  3. //-----------------------------------------------------------------------------  
  4. DInputClass::DInputClass()  
  5. {  
  6.     m_pDirectInput = NULL;  
  7.     m_KeyboardDevice = NULL;  
  8.     ZeroMemory(m_keyBuffer,sizeof(char)*256);  
  9.     m_MouseDevice= NULL;  
  10.     ZeroMemory(&m_MouseState, sizeof(m_MouseState));  
  11. }  
然后,我们实现lnit 函数,用于完成Directlnput 初始化的四步曲,其中我们完成了键盘和鼠标的双双初始化。注意这里我们需要把WinMain 函数中的HWND 和HINSTANCE 这两个参数传进来,因为在Directlnput 初始化中需要用到这两个参数的,这样才能确保我们的Directlnput 与主窗口的关联。然后我们还设置了两个参数keyboardCoopFlags 和mouseCoopFlags ,这样我们可以在初始化时在外部自定义鼠标和键盘的协作级别了。lnit 函数的实现代码就是如下:
  1. //-----------------------------------------------------------------------------  
  2. // Name:DInputClass::Init()  
  3. // Desc: 初始化DirectInput键盘及鼠标输入设备  
  4. //-----------------------------------------------------------------------------  
  5. HRESULT DInputClass::Init( HWND hWnd,HINSTANCE hInstance,DWORD keyboardCoopFlags, DWORD mouseCoopFlags )  
  6. {  
  7.     HRESULT hr;  
  8.     //初始化一个IDirectInput8接口对象  
  9.     HR(DirectInput8Create( hInstance, DIRECTINPUT_VERSION,   
  10.         IID_IDirectInput8,(void**)&m_pDirectInput,NULL ));  
  11.   
  12.     //进行键盘设备的初始化  
  13.     HR( m_pDirectInput->CreateDevice( GUID_SysKeyboard, &m_KeyboardDevice, NULL ));  
  14.     HR( m_KeyboardDevice->SetCooperativeLevel( hWnd, keyboardCoopFlags));  
  15.     HR( m_KeyboardDevice->SetDataFormat( &c_dfDIKeyboard ));  
  16.     HR( m_KeyboardDevice->Acquire( ));  
  17.     HR( m_KeyboardDevice->Poll( ));  
  18.   
  19.     //进行鼠标设备的初始化  
  20.     HR( m_pDirectInput->CreateDevice( GUID_SysMouse, &m_MouseDevice, NULL ));  
  21.     HR( m_MouseDevice->SetCooperativeLevel( hWnd,mouseCoopFlags));  
  22.     HR( m_MouseDevice->SetDataFormat( &c_dfDIMouse ));  
  23.     HR( m_MouseDevice->Acquire( ));  
  24.     HR( m_KeyboardDevice->Poll( ));  
  25.   
  26.     return S_OK;  
  27. }  
初始化完成, 下面就是读取鼠标和键盘消息了。对应之前我们使用的全局函数Read_Device(),本次我们定义了一个名为Getlnput()的函数,它的内部实现思想和代码和Read_Device()基本一致,也就是不断对设备进行获取,设备丢失就重新获取, “断线重连” :
  1. //-----------------------------------------------------------------------------  
  2. // Name:DInputClass::GetInput()  
  3. // Desc: 用于获取输入信息的函数  
  4. //-----------------------------------------------------------------------------  
  5. void DInputClass::GetInput()  
  6. {  
  7.     HRESULT hr = m_KeyboardDevice->GetDeviceState(sizeof(m_keyBuffer), (void**)&m_keyBuffer);   
  8.     //获取键盘输入消息  
  9.     if(hr)  
  10.     {  
  11.         m_KeyboardDevice->Acquire();    
  12.         m_KeyboardDevice->GetDeviceState( sizeof(m_keyBuffer),(LPVOID)m_keyBuffer );  
  13.     }  
  14.   
  15.     hr = m_MouseDevice->GetDeviceState(sizeof(DIMOUSESTATE), (void**)&m_MouseState);   
  16.     //获取鼠标输入消息  
  17.     if(hr)  
  18.     {  
  19.         m_MouseDevice->Acquire();  
  20.         m_MouseDevice->GetDeviceState( sizeof(DIMOUSESTATE), (void**)&m_MouseState);  
  21.     }  
  22. }  
剩下的几个函数就好说了,就是对键盘值进行判断,对鼠标按键进行判断,以及返回鼠标指针的当前坐标, 实现代码如下:
  1. //-----------------------------------------------------------------------------  
  2. // Name:DInputClass::IsKeyDown()  
  3. // Desc: 判断键盘上某个键是否按下  
  4. //-----------------------------------------------------------------------------  
  5. bool DInputClass::IsKeyDown(int iKey)  
  6. {  
  7.     if(m_keyBuffer[iKey] & 0x80)  
  8.         return true;  
  9.     else  
  10.         return false;  
  11. }  
  12.   
  13.   
  14. //-----------------------------------------------------------------------------  
  15. // Name:DInputClass::IsMouseButtonDown()  
  16. // Desc: 判断鼠标上某键是否按下  
  17. //-----------------------------------------------------------------------------  
  18. bool DInputClass::IsMouseButtonDown(int button)  
  19. {  
  20.     return (m_MouseState.rgbButtons[button] & 0x80) != 0;  
  21. }  
  22.   
  23. //-----------------------------------------------------------------------------  
  24. // Name:DInputClass::MouseDX  
  25. // Desc: 返回鼠标指针的X轴坐标值  
  26. //-----------------------------------------------------------------------------  
  27. float DInputClass::MouseDX()  
  28. {  
  29.     return (float)m_MouseState.lX;  
  30. }  
  31.   
  32. //-----------------------------------------------------------------------------  
  33. // Name:DInputClass::MouseDY  
  34. // Desc: 返回鼠标指针的Y轴坐标值  
  35. //-----------------------------------------------------------------------------  
  36. float DInputClass::MouseDY()  
  37. {  
  38.     return (float)m_MouseState.lY;  
  39. }  
  40.   
  41. //-----------------------------------------------------------------------------  
  42. // Name:DInputClass::MouseDZ  
  43. // Desc: 返回鼠标指针的Z轴坐标值(滚轮)  
  44. //-----------------------------------------------------------------------------  
  45. float DInputClass::MouseDZ()  
  46. {  
  47.     return (float)m_MouseState.lZ;  
  48. }  
当然,不要忘了在析构函数中需要释放掉COM 接口对象和指针:
  1. //-----------------------------------------------------------------------------  
  2. // Desc: 析构函数  
  3. //-----------------------------------------------------------------------------  
  4. DInputClass::~DInputClass(void)  
  5. {  
  6.     if(m_KeyboardDevice != NULL)  
  7.         m_KeyboardDevice->Unacquire();  
  8.     if(m_MouseDevice != NULL)  
  9.         m_MouseDevice->Unacquire();  
  10.     SAFE_RELEASE(m_KeyboardDevice);  
  11.     SAFE_RELEASE(m_MouseDevice);  
  12.     SAFE_RELEASE(m_pDirectInput);  
  13. }  
这样, Directlnput 就被我们封装在了一个类中了。


15.9 封装好的DirectInput类的使用

定义一个类对象:
  1. DInputClass*    g_pDInput = NULL;         //一个DInputClass类的指针  

在适当的地方进行初始化:
  1. //进行DirectInput类的初始化  
  2. g_pDInput = new DInputClass();  
  3. g_pDInput->Init(hwnd,hInstance,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);  
再把进行输入数据读取的函数lnit 放在适当的地方( Update 函数中) :
  1. //使用DirectInput类读取数据  
  2. g_pDInput->GetInput();  
针对要判断的技键,进行一通IsKeyDown , IsMouseButtonDown , MouseDX, MouseDY, MouseDZ 的调用就OK 了。比如:
  1.        // 按住鼠标左键并拖动,为平移操作  
  2. static FLOAT fPosX = 0.0f, fPosY = 0.0f, fPosZ = 0.0f;  
  3.   
  4. if (g_pDInput->IsMouseButtonDown(0))   
  5. {  
  6.     fPosX  = (g_pDInput->MouseDX())*  0.08f;  
  7.     fPosY  = (g_pDInput->MouseDY()) * -0.08f;  
  8. }  
另外再提一点,如果准备把Directlnput 封装在类中,且协作级别需要取到前台模式或者独占模式的话,则Directlnput 的初始化最好放在UpdateWindow(hwnd)之后进行。不然因为前台模式或者独占模式的霸道特性,可能引起hwnd 的访问出问题,从而造成Directlnput 甚至Direct3D 初始化的失败,内存溢出。

15.10 示例程序D3Ddemo9

我们刚刚封装完Directlnput 到类中,就来接着这个示例程序来试一下我们亲手写的这个类的锋芒。
因为从本篇文章的示例程序开始,我们就开始涉及到对某些功能用类进行封装了,所以之后的源代码就不单单是一个面向过程的main .cpp 文件了,而是配套着有了对应类的头文件以及源文件。
我们另外写的一个D3DUtil.h 文件,这个D3DUtil.h 主要用于公共辅助宏的定义。首先来看一下它的实现代码:
  1. //=============================================================================  
  2. // Desc: D3DUtil.h头文件,用于公共辅助宏的定义   
  3. //=============================================================================  
  4.   
  5. #pragma once  
  6.   
  7.   
  8. #ifndef HR  
  9. #define HR(x)    { hr = x; if( FAILED(hr) ) { return hr; } }         //自定义一个HR宏,方便执行错误的返回  
  10. #endif  
  11.   
  12. #ifndef SAFE_DELETE                   
  13. #define SAFE_DELETE(p)       { if(p) { delete (p);     (p)=NULL; } }       //自定义一个SAFE_RELEASE()宏,便于指针资源的释放  
  14. #endif      
  15.   
  16. #ifndef SAFE_RELEASE              
  17. #define SAFE_RELEASE(p)      { if(p) { (p)->Release(); (p)=NULL; } }     //自定义一个SAFE_RELEASE()宏,便于COM资源的释放  
  18. #endif  
类DlnputClass 的代码基本上在上文已经贴出了,这里就不再贴出占篇幅。
接下来看一下核心代码main.cpp 需要修改的地方,主要就是在WinMain 函数中加入DlnputClass 类的初始化相关代码,下面贴出的代码已经省略部分之前己经贴出过无数次的窗口创建相关的代码, 但保留了注释,以方便大家理解。
  1. //-----------------------------------【WinMain( )函数】--------------------------------------  
  2. //  描述:Windows应用程序的入口函数,我们的程序从这里开始  
  3. //------------------------------------------------------------------------------------------------  
  4. int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd)  
  5. {  
  6.   
  7.     //【1】窗口创建四步曲之一:开始设计一个完整的窗口类  
  8.     //【2】窗口创建四步曲之二:注册窗口类  
  9.     //【3】窗口创建四步曲之三:正式创建窗口  
  10.   
  11.     //Direct3D资源的初始化,调用失败用messagebox予以显示  
  12.     if (!(S_OK==Direct3D_Init (hwnd,hInstance)))  
  13.     {  
  14.         MessageBox(hwnd, _T("Direct3D初始化失败~!"), _T("浅墨的消息窗口"), 0); //使用MessageBox函数,创建一个消息窗口   
  15.     }  
  16.   
  17.     //【4】窗口创建四步曲之四:窗口的移动、显示与更新  
  18.     MoveWindow(hwnd,250,80,WINDOW_WIDTH,WINDOW_HEIGHT,true);        //调整窗口显示时的位置,使窗口左上角位于(250,80)处  
  19.     ShowWindow( hwnd, nShowCmd );    //调用ShowWindow函数来显示窗口  
  20.     UpdateWindow(hwnd);                     //对窗口进行更新,就像我们买了新房子要装修一样  
  21.   
  22.     //进行DirectInput类的初始化  
  23.     g_pDInput = new DInputClass();  
  24.     g_pDInput->Init(hwnd,hInstance,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);  
  25.   
  26.     //【5】消息循环过程  
  27.     //【6】窗口类的注销  
  28.     UnregisterClass(L"ForTheDreamOfGameDevelop", wndClass.hInstance);  //程序准备结束,注销窗口类  
  29.     return 0;    
  30. }  

然后,我们在Direct3D_Update 中加入相关按键的检测与处理代码。

  1. //-----------------------------------【Direct3D_Update( )函数】--------------------------------  
  2. //  描述:不是即时渲染代码但是需要即时调用的,如按键后的坐标的更改,都放在这里  
  3. //--------------------------------------------------------------------------------------------------  
  4. void    Direct3D_Update( HWND hwnd)  
  5. {  
  6.     //使用DirectInput类读取数据  
  7.     g_pDInput->GetInput();  
  8.   
  9.     // 按住鼠标左键并拖动,为平移操作  
  10.     static FLOAT fPosX = 0.0f, fPosY = 0.0f, fPosZ = 0.0f;  
  11.       
  12.     if (g_pDInput->IsMouseButtonDown(0))   
  13.     {  
  14.         fPosX  = (g_pDInput->MouseDX())*  0.08f;  
  15.         fPosY  = (g_pDInput->MouseDY()) * -0.08f;  
  16.     }  
  17.   
  18.     //鼠标滚轮,为观察点收缩操作  
  19.     fPosZ  = (g_pDInput->MouseDZ())* 0.02f;  
  20.   
  21.     // 平移物体  
  22.     if (g_pDInput->IsKeyDown(DIK_A)) fPosX -= 0.005f;  
  23.     if (g_pDInput->IsKeyDown(DIK_D)) fPosX  = 0.005f;  
  24.     if (g_pDInput->IsKeyDown(DIK_W)) fPosY  = 0.005f;  
  25.     if (g_pDInput->IsKeyDown(DIK_S)) fPosY -= 0.005f;  
  26.   
  27.     D3DXMatrixTranslation(&g_matWorld, fPosX, fPosY, fPosZ);  
  28.   
  29.     // 按住鼠标右键并拖动,为旋转操作  
  30.     static float fAngleX = 0.0f, fAngleY =0.0f;  
  31.       
  32.     if (g_pDInput->IsMouseButtonDown(1))   
  33.     {  
  34.         fAngleX  = (g_pDInput->MouseDY())* -0.01f;  
  35.         fAngleY  = (g_pDInput->MouseDX()) * -0.01f;  
  36.     }  
  37.     // 旋转物体  
  38.     if (g_pDInput->IsKeyDown(DIK_UP)) fAngleX  = 0.005f;  
  39.     if (g_pDInput->IsKeyDown(DIK_DOWN)) fAngleX -= 0.005f;  
  40.     if (g_pDInput->IsKeyDown(DIK_LEFT)) fAngleY -= 0.005f;  
  41.     if (g_pDInput->IsKeyDown(DIK_RIGHT)) fAngleY  = 0.005f;  
  42.   
  43.   
  44.     D3DXMATRIX Rx, Ry;  
  45.     D3DXMatrixRotationX(&Rx, fAngleX);  
  46.     D3DXMatrixRotationY(&Ry, fAngleY);  
  47.   
  48.     //得到最终的矩阵并设置  
  49.     g_matWorld = Rx * Ry * g_matWorld;//得到最终的矩阵  
  50.     g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_matWorld);//设置世界矩阵  
  51. }  
然后我们看一看Direct3D_Render()函数中的代码:
  1. //-----------------------------------【Direct3D_Render( )函数】-------------------------------  
  2. //  描述:使用Direct3D进行渲染  
  3. //--------------------------------------------------------------------------------------------------  
  4. void Direct3D_Render(HWND hwnd)  
  5. {  
  6.   
  7.     //--------------------------------------------------------------------------------------  
  8.     // 【Direct3D渲染五步曲之一】:清屏操作  
  9.     //--------------------------------------------------------------------------------------  
  10.     g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(100, 100, 100), 1.0f, 0);  
  11.   
  12.     //定义一个矩形,用于获取主窗口矩形  
  13.     RECT formatRect;  
  14.     GetClientRect(hwnd, &formatRect);  
  15.       
  16.     //--------------------------------------------------------------------------------------  
  17.     // 【Direct3D渲染五步曲之二】:开始绘制  
  18.     //--------------------------------------------------------------------------------------  
  19.     g_pd3dDevice->BeginScene();                     // 开始绘制  
  20.   
  21.     // 绘制网格  
  22.     for (DWORD i = 0; i < g_dwNumMtrls; i )  
  23.     {  
  24.         g_pd3dDevice->SetMaterial(&g_pMaterials[i]);  
  25.         g_pd3dDevice->SetTexture(0, g_pTextures[i]);  
  26.         g_pMesh->DrawSubset(i);  
  27.     }  
  28.   
  29.             //在窗口右上角处,显示每秒帧数  
  30.             int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() );  
  31.             g_pTextFPS->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_RGBA(0,239,136,255));  
  32.   
  33.             //显示显卡类型名  
  34.             g_pTextAdaperName->DrawText(NULL,g_strAdapterName, -1, &formatRect,   
  35.                 DT_TOP | DT_LEFT, D3DXCOLOR(1.0f, 0.5f, 0.0f, 1.0f));  
  36.   
  37.             // 输出绘制信息  
  38.              formatRect.top = 30;  
  39.             static wchar_t strInfo[256] = {0};  
  40.             swprintf_s(strInfo,-1, L"模型坐标: (%.2f, %.2f, %.2f)", g_matWorld._41, g_matWorld._42, g_matWorld._43);  
  41.             g_pTextHelper->DrawText(NULL, strInfo, -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(135,239,136,255));  
  42.   
  43.             // 输出帮助信息  
  44.             formatRect.left = 0,formatRect.top = 380;  
  45.             g_pTextInfor->DrawText(NULL, L"控制说明:", -1, &formatRect,   
  46.                 DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(235,123,230,255));  
  47.             formatRect.top  = 35;  
  48.             g_pTextHelper->DrawText(NULL, L"    按住鼠标左键并拖动:平移模型", -1, &formatRect,   
  49.                 DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255));  
  50.             formatRect.top  = 25;  
  51.             g_pTextHelper->DrawText(NULL, L"    按住鼠标右键并拖动:旋转模型", -1, &formatRect,   
  52.                 DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255));  
  53.             formatRect.top  = 25;  
  54.             g_pTextHelper->DrawText(NULL, L"    滑动鼠标滚轮:拉伸模型", -1, &formatRect,   
  55.                 DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255));  
  56.             formatRect.top  = 25;  
  57.             g_pTextHelper->DrawText(NULL, L"    W、S、A、D键:平移模型 ", -1, &formatRect,   
  58.                 DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255));  
  59.             formatRect.top  = 25;  
  60.             g_pTextHelper->DrawText(NULL, L"    上、下、左、右方向键:旋转模型 ", -1, &formatRect,   
  61.                 DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255));  
  62.             formatRect.top  = 25;  
  63.             g_pTextHelper->DrawText(NULL, L"    ESC键 : 退出程序", -1, &formatRect,   
  64.                 DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255));  
  65.   
  66.     //--------------------------------------------------------------------------------------  
  67.     // 【Direct3D渲染五步曲之四】:结束绘制  
  68.     //--------------------------------------------------------------------------------------  
  69.     g_pd3dDevice->EndScene();                       // 结束绘制  
  70.     //--------------------------------------------------------------------------------------  
  71.     // 【Direct3D渲染五步曲之五】:显示翻转  
  72.     //--------------------------------------------------------------------------------------  
  73.     g_pd3dDevice->Present(NULL, NULL, NULL, NULL);  // 翻转与显示  
  74.        
  75. }  
我们可以发现Direct3D_Render()函数中并没有矩阵设置相关的代码,因为在消息循环中,每次调用完Direct3D_Update()函数,马上就调用Direct3D_Render() 函数,所以我们在Direct3D_Update()函数的结尾对接下来将要绘制的物体的世界矩阵进行设置,就相当于是在渲染之前进行了要渲染的物体的矩阵的设置。
最后看一下运行截图,这次的demo 中载入了一个非常酷的变形金刚的模型,我们可以用鼠标和键盘来调整视角, 全方位地观察这个非常酷的“擎天柱” 3D 模型:

15.11 章节小憩

看一看前面我们写出来的程序,再看这章我们渲染出来的小萝莉和擎天柱,一下子跨度这么大, 只是因为我们提前用到了后面才会讲到的知识。别着急,后面会把这章示例程序完整而详细的知识为大家娓娓道来的。准备着, 让我们继续向下一个知识点进发吧。


原文链接

著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台