8.2 DirectX键盘控制
游戏中使用键盘具有特殊性。一般的Windows程序把按键当做文本输入工具,当按下一个按键时,产生响应的字符输入事件,当保持按下状态一定时间后,应用程序会收到不断重复的字符,直到松开按键。系统提供了一些服务,对持续按下的按键重复,把扫描码转换成虚拟键码。因而使用API能够很好地支持文字输入。
而游戏中,键盘实质上是一个有很多个按键的手柄:
(1)并不需要自动的重复文字。系统提供了自动重复的功能,使得按键被按下一段时间后,应用程序不断地得到重复的字符,显然这并不适合游戏。
(2)只需要读取扫描码。游戏中常常需要知道具体是哪个键被按下,而不是输入了某个字符。例如,在模拟器游戏NeoRAG里,小键盘的数字键用做A、B功能键,而大键盘上的数字键用于投币、控制音量。一般应用程序里,二者都用于数字的输入,而在游戏里,它们的作用是不一样的,使用DirectInput就能根据扫描码很方便地区分它们。
(3)希望直接访问硬件,而不是用API的消息机制,以提高效率。使用DirectInput能够直接读取硬件的状态,获取键盘的扫描码。
DirectInput技术提供的接口主要被封装在DirectInput对象和DirectInputDevice类中。那么,怎么创建这些类的实例,判断哪些按键被按下呢?
8.2.1 初始化键盘对象
1.创建DirectInput对象
与DirectX的其他组件的使用方法类似,要使用DirectInput,需要创建一个根对象,即DirectInput对象。创建这个对象的函数是一个全局函数:
HRESULT WINAPI DirectInput8Create(
HINSTANCE hinst, //Windows进程句柄
DWORD dwVersion, //版本通常取DIRECTINPUT_VERSION
REFIID riidltf, //接口的标识,通常取IID_IDirectInput8
LPVOID *ppvOut, //返回的DirectInput对象指针
LPUNKNOWN punkOuter //COM对象指针,一般取NULL
};
2.创建设备对象
DirectInput类提供了创建设备接口的方法:
HRESULT IDirectInput8::CreateDevice(
REFGUID rguid, //设备标识
LPDIRECTINPUTDEVICE *lplpDirectInputDevice, //DirectInput设备对象
LPUNKNOWN pUnkOuter //COM对象
);
GUID(globally unique identifier)是设备的全球唯一标识,它用于区分各设备。可以枚举出系统已安装的设备的GUID,然后选择一个,也可以使用已经定义好的系统默认的键盘设备:GUID_SysKeyboard。
第二个参数用于保存创建的设备的指针。
在下面的程序片断中,创建了DirectInput和DirectInputDevice的键盘对象:
//创建DirectInput8对象
if(DI_OK!=DirectInput8Create( hInst,DIRECTINPUT_VERSION,
IID_IDirectInput8,(LPVOID*)&m_pDInput,NULL))
MessageBox(hWnd,"创建DInput 对象失败!","ERROR",
MB_ICONERROR|MB_OK);
//创建键盘设备
if(DI_OK!=m_pDInput->CreateDevice(GUID_SysKeyboard,
&m_pDInputKB,NULL))
MessageBox(hWnd,"创建键盘设备失败!","ERROR",MB_ICONERROR|MB_OK);
8.2.2 设置键盘设备状态
1.设置数据格式
对于接收到的键盘数据,需要按照一定的格式进行分析和处理,因此,必须先设置键盘设备的数据格式。函数SetDataFormat用于设置数据格式:
HRESULT IDirectInputDevice8::SetDataFormat(
LPCDIDATAFORMAT lpdf //数据格式
);
LPCDIDATAFORMAT是一个指向DIDATAFORMAT的指针:
typedef struct DIDATAFORMAT {
DWORD dwSize; //结构的大小
DWORD dwObjSize; // DIOBJECTDATAFORMAT结构的大小
DWORD dwFlags; //数据的附加标识
DWORD dwDataSize; //数据包的大小
DWORD dwNumObjs; //在rgodf数组里的对象数目
LPDIOBJECTDATAFORMAT rgodf; //指向一个DIOBJECTDATAFORMAT地址
} DIDATAFORMAT, *LPDIDATAFORMAT;
typedef const DIDATAFORMAT *LPCDIDATAFORMAT;
事实上,一般情况下,开发人员不需要自己创建DIDATAFORMAT这个结构体,而是直接使用已经定义好的全局变量来使用标准的设备数据格式,这些变量包括:
c_dfDIKeyboard //标准键盘对象
c_dfDIMouse //标准鼠标对象
c_dfDIMouse2 //标准鼠标对象
c_dfDIJoystick //标准游戏杆对象
c_dfDIJoystick2 //标准游戏杆对象
如果要创建DIDATAFORMAT结构体,首先要枚举设备提供哪些对象,如按键、滚轴、滚轮。如果设置的数据格式描述了设备并不提供的对象,此函数就会返回DIERR_INVALIDPARAM。
下面的代码设置了标准键盘对象的数据格式:
//设置数据格式
if(DI_OK!= m_pDInputKB->SetDataFormat(&c_dfDIKeyboard))
MessageBox(hWnd,"设置键盘数据格式失败!","ERROR",
MB_ICONERROR|MB_OK);
2.设置键盘协调层级
接下来就要设置键盘的协调层级,以决定程序对键盘设备的控制权。函数SetCooperativeLevel用于设置键盘协调层级:
HRESULT IDirectInputDevice8::SetCooperativeLevel(
HWND hwnd,
DWORD dwFlags
);
hwnd参数用于设置DirectInput相关的窗口,使用IDirectInput8::ConfigureDevices可以显示DirectInput设备的配置窗口。dwFlags指定了设备的协调层级。
HRESULT ConfigureDevices(
LPDICONFIGUREDEVICESCALLBACK lpdiCallback, //每次设备改变的回调函数
LPDICONFIGUREDEVICESPARAMS lpdiCDParams, //设备参数
DWORD dwFlags, //附加标识
LPVOID pvRefData //传给回调函数的参数
);
下面的代码设置设备的协调层级为前台非独占模式:
m_pDInputKB->SetCooperativeLevel(hWnd,
DISCL_NONEXCLUSIVE|DISCL_FOREGROUND);
8.2.3 获取键盘输入
1.取得键盘控制权
在读取键盘输入的数据之前,必须先让应用程序获取设备的控制权。
HRESULT Acquire(VOID);
在获取设备前必须设置好数据格式或动作映射,读取设备数据前必须获取设备。
当设备控制权失去后,读取设备数据时会返回DIERR_INPUTLOST错误,如果要重新获取控制权,可以在窗口激活消息响应过程中重新获取设备:
case WM_ACTIVATE:
if( WM_INACTIVE != wParam && g_pKeyboard )
{ // 窗口激活后获取设备控制权
g_ pKeyboard ->Acquire();
}
break;
2.读取键盘数据
每种设备都可以使用立即数据与缓冲数据,而二者的数据获取、表示和存储方式也不尽相同。
1)键盘立即数据
获取立即数据的函数为GetDeviceState:
HRESULT GetDeviceState(
DWORD cbData,
LPVOID lpvData
);
设备状态将被保存到lpvData这个缓冲区中,cbData是它的大小。一般使用256个字节的字符数组作为缓冲区。这个数组按照物理键码的顺序存储,每个字节的最高位分别存储了各个按键的状态,要判断某个键是否被按下,只要把这个字符和0x80作AND运算,结果为非0就表示按键是按下的状态。
例如,以下语句判断了【A】键的状态:
if(DI_OK!= m_pDInputKB->GetDeviceState(
sizeof(m_strKeyState), m_strKeyState))
MessageBox(NULL,"Failed to GetDeviceState","ERROR",
MB_ICONERROR|MB_OK);
if(m_strKeyState[DIK_A] & 0x80)
vectKB.x-=5;
需要注意的是:
(1)扫描键码和Windows API中的虚拟键码是不同的,虚拟键码是由扫描码转换而来的。
(2)DirectInput为每个PC增强键盘的按键定义了一个常量即物理键码,这个常量就是它们的扫描码,如表8-2所示。对于扫描码不同的兼容键盘,DirectInput会把它们转换成物理键码。并非所有的PC增强键盘都拥有所有的键,例如DIK_LWIN、DIK_RWIN、and DIK_APPS、F11、F12等,而且这些按键的存在性无法判断。
表8-2 DirectInput键码
|
DirectInput键码 |
描 述 |
|
DIK_LSHIFT,DIK_RSHIFT |
Shift键 |
|
DIK_LMENU/DIK_LALT DIK_RMENU/DIK_RALT |
Alt键,DIK_LALT,DIK_RALT是旧名称 |
|
DIK_LCONTROL,DIK_RCONTROL |
Ctrl键 |
|
DIK_LEFTARROW,DIK_RIGHTARROW DIK_UPARROW,DIK_DOWNARROW DIK_LEFT,DIK_RIGHT,DIK_UP,DIK_DOWN |
方向键前四个对应于后四个,后者是旧名称 |
|
DIK_F1...DIK_F15 |
功能键 |
|
DIK_ESCAPE |
Esc键 |
续表
|
DirectInput键码 |
描 述 |
|
DIK_SPACE |
空格键 |
|
DIK_RETURN |
回车键 |
|
DIK_NUMPAD0, …, DIK_NUMPAD9 |
小键盘数字键 |
|
DIK_0, … ,DIK_9 |
主键盘数字键 |
|
DIK_TAB |
Tab键 |
|
DIK_A, … DIK_Z |
字母键 |
|
VK_INSERT, VK_DELETE, DIK_HOME, DIK_END, DIK_PRIOR, DIK_NEXT |
插入,删除,HOME,END,PageUp,PageDown键 |
2)键盘缓冲数据
使用缓冲数据的机制是DirectInput将数据读入缓冲区,客户程序从缓冲区中读取输入事件。
访问缓冲数据前,首先要设置缓冲区大小,默认的缓冲区大小为0。
设置缓冲区大小如下:
HRESULT IDirectInputDevice8::SetProperty(
REFGUID rguidProp,
LPCDIPROPHEADER pdiph
);
这个函数可以指定很多Device参数,要设置缓冲区大小,rguidProp取DIPROP_ BUFFERSIZE,表示调用这个函数要设置缓冲区大小。
读取缓冲数据,要定义DIDEVICEOBJECTDATA结构类型的数组。缓冲区用于存储缓冲输入事件,而这个数组用于从缓冲中读取信息,可以一次读取单个事件,也可以一次读取多个事件。每个DIDEVICEOBJECTDATA结构存储一个事件,这个结构体是这样的:
typedef struct DIDEVICEOBJECTDATA {
DWORD dwOfs; //发生事件的设备对象数据存储位置,也就是键码
DWORD dwData; //事件的相关数据
DWORD dwTimeStamp; //时间戳
DWORD dwSequence; //顺序号
UINT_PTR uAppData;
} DIDEVICEOBJECTDATA, *LPDIDEVICEOBJECTDATA;
typedef const DIDEVICEOBJECTDATA *LPCDIDEVICEOBJECTDATA;
获取键盘缓冲数据:
HRESULT IDirectInputDevice8::GetDeviceData(
DWORD cbObjectData,
LPDIDEVICEOBJECTDATA rgdod,
LPDWORD pdwInOut,
DWORD dwFlags
);
DIDEVICEOBJECTDATA数组存储了键的状态变化信息,包括按下、释放等事件。由于DirectInput直接访问硬件,所以系统提供的按键重复的服务对DirectInput是无效的。关于按键重复,在Windows控制面板中可以调节其延时和重复频率。
8.2.4 释放设备
DirectInput对象类似于COM对象,在不再被使用的时候,需要释放对象。
1.释放设备的访问权
HRESULT Unacquire(VOID);
使用Unacquire函数后,人为地放弃了对设备的控制权,如果要重新使用设备,必须调用Acquire函数重新获取对设备的控制权。
m_pDInputKB->Unacquire();
2.释放对象
程序结束时,需要销毁所有的DirectInput对象:
#define SAFE_RELEASE(p) if(p) {p->Release();p=NULL;}
SAFE_RELEASE(m_pDInputKB);
SAFE_RELEASE(m_pDInput);







