键盘概述:
当我们在键盘上按下一个键时,字符就神奇的出现在了屏幕上.键盘跟系统之间的交互是非常烦琐的,但作为游戏程序员的我们必须理解这里面的奥秘,为以后的开发扫平障碍.
当我们按下或者是释放一个键时,一个信号将被传送给键盘的微处理器,随后键盘微处理器将向计算机系统"申请"一个中断,同时系统从键盘那里获得了一个字符码,从而使得系统得知到底是那个键被按下或者释放,微处理器给计算机系统传送的那个字符码被称作扫描码.下图让我们有个更为清晰的理解:
这里需要指出的是一个扫描码的大小是一个字节,其中低7位(即bit0-6)表示哪个键被操作,而最高位代表是被按下还是被释放.所以我们所能处理的最多的键的数目是128.
windows中的键盘处理:
想在windows平台上混碗饭吃的人如果不懂消息机制的话那将很难,正是windows的消息机制使得对于键盘的处理变的简单.首先windows把扫描码转换为虚拟码和ASCII码,然后通过消息机制来告诉程序员某个键被按下了.虚拟码只是将原来的扫描码在windows里进行了包装,用VK_A而不是30来表示A.而ASCII码是为了实现扫描码和字符之间的对应关系的,在ASCII码里面,A 和 a所对应的ASCII码是不同的,最多表示128种不同的字符.为了能表示更多的字符,有时候要用到扩展的ASCII码,所谓扩展就是增添了一位附加信息,这样就使得可以表示的字符数目达到了256个,但是仍然不能达到要求,这也是Unicode产生的原因之一.到了Unicode每个字符用16个比特位来表示,所以总共能表示65535种字符,满足了目前所有需求.
对于接收到的虚拟码或者ASCII码如何处理就取决于程序员了,如果我们想用来做文字处理,那么我们就把字符插入到编辑区域.对于游戏来说大多是控制游戏中的各种角色的.
windows中鼠标的处理:
鼠标相对于键盘来说就更加简单了,因为鼠标上的"零件"实在是太少了.当我们按下一个键时就给系统发送一个信号,释放时同样要向系统发送信号.鼠标每隔一个很小的时间间隔就想系统报告它的移动信息等,鼠标的驱动程序读入这些数据然后转换成相应的形式.然而用传统的消息机制来处理鼠标消息是很慢的,有时候不能满足游戏的需要,因为每个鼠标消息都要传送给消息处理过程,然后再被插入到相应的消息队列等待处理,这对于游戏的实时性来说是很不利的,玩家可不想在自己发出命令后要过一段时间才有反应,要的是速度!
从上面的表述我们发现,传统的windows输入处理都要先由设备驱动程序处理,然后再交给系统处理,最后才再给相应的应用程序.为什么不绕开系统而让设备驱动程序跟应用程序直接交互呢?这就是DirectInput的原理.
DirectInput基础:
DirectInput用一组COM对象来表示输入系统和具体的输入设备.最主要的对象 IDirectInput8,用来初始化输入系统和创建输入设备对象.
DirectInput COM 对象
IDirectInput8 最中要的DirectInput COM对象,其他所有的对象都要通过它来创建.
IDirectInputDevice8 用来表示输入设备的COM对象,每种输入设备都与之对应的COM设备对象.
IDirectInputEffect 用来实现反馈效果的COM对象.
所有的输入设备都使用同一个接口对象来处理:IDirectInputDevice8.每种设备都以此为基础再加上各自的特有信息.下图可以帮我们理解:
IDirectInput8创建各种IDirectInputDevice8,然后由IDirectInputDevice8来创建IDirectInputEffect对象.
IDirectInput8组件对象包含一组函数用来初始化数据系统,获得输入设备接口.用的最多的是以下两个函数:
IDirectInput8::EnumDevices() 和 IDirectInput8::CreateDevice().在以后的学习中我们会详细的介绍这两个函数.
DirectInput的初始化:
为了使用DirectInput,我们首先要在程序文件中包含"DInput.h",并且连接DInput8.lib.IDirectInput8对象代表了整个输入系统,所以它是最重要的,看下面的代码:
IDirectInput8 g_pDI; // 声明全局的IDirectInput8对象
DirectInplut为我们提供了DirectInput8Create()帮助器函数,下面是它的原型:
HRESULT WINAPI DirectInput8Create(
HINSTANCE hInstance, // 应用程序实例
DWORD dwVersion, // DIRECTINPUT_VERSION
REFIID riidltf, // IID_IDirectInput8
LPVOID *ppvOut, // 要创建对象的指针
LPUNKNOWN pUnkOuter); // set to NULL
这个函数中的大多数参数取默认值即可,我们只需要提供所要创建对象的指针即可.
下面我们来看一个完整的创建对象的例子:
IDirectInput8 *g_pDI; // global DirectInput object
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int nCmdShow)
{
HRESULT hr;
hr = DirectInput8Create(hInst, DIRECTINPUT_VERSION, \
IID_IDirectInput8, (void**)&g_pDI, NULL);
// return failure if an error occurred
if(FAILED(hr))
return FALSE;
// Go on with program here
初始化输入系统就这么几句代码搞定了,下面我们就来创建具体的输入设备.
输入设备的创建:
很高兴我们又一次地站在了巨人的肩膀上,微软在简化输入系统方面做了很多工作,这就使得我们学习起来很轻松.我们可以使用同一个COM对象来处理系统中所有的输入设备.各种输入设备的创建和使用是极为类似的,我们首先给出创建和使用它们的步骤,见下表:
创建和使用输入设备的步骤:
步骤 使用到的接口函数
Obtain a device GUID IDirectInput8::EnumDevices
Create the device COM object IDirectInput8::CreateDevice
Set the data format IDirectInputDevice8::SetDataFormat
Set the cooperative level IDirectInputDevice8::SetCooperativeLevel
Set any special properties IDirectInputDevice8::SetProperty
Acquire the device IDirectInputDevice8::Acquire
Poll the device IDirectInputDevice8::Poll
Read in data IDirectInputDevice8::GetDeviceState
别忘记了首先要声明一个设备对象指针:
IDirectInputDevice8 *pDIDevice;
下面我们来逐一说明每个步骤:
获得唯一设备号:
系统中的每个输入设备都有一个GUID(全局唯一标识),要想使用输入设备,我们就必须先得到它的GUID.对于键盘和鼠标来说这个工作是很简单的,因为DirectInput分别为它们定义了GUID_SysKeyboard和GUID_SysMouse.但是对于其它的输入设备,我们必须通过枚举来获得它们的GUID:
HRESULT IDirectInput8::EnumDevices(
DWORD dwDevType, //所要枚举的设备类型
LPDIENUMCALLBACK lpCallback, //每当找到一个该类型的设备时将自动调用该函数
LPVOID pvRef, // 可以把它当成上面回调函数的参数
DWORD dwFlags); // 标志位
下面是dwFlags的取值:
DIEDFL_ALLDEVICES
All installed devices are enumerated. This is the default behavior.
DIEDFL_ATTACHEDONLY
Only attached and installed devices.
DIEDFL_FORCEFEEDBACK
Only devices that support force feedback.
DIEDFL_INCLUDEALIASES
Include devices that are aliases for other devices.
DIEDFL_INCLUDEHIDDEN
Include hidden devices.
DIEDFL_INCLUDEPHANTOMS
Include phantom (placeholder) devices.
下面是回调函数的声明:
BOOL CALLBACK DIEnumDevicesProc(
LPDIDEVICEINSTANCE lpddi, // 设备结构
LPVOID pvRef);
lpddi是一个指向DIDEVICEINSTANCE结构的指针,它包含了当前所找到设备一些信息.下面是它的详细定义:
typedef struct {
DWORD dwSize; // Size of this structure
GUID guidInstance; // device GUID
GUID guidProduct; // OEM supplied GUID of device
DWORD dwDevType; // Device type
TCHAR tszInstanceName[MAX_PATH]; //Name of device
TCHAR tszProductName[MAX_PATH]; //Name of product
GUID guidFFDriver; // GUID of force-feedback driver
WORD wUsagePage; // Usage page if an HID device
WORD wUsage; // Usage code if an HID device
} DIDEVICEINSTANCE;
下面就让我们来看一个具体的例子,它的功能是枚举系统中的所有输入设备,当找到一个后就弹出一对话框,根据我们的选择来决定是继续枚举还是停止运行:
IDirectInput8 *g_pDI;
BOOL InitDIAndEnumAllDevices(HWND hWnd,HINSTANCE hInst)
{
if(FAILED(DirectInput8Create(hInst, DIRECTINPUT_VERSION,IID_IDirectInput8, (void**)&g_pDI, NULL)))
return FALSE;
g_pDI->EnumDevices(DI8DEVCLASS_ALL, EnumDevices,(LPVOID)hWnd, DIEDFL_ALLDEVICES);
return TRUE;
}
BOOL CALLBACK EnumDevices(LPCDIDEVICEINSTANCE pdInst, LPVOID pvRef)
{
int Result;
// Display a message box with name of device found
Result = MessageBox((HWND)pvRef, pdInst->tszInstanceName,
"Device Found", MB_OKCANCEL);
// Tell it to continue enumeration if OK pressed
if(Result == IDOK)
return DIENUM_CONTINUE;
// Stop enumeration
return DIENUM_STOP;
}
然后把InitDIAndEnumAllDevices()插入到程序的相应位置就可以了.
设备对象的创建:
现在我们已经有了GUID,接下来就是要创建具体的设备接口对象了,这个工作是有下面这个函数来完成的:
HRESULT IDirectInput8::CreateDevice(
REFGUID rguid, // GUID of device to create, predefined or from enumeration
LPDIRECTINPUTDEVICE *lplpDirectInputDevice, // pointer to the object you’re creating
LPUNKNOWN pUnkOuter); // NULL - not used
它的参数都很明了,这里我们就不再多说,直接来看一个例子:
IDirectInputDevice8 *pDIDevice;
HRESULT hr = g_pDI->CreateDevice(DeviceGUID, &pDIDevice, NULL);
或许感觉这个例子还是不够具体,我们就来看看如何使用键盘:
IDirectInputDevice8 *pDIDevice;
HRESULT hr = pDI->CreateDevice(GUID_SysKeyboard,&pDIDevice, NULL);
设置数据格式:
各种输入设备发送的信息都是不同的,所以我们无法以一种固定的格式来接收所有的输入信息,所以我们需要为每个输入设备都设置一种数据格式以便来正确的接受来自设备的数据,设置工作由下面这个函数来完成:
HRESULT IDirectInputDevice8::SetDataFormat(LPCDIDATAFORMAT lpdf);
该函数只有一个参数,一个指向DIDATAFORMAT结构的指针,下面我们来这个函数的具体定义:
typedef struct {
DWORD dwSize; // Size of this structure
DWORD dwObjSize; // Size of DIOBJECTDATAFORMAT structure
DWORD dwFlags; // Flags determining if device works in absolute mode (DIDF_ABSAXIS) or relative (DIDF_RELAXIS)
DWORD dwDataSize; // Size of data packets received from device (in multiples of 4)
DWORD dwNumObjs; // Number of objects in the rgodf array
LPDIOBJECTDATAFORMAT rgodf; // Address to an array of DIOBJECTDATAFORMAT structures.
} DIDATAFORMAT, *LPDIDATAFORMAT;
又是一个讨厌的数据结构,尽管它不是很复杂,但是我们见到的类似的数据结构实在是太多了,好在大多数情况下不用我们自己来创建其实例,因为DirectInput已经预定义好了一些:
Device Data Structure Example
Keyboard c_dfDIKeyboard pDIDevice->SetDataFormat(&c_dfDIKeyboard);
Mouse c_dfDIMouse pDIDevice->SetDataFormat(&c_dfDIMouse);
Joystick c_dfJoystick pDIDevice->SetDataFormat(&c_dfDIJoystick);
我们又一次站在了巨人的肩膀上,尽情享受着前人的果实,感觉着实舒服.如果你对他们的工作感到不屑,或者想自己开发这些,我不太赞同,不要重复发明轮子!
设置设备的共享等级:
游戏中往往使用多个输入设备,鼠标,键盘,游戏杆,甚至更多.但这里面有一个我们不得不考虑的问题:当我们使用这些输入设备的时候是否允许其它的应用程序同时使用.我们可以很霸道地独占这些设备,但这并不是最好的选择.让我们来看看如何设置吧:
HRESULT IDirectInputDevice8::SetCooperativeLevel(
HWND hWnd, // handle to the parent window
DWORD dwFlags);// flags determining how to share access
hWnd是窗口句柄,dwFlags可以从以下值中选择:
等级 详细描述
DISCL_NONEXCLUSIVE 允许其他程序使用,并且不会干扰其他应用程序的使用
DISCL_EXCLUSIVE 独占模式,其它应用都不能使用
DISCL_FOREGROUND 前台模式,也就是说使用它的程序必须处于激活状态,如果它失去焦点就会自动失去设备,再它重新获得焦点的时候必须重新获得设备使用权
DISCL_BACKGROUND 后台模式,无论是否激活都能使用
DISCL_NOWINKEY This disables the Windows logo key.
当我们设置这些标志时,或者DISCL_EXCLUSIVE 或者 DISCL_NONEXCLUSIVE,并且要跟DISCL_FOREGROUND 或者
DISCL_BACKGROUND 合起来使用.我们建议大家这样来组合:
pDIDevice->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
设置特殊属性:
除了前面我们所设置的属性外,我们还可以设置一些更为高级的属性.比如是使用相对坐标还是绝对坐标,相对坐标是相对上一次移动了的坐标,而绝对坐标是以一点为原点来算的.我们还可以来设置数据缓冲,我们可以来设置缓冲区的大小从而以我们喜欢的节奏来处理数据,所有的设置都是通过下面的代码来实现的:
HRESULT IDirectInputDevice8::SetProperty(
REFGUID rguidProp, // GUID of property
LPCDIPROPHEADER pdiph); // DIPROPHEADER containing data about the property being set
下面是DIPROPRHEADER的定义:
typedef struct {
DWORD dwSize; // Size of the enclosing structure
DWORD dwHeaderSize; // Size of this structure
DWORD dwObj; // What value we’re setting
DWORD dwHow; // How you’re setting the value
} DIPROPHEADER, *LPDIPROPHEADER;
可以参阅DirectX SDK了解具体如何来使用上述代码来设置相应的属性.
获得设备:
在设备能被使用之前首先要得到它,这样才能使得我们的程序能接触到设备,不管设备是共享还是独占的.这里有一点需要注意:其它应用程序也是可以获得设备,所以必要的时候我们还要重新获得设备.
那我们怎么知道什么时候该获得设备呢?第一次通常是创建设备对象时,使用设备之前.其它时候就是其它程序夺取了使用权之后.下面的代码用来获得设备:
HRESULT IDirectInputDevice8::Acquire();
我们还可以释放:
HRESULT IDirectInputDevice8::Unacquire();
为了避免在运行期间出现错误,接下来应该调用下面这句:
HRESULT IDirectInputDevice8::Poll();
这个函数的调用能够保证数据的正确性.
数据的读入:
终于,我们能够从输入设备中读入数据了,这个过程是由IDirectInputDevice8::GetDeviceState()来完成的.下面是它的原型:
HRESULT IDirectInputDevice8::GetDeviceState(
DWORD cbData, // 数据缓冲区的大小
LPVOID lpvData); // 数据缓冲区
第二个参数是需要的数据缓冲区,对于各种不同的数据设备数据缓冲区是不同的.
下面是一段读入数据的代码:
BOOL ReadDevice(IDirectInputDevice8 *pDIDevice,
void *DataBuffer, long BufferSize)
{
HRESULT hr;
while(1)
{
// Poll device
g_pDIDevice->Poll();
// Read in state
if(SUCCEEDED(hr = g_pDIDevice->GetDeviceState(BufferSize,(LPVOID)DataBuffer)))
break;
// Return on an unknown error
if(hr != DIERR_INPUTLOST && hr != DIERR_NOTACQUIRED)
return FALSE;
// Reacquire and try again
if(FAILED(g_pDIDevice->Acquire()))
return FALSE;
}
// Return a success
return TRUE;
}
下面我们来看看具体的处理键盘和鼠标的例子.
键盘的处理:
IDirectInputDevice8*InitKeyboard(HWND hWnd, IDirectInput8 *pDI)
{
IDirectInputDevice8 *pDIDevice;
// Create the device object
if(FAILED(pDI->CreateDevice(GUID_SysKeyboard,
&pDIDevice, NULL)))
return NULL;
// Set the data format
if(FAILED(pDIDevice->SetDataFormat(&c_dfDIKeyboard)))
{
pDIDevice->Release();
return NULL;
}
// Set the cooperative mode
if(FAILED(pDIDevice->SetCooperativeLevel(hWnd,
DISCL_FOREGROUND | DISCL_NONEXCLUSIVE)))
{
pDIDevice->Release();
return NULL;
}
// Acquire the device for use
if(FAILED(pDIDevice->Acquire()))
{
pDIDevice->Release();
return NULL;
}
// Everything was a success, return the pointer
return pDIDevice;
}
上面的代码并不难理解,因为都是按照我们前面的讲述来的,所以这里就不再重复.这里只是做好了初始化的工作,在开始读数据之前我们首先要理解键盘的数据是如何保存的.我们必须提供一个256字节的数组,每个字节保存一个键的信息.所以我们一共可以处理256个键.每个键有两个状态:按下或者释放.为了查看键的状态通过查看相应字节的最高位(位7),如果是1则被按下,否则是处于释放状态.
char KeyStateBuffer[256];
if((pDIDKeyboard = InitKeyboard(g_hWnd, g_pDI)) != NULL)
{
// read in the data
ReadData(pDIDKeyboard, (void*)KeyStateBuffer, 256);
}
#define KeyState(x) ((KeyStateBuffer[x] & 0x80) ? TRUE : FALSE)
if(KeyState(VK_LEFT) == TRUE)
{
// Left arrow is being pressed
}
鼠标的处理:
鼠标的初始化跟键盘的一样,只不过是将数据格式由键盘改成了鼠标.这里就不再重复那些代码了.
处理鼠标时需要调用DirectInputDevice8::GetDeviceState()函数,该函数填充了一个DIMOUSESTATE结构体,它里面包含了关于鼠标的信息:
typedef struct {
LONG lX; // Relative change in X coordinate
LONG lY; // Relative change in Y coordinate
LONG lZ; // Relative change in Z coordinate
BYTE rgbButtons[4]; // Button pressed flags
} DIMOUSESTATE, *LPDIMOUSESTATE;
注意,这里的坐标值是相对的,而我们要想得到绝对位置就必须维护两个全局变量来保存绝对位置:
IDirectInputDevice8 *pDIDMouse;
// The mouse coordinates
long g_MouseXPos = 0, g_MouseYPos = 0;
// The data buffer to store the mouse state
DIMOUSESTATE MouseState;
if((pDIDMouse = InitMouse(g_hWnd, g_pDI)) != NULL)
{
// read in the data
ReadData(pDIDMouse, (void*)MouseState, sizeof(DIMOUSESTATE));
// update the absolute coordinates
g_MouseXPos += MouseState.lX;
g_MouseYPos += MouseState.lY;
}
#define MouseButtonState(x) ((MouseState.rgbButtons[x] & 0x80) ? TRUE : FALSE)
小结:在这段时间里我们学习了如何更快的处理输入,为以后做好游戏打好坚实的基础.