在Windows程序中,用户通过键盘或者鼠标输入的消息并不是应用程序直接处理的,而是通过Windows的消息机制转发给Windows操作系统。Windows操作系统对这些消息进行响应后,在通过回调应用程序的窗口过程函数进行相应的消息处理。在DirectX中,微软为我们提供了名为DirectInput接口对象来实现用户的输入。DirectInput直接和硬件驱动打交道,处理用户的输入要比消息机制快的多。
DirectInput这套API自DirectX8更新以来,功能已经足够完善了。因此我们使用DirectInput的API的话,基本上就是DirectInput8。DirectInput由DirectInput8,IDirectInputDevice8和IDirectInputEffect8这3个接口组成。DirectInput8主要用于输入设备的创建,IDirectInputDevice8就是输入设备(键盘,鼠标等等),IDirectInputEffect8主要用于控制设备的力反馈效果(游戏杆)。
使用Input获取用户输入信息的话,首先要创建输入设备(键盘和鼠标)。IDirectInput8接口对象的CreateDevice方法来创建输入设备。获取到鼠标或者键盘设备之后,还有对其数据格式进行设置。数据格式用于表示设备状态信息的存储方式,每种设备都有一种读取对应数据的特定数据格式,所以对每种设备都要区别对待。设置数据格式通常使用SetDataFormat方法,该方法的只有一个参数,这个参数都是预定义值,如果是键盘的话,我们一般使用标准键盘c_dfDIKeyboard,如果是鼠标的话,我们一般使用标准鼠标c_dfDIMouse。
另外,这些输入设备都可能被多个Windows应用程序同时使用。因此,需要一种方式来共享和协调各个应用程序对设备的访问。在DirectInput中,使用协作级别来处理这种情况,它表示了应用程序对设备的控制权。我们使用SetCooperativeLevel方法来设置设备的协作级别,设备的协作级别类型,系统预定义值如下:
DISCL_EXCLUSIVE 表示独占模式,本应用程序唯一使用该设备
DISCL_NONEXCLUSIVE 表示共享模型,多个应用程序可以共同使用该设备
DISCL_FOREGROUND 表示前台模式,只有当前窗口激活状态,才能获取设备控制权
DISCL_BACKGROUND 表示后台模型,可以在任何状态下获取设备控制权
DISCL_NOWINKEY 表示让Windows键失效
最后一步我们还需要使用Acquire方法来获取对设备的控制权,获取到输入设备的控制权之后,就可以调用GetDeviceState方法来读取设备的数据。获取的设备数据格式和我们之前通过SetDataFormat方法是相对应的。对于鼠标来讲,它就是DIMOUSESTATE结构体,而键盘来讲,它就是一个char数组而已。
读取设备数据一般使用循环,原因在于设备有可能被其他应用程序获得控制权,那么我们的程序就会失去控制权,也就无法获取设备的数据,因此需要循环获取轮询设备控制权。剩下的就是我们对获取到的设备数据进行处理了。键盘数据的处理比较简单,其实就是获取按下键盘的对应的键值,这些都是预定义好的。鼠标数据DIMOUSESTATE 是一个结构体:
typedef struct _DIMOUSESTATE {
LONG lX; // X轴
LONG lY; // Y轴
LONG lZ; // 滚轮的相对移动量,没移动是0,正值向前,负值向后
BYTE rgbButtons[4]; // 四个按钮状态,左键/右键/中键/???
} DIMOUSESTATE, *LPDIMOUSESTATE;
有两种同鼠标通信的方法:绝对模式和相对模式。在绝对模式下,鼠标返回鼠标指针所处位置在屏幕坐标系里的坐标。因此,在屏幕分辨率为640×480时,你可以预料鼠标位置会在0~639、0~479范围内变化。在相对模式下,鼠标坐标则是根据上一个位置到当前位置的偏移量,同样的鼠标滚轮也是相对移动量。我们可以定义一个全局的鼠标绝对位置坐标,并且进行初始化操作(使用SetCursorPos方法同步初始值),然后累加这个偏移量(lX/ lY)就能够获取到鼠标的绝对位置坐标,这个坐标是相对于屏幕左上角而言的。我们可以使用GetCursorPos方法来进行对比,看这个绝对坐标计算的是否正确。但是,在实际运行过程中,可能会发现使用偏移量计算出来的绝对位置和使用GetCursorPos方法获取的绝对位置不一样,计算而来的偏大一些。这是因为DirectInput是直接和硬件驱动打交道。如果你的电脑设置了鼠标急速了,那么这个偏移量而变大,从而计算出来的绝对坐标也会偏大。我们可以关闭本地电脑的鼠标加速和取消指针精度,如下图:
控制面板(小图标)->轻松访问中心->使鼠标更易于使用->设置鼠标键
控制面板(小图标)->轻松访问中心->使鼠标更易于使用->鼠标设置->指针选项
当然,我们不可能让每个游戏玩家都这样去设置。我们需要换一个角度去理解这个问题。其实在游戏开发过程中,使用最多的就是偏移量,而不会直接使用鼠标绝对坐标值。例如,我们使用鼠标控制游戏角色人物行走的话,游戏角色人物在地图上的坐标不是鼠标的绝对坐标,但是需要根据鼠标的偏移量来控制游戏角色人物的行走。这样的话,DirectInput给我们的偏移量就是最佳数据。但我们非要准确获取鼠标绝对坐标,使用GetCursorPos方法即可。
介绍完之后,我们使用VS2019来创建新项目“D3D_09_Input”,首先我们创建公共头文件“main.h”文件,内容如下:
#pragma once
#include <windows.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <dinput.h>
#include <time.h>
#include <math.h>
#include <iostream>
#include <fstream>
#include <string>
// 引入依赖的库文件
#pragma comment(lib,"d3d9.lib")
#pragma comment(lib,"d3dx9.lib")
#pragma comment(lib,"dinput8.lib")
#pragma comment(lib,"dxguid.lib")
#pragma comment(lib,"winmm.lib")
#define WINDOW_LEFT 200 // 窗口位置
#define WINDOW_TOP 100 // 窗口位置
#define WINDOW_WIDTH 800 // 窗口宽度
#define WINDOW_HEIGHT 600 // 窗口高度
#define WINDOW_TITLE L"D3D游戏开发" // 窗口标题
#define CLASS_NAME L"D3D游戏开发" // 窗口类名
这里面最重要的一点就是我们增加了dinput.h头文件用于输入设备的开发支持,同时增加了对应的库文件。本案例中,我们直接对Input输入设备进行封装成类,该类主要包含键盘和鼠标两种设备,以及获取两种设备的输入信息。我们创建“InputClass.h”和“InputClass.cpp”两个文件,首先是“InputClass.h”内容如下:
#pragma once
#include "main.h"
// 输入类
class InputClass {
public:
LPDIRECT3DDEVICE9 D3DDevice = NULL; // Direct3D设备指针对象
LPDIRECTINPUT8 D3DDirectInput = NULL; // DirectInput接口对象
LPDIRECTINPUTDEVICE8 D3DMouseDevice = NULL; // 鼠标设备对象
DIMOUSESTATE D3DMouseState = { 0 }; // 获取鼠标状态数据(点击事件)
int click = 0; // 是否点击:1左击,2是右击
long x = 0.0f, y = 0.0f, m = 0.0f; // 鼠标坐标和滚轮(相对位置)
long x1 = 0.0f, y1 = 0.0f; // 鼠标坐标(绝对位置)
LPDIRECTINPUTDEVICE8 D3DKeyboardDevice = NULL; // 键盘设备对象
char D3DKeyboardState[256] = { 0 }; // 获取键盘状态数据(键盘事件)
char key[10] = { 0 }; // 按键名称
public:
// 构造方法
InputClass(LPDIRECT3DDEVICE9 device, HWND hwnd, HINSTANCE hInstance);
// 获取输入设备数据
bool read(IDirectInputDevice8* device, void* buffer, long size);
// 读取鼠标和键盘数据
void update();
// 析构方法
~InputClass();
};
我们在类中分别声明了鼠标设备和键盘设备,以及设备的输入信息,而且还声明了用户输入的鼠标信息和键盘键值信息。键盘的键值信息使用一个char key数组即可,而鼠标信息则需要记录点击事件,以及相对和绝对位置信息。具体实现“InputClass.cpp”内容如下:
#include "InputClass.h"
// 输入类: 构造方法
InputClass::InputClass(LPDIRECT3DDEVICE9 device, HWND hwnd, HINSTANCE hInstance) {
// Direct3D设备指针对象
D3DDevice = device;
// 实例化 DirectInput 接口对象
DirectInput8Create(
hInstance, // 表示Windows程序句柄
0x0800, // 表示DirectInput版本号,使用0x0800即可
IID_IDirectInput8, // 表示接口的标志,使用IID_IDirectInput8即可
(void**)&D3DDirectInput, // 表示IDirectInput8接口对象指针
NULL); // 直接设置Null即可
// 鼠标设备初始化
D3DDirectInput->CreateDevice(
GUID_SysMouse, // 设备的GUID
&D3DMouseDevice, // 设备对象指针
NULL); // 直接设置NULL即可
// 设置数据格式和协作级别
D3DMouseDevice->SetDataFormat(&c_dfDIMouse);
D3DMouseDevice->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
// 获取设备控制权
D3DMouseDevice->Acquire();
// 键盘设备初始化
D3DDirectInput->CreateDevice(GUID_SysKeyboard, &D3DKeyboardDevice, NULL);
// 设置数据格式和协作级别
D3DKeyboardDevice->SetDataFormat(&c_dfDIKeyboard);
D3DKeyboardDevice->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
// 获取设备控制权
D3DKeyboardDevice->Acquire();
// 释放 DirectInput 对象
D3DDirectInput->Release();
D3DDirectInput = NULL;
// 设置鼠标在窗口左上角位置,不是屏幕左上角
x = WINDOW_LEFT;
y = WINDOW_TOP;
SetCursorPos(WINDOW_LEFT, WINDOW_TOP);
};
// 输入类: 获取输入设备数据
bool InputClass::read(IDirectInputDevice8* device, void* buffer, long size) {
HRESULT hr;
while (true)
{
device->Poll(); // 轮询设备
device->Acquire(); // 获取设备的控制权
if (SUCCEEDED(hr = device->GetDeviceState(size, buffer))) break;
if (hr != DIERR_INPUTLOST || hr != DIERR_NOTACQUIRED) return FALSE;
if (FAILED(device->Acquire())) return FALSE;
}
return TRUE;
};
// 输入类: 读取鼠标和键盘数据
void InputClass::update() {
// 读取鼠标输入
click = 0;
::ZeroMemory(&D3DMouseState, sizeof(D3DMouseState));
read(D3DMouseDevice, (LPVOID)&D3DMouseState, sizeof(D3DMouseState));
// 记录鼠标偏移量
x = D3DMouseState.lX;
y = D3DMouseState.lY;
m = D3DMouseState.lZ;
// 左键点击
if (D3DMouseState.rgbButtons[0] & 0x80) click = 1;
// 右键单价
if (D3DMouseState.rgbButtons[1] & 0x80) click = 2;
// 计算鼠标绝对坐标,不是窗口系坐标,而是屏幕系坐标
//x1 += D3DMouseState.lX;
//y1 += D3DMouseState.lY;
// 鼠标绝对位置,屏幕系坐标,相对于屏幕左上角(0,0)位置
POINT pt;
GetCursorPos(&pt);
x1 = pt.x;
y1 = pt.y;
// 读取键盘输入
strcpy_s(key, 10, "");
::ZeroMemory(D3DKeyboardState, sizeof(D3DKeyboardState));
read(D3DKeyboardDevice, (LPVOID)D3DKeyboardState, sizeof(D3DKeyboardState));
// 键盘按键,省略一部分代码
if (D3DKeyboardState[DIK_1] & 0x80) strcpy_s(key, 10, "1");
};
// 输入类: 析构方法
InputClass::~InputClass() {
if (NULL != D3DMouseDevice) D3DMouseDevice->Unacquire();
if (NULL != D3DMouseDevice) D3DMouseDevice->Release();
D3DMouseDevice = NULL;
if (NULL != D3DKeyboardDevice) D3DKeyboardDevice->Unacquire();
if (NULL != D3DKeyboardDevice) D3DKeyboardDevice->Release();
D3DKeyboardDevice = NULL;
};
在构造函数中,我们根据上面讲的流程,依次创建了鼠标和键盘设备,并设置数据格式和协作级别等等,然后read方法用于获取输入设备的信息,而update方法才是对输入信息的详细处理。从这里我们就可以看出来,update函数才是我们封装的重点。如果要在游戏中获取用户输入事件信息,我们就可以通过update函数来获取到啦。接下来,我们就开始“main.cpp”文件的内容。我们之前都是通过Windows的消息机制来获取用户的输入事件,也都是在窗口过程函数WndProc来获取用户的输入信息。那么既然有了DirectX的Input,我们自然就不需要Windows的消息机制了。但是,窗口过程函数WndProc还是必须存在的。首先,我们还是全局变量的声明:
// 引入头文件
#include "main.h"
#include "InputClass.h"
// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;
// 输入类
InputClass* input = NULL;
// 字体指针对象
LPD3DXFONT D3DFont = NULL;
// 鼠标信息
std::string mouseStr = "";
// 键盘信息
std::string keyStr = "";
// 字符串转换
wchar_t* stringToWchar(std::string str);
本案例只是对Input的演示,因此我们使用字体对象来将鼠标和键盘信息打印到窗体上面。既然我们已经不想使用Windows的消息机制了,那么我们之前声明的update函数的参数就不太合适了,这里我们重新声明一个新参数的update函数,如下:
// 声明游戏循环中处理用户输入函数
void update(HWND hwnd, HINSTANCE hInstance);
然后就是程序入口函数wWinMain里面的第五步(消息循环过程)中做调整:
// 获取消息并交给窗口过程函数
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
// 游戏循环逻辑处理
update(hwnd, hInstance);
render(hwnd);
}
其中这个改动,在我们项目“D3D_07_Mario”中就调整过。这里调整完之后,我们还要继续调整窗口过程函数WndProc,因为我们不需要在这个函数中来获取输入设备信息了。当然,我不能全部清空该函数内代码,还是要保留两个基本事件:
switch (message)
{
case WM_PAINT:
ValidateRect(hwnd, NULL); // 窗口重绘消息
break;
case WM_DESTROY:
PostQuitMessage(0); // 窗口销毁消息
break;
default: // 调用缺省的窗口过程
return DefWindowProc(hwnd, message, wParam, lParam);
}
return 0;
关于update函数的调整,我们重新阐明一下。因为最初我们的设计就是在游戏的循环中分别执行update和render来处理用户输入和渲染画面。但是由于Windows应用程序的消息机制,我们不得不使用窗体过程化函数来处理用户输入,并调用我们的update函数。但是,现在我们可以通过Input来直接获取输入设备的信息,那么我们就可以不再受制于窗体过程化函数。这样,又能够回到我们最初的设计模式。接下来就是我们的initScene函数,代码如下:
// 创建一个字体对象
D3DXCreateFont(D3DDevice, 24, 0, 1, 1, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"微软雅黑", &D3DFont);
// 初始化输入类
input = new InputClass(D3DDevice, hwnd, hInstance);
// 线性纹理
//D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
//D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
// 初始化投影变换
//initProjection();
// 初始化光照
//initLight();
由于我们没有任何游戏对象的绘制,因此我们暂时不需要投影和光照。接下来,就是我们的update函数了,非常的明显,我们需要在这里调用InputClass类的update函数,用于获取输入设备的信息。当然,如果用户没有输入事件,则里面的数据也是为空的状态。
// 获取键盘和鼠标信息
input->update();
// 显示鼠标位置和绝对位置
mouseStr = "鼠标相对位置:x = " + std::to_string(input->x) + ", y = " + std::to_string(input->y);
mouseStr += "鼠标绝对位置:x = " + std::to_string(input->x1) + ", y = " + std::to_string(input->y1);
// 左键
if (input->click == 1) mouseStr += ",点击左键";
// 右键
if (input->click == 2) mouseStr += ",点击右键";
// 键盘输入设备信息
keyStr = "按下" + std::string(input->key) + "键";
接下来就是renderScene函数,也就是把输入设备的信息打印到窗体上,
// 定义一个矩形(窗口)
RECT formatRect;
GetClientRect(hwnd, &formatRect);
// 绘制文字
formatRect.top = 20;
formatRect.left = 20;
D3DFont->DrawText(0, stringToWchar(mouseStr), -1, &formatRect, DT_LEFT, D3DCOLOR_XRGB(255, 0, 0));
formatRect.top += 50;
D3DFont->DrawText(0, stringToWchar(keyStr), -1, &formatRect, DT_LEFT, D3DCOLOR_XRGB(255, 0, 0));
最后就是stringToWchar函数,这个函数我们之前使用过,
// string 转 wchar_t 字符串
wchar_t* stringToWchar(std::string inputStr) {
int len = (int)(inputStr.length() + 1);
wchar_t* inputStr2 = new wchar_t[len];
MultiByteToWideChar(CP_ACP, 0, inputStr.c_str(), -1, inputStr2, len);
return inputStr2;
}
运行效果如下:
本课程的所有代码案例下载地址:
备注:这是我们游戏开发系列教程的第二个课程,这个课程主要使用C++语言和DirectX来讲解游戏开发中的一些基础理论知识。学习目标主要依理解理论知识为主,附带的C++代码能够看懂且运行成功即可,不要求我们使用DirectX来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!