8.3 DirectX鼠标控制
使用鼠标设备的基本流程与键盘相似,只是鼠标的数据结构与键盘有所不同。
8.3.1 使用鼠标设备的流程
1.初始化
创建设备IDirectInputDevice8对象,需要特别指定rguid取全局变量 GUID_ SysMouse,表示要创建的是一个默认的系统鼠标设备对象。
//创建DirectInput8对象
if(DI_OK!=DirectInput8Create( hInst,DIRECTINPUT_VERSION,
IID_IDirectInput8,(LPVOID*)&m_pDInput,NULL))
MessageBox(hWnd,"创建DirectInput 对象失败!","ERROR",
MB_ICONERROR|MB_OK);
//创建鼠标设备
if(DI_OK!=m_pDInput->CreateDevice(GUID_SysMouse,&m_pDInputMouse,
NULL))
MessageBox(hWnd,"创建鼠标设备失败!","ERROR",MB_ICONERROR|MB_OK);
2.设置数据格式
鼠标的数据结构与键盘不同,如果要使用自定义的数据格式,就需要自定义DIDATAFORMAT指定自己的数据格式。当然,指定数据格式前,需要定义存储设备数据的数据类型。
以下MouseState结构体定义了一个两键鼠标的结构,数组g_aObjectFormats[ ]定义了接收每个设备对象数据的方式,例如该数组的第一个元素表示将ID为GUID_XAxis的设备对象,也就是X轴的数据存放到自定义的MouseState结构中偏移量为FIELD_ OFFSET(MouseState, lAxisX)的位置,DIDFT_AXIS | DIDFT_ANYINSTANCE指定了设备类型和返回所有设备实例的数据。
创建了DIOBJECTDATAFORMAT和DIDATAFORMAT后,用SetDataFormat函数使其生效:
struct MouseState
{
LONG lAxisX;
LONG lAxisY;
BYTE abButtons[3];
BYTE bPadding; // Structure must be DWORD multiple in size.
};
DIOBJECTDATAFORMAT g_aObjectFormats[] =
{
{&GUID_XAxis, FIELD_OFFSET(MouseState, lAxisX), // X axis
DIDFT_AXIS | DIDFT_ANYINSTANCE, 0 },
{&GUID_YAxis, FIELD_OFFSET(MouseState, lAxisY), // Y axis
DIDFT_AXIS | DIDFT_ANYINSTANCE, 0 },
{0, FIELD_OFFSET(MouseState, abButtons[0]), // Button 0
DIDFT_BUTTON | DIDFT_ANYINSTANCE, 0 },
{0, FIELD_OFFSET(MouseState, abButtons[1]), // Button 1 (optional)
DIDFT_BUTTON | DIDFT_ANYINSTANCE | DIDFT_OPTIONAL, 0 },
{0, FIELD_OFFSET(MouseState, abButtons[2]), // Button 2 (optional)
DIDFT_BUTTON | DIDFT_ANYINSTANCE | DIDFT_OPTIONAL, 0 }
};
DIDATAFORMAT g_dfMouse =
{
sizeof(DIDATAFORMAT),
sizeof(DIOBJECTDATAFORMAT),
DIDF_ABSAXIS,
sizeof(MouseState),
numMouseObjects,
g_aObjectFormats
};
3.设置鼠标的协调层级
DISCL_EXCLUSIVE:表示此应用程序是唯一独占鼠标设备控制权的应用程序,使用该模式后,光标会消失,当然也就无法点击窗口以外的其他窗口。
DISCL_NONEXCLUSIVE:鼠标不会消失,鼠标可以移动到窗口以外的区域点击其他窗口,这时原应用程序失去控制权。
DISCL_FOREGROUND:指定只有在前台模式下获取鼠标输入。
DISCL_BACKGROUND:指定在前台和后台模式下都可以获取鼠标输入。
鼠标的协调层级取不同的值,差别比较明显。通常情况下,使用鼠标时会使用后台非独占模式。这样,鼠标可以激活其他窗口,同时失去对原窗口的控制。在CS中,如果窗口处于非激活模式时,移动鼠标不会改变游戏视角,如图8-3所示。

图8-3 鼠标后台非独占模式
4.获取设备控制权
同样需要调用函数:
HRESULT Acquire(VOID);
控制权失去后,重获控制权同样需要再调用此函数。
5.获取鼠标数据
与键盘相比,鼠标的数据有两种:鼠标移动和鼠标按键。因此,应用程序需要分别获取两种数据。
1)使用鼠标的坐标系统
默认情况下,鼠标的移动量是相对位移,单位是硬件移动的点数(dps),和光标、屏幕分辨率没有关系。光标只是用于表示鼠标位置的一个移动的光标,而这里的移动量是鼠标的移动量,而非光标。
鼠标本身是相对设备,它的移动并没有范围。当然也可以使用绝对位移。
当使用相对模式时,立即数据返回的是自上次读取鼠标数据后到这次读取鼠标数据之间发生的位移,缓冲数据返回的是上次写入缓冲区后鼠标的移动量。
使用绝对移动量时,返回的是鼠标自从获取后移动量的累计值。这两种方法,正值和负值分别表示坐标轴的正向和反向移动量,如表8-3所示。
表8-3 鼠标移动方向
|
坐标轴 |
+ |
- |
|
X |
右方 |
左方 |
|
Y |
向上 |
向下 |
|
Z |
滚轮背向玩家转动 |
向玩家转动 |
改变绝对坐标和相对坐标需要使用这个函数:
HRESULT SetProperty(
REFGUID rguidProp,
LPCDIPROPHEADER pdiph
);
rguidProp=DIPROP_AXISMODE;
DIPROPDWORD的dwData成员取:
DIPROPAXISMODE_ABS 或 DIPROPAXISMODE_REL
分别表示取相对值÷或绝对值。
2)定义有智能的鼠标
不同的鼠标硬件,移动相同距离报告的数据常常不一样,不同的玩家习惯的鼠标的灵敏度也不同,那么,怎么使鼠标具有灵敏度这一属性呢?
比较简单的做法是获取鼠标移动量的数据后,将移动量乘以一个灵敏度系数,然后来控制光标(也可能是准星、摄像机方向、角色的位置等):
x=k*lx;
使用Windows的用户可能会发现,用不同的速度移动鼠标相同的距离,产生的结果是不一样的,快速移动时,光标移动的距离明显大。很容易理解,快速移动通常发生在用户需要移动的距离比较大的时候,这时再给上面这个结果x乘上一个加速系数,使得光标变得智能化。这个系数也可以根据这一帧鼠标的移动量来计算移动速度:
x=k*lx* K1*(lx /FrameTime)=k*K1*lx2/FrameTime;
这个系数可以合并,那么公式变为:
x=Ks*lx2/(float)FrameTime
这种算法也可以改成K1取不连续值的情况。对于y方向,同样可以这样处理。计算时考虑到精度,使用浮点数。
3)立即数据
用IDirectInputDevice8::GetDeviceState返回立即数据,存放到DIMOUSESTATE或DIMOUSESTATE2结构体中:
typedef struct DIMOUSESTATE {
LONG lX; //lX,lY,lZ分别是x,y,z方向移动量
LONG lY;
LONG lZ;
BYTE rgbButtons[4]; //按键状态,最高位1表示被按下,0表示松开
} DIMOUSESTATE, *LPDIMOUSESTATE;
前3个成员分别是鼠标的移动量,后4个字节用于表示按键的状态,这和键盘数据缓冲区的表示方法一样,也是最高位表示状态。对于传统的鼠标,这个数组的前3个字节分别对应于左、右、中键。
DIMOUSESTATE2中数组rgbButtons有8个字节,可以返回8个按键的状态。使用这两个结构体时,只要把结构体的大小告诉函数GetDeviceState,此函数就会根据大小自动判断是前者还是后者。
4)缓冲数据
使用缓冲模式,需要设置缓冲区大小。这和设置键盘数据缓冲区的方法是一样的,缓冲区可以一次读完,也可以分步读取。
可以根据DIDEVICEOBJECTDATA的dwOfs成员判断是哪个设备对象的事件,对于系统鼠标,dwOfs的取值有以下几种:
DIMOFS_BUTTON0 ~ DIMOFS_BUTTON7
DIMOFS_X
DIMOFS_Y
DIMOFS_Z
这个dwOfs表示改变状态的设备对象在DIMOUSESTATE中对应的成员的偏移地址,它表示是哪个设备对象的事件。但是,数据并非保存于DIMOUSESTATE类型的结构体中,而是保存在以下结构体中:
typedef struct DIDEVICEOBJECTDATA {
DWORD dwOfs;
DWORD dwData;
DWORD dwTimeStamp;
DWORD dwSequence;
UINT_PTR uAppData;
} DIDEVICEOBJECTDATA, *LPDIDEVICEOBJECTDATA;
typedef const DIDEVICEOBJECTDATA *LPCDIDEVICEOBJECTDATA;
dwData相当于lx、ly、lz或rgbButtons[]中dwOfs指明的那一个,例如,按下鼠标左键,dwOfs= DIMOFS_BUTTON0,而dwData就是rgbButtons[0],知道了是此按键的信息,要判断到底是何事件,同样要判断dwData低字节的最高位是否为1,这一位是1时按键被按下,0表示松开。也就是要判断dwData & 0x80是否非0。
同样,dwOfs= DIMOFS_X时表示是鼠标移动的信息,dwData就是鼠标的移动量。
那么,用缓冲模式获取鼠标的移动信息是否合适呢?显然比较麻烦,还需要累加每个移动事件的移动量。而使用立即模式则比较合适,可以直接得到从上次读取到这次之间发生的位移量。但如何保证每个按键事件被获取呢?DirectInput允许同时使用缓冲模式和立即模式,在这里可以用缓冲模式读取按键事件,而用立即模式读取鼠标位移量。
6.鼠标丢失
当用户切换到其他应用程序,甚至是激活菜单时,Windows会强制使应用程序失去设备访问权。GetDeviceState或GetDeviceData都会返回DIERR_INPUTLOST错误。这时需要尝试用Acquire函数再次获取设备。
7.关闭DirectInput
使用完DirectInput,也必须释放这些COM对象:
#define SAFE_RELEASE(p) if(p) {p->Release();p=NULL;}
m_pDInputMouse->Unacquire();
SAFE_RELEASE(m_pDInputMouse);
SAFE_RELEASE(m_pDInput);
8.3.2 使用鼠标控制
以下代码实现了鼠标对简单游戏的控制,鼠标控制战机的移动,鼠标左键按下表示发射子弹。
【例8-2】实现鼠标控制:
#include "stdafx.h"
#include "鼠标获取.h"
#include <dinput.h>
#include <dinputd.h>
#define MAX_LOADSTRING 100
#define SAFE_RELEASE(p) if(p) {p->Release();p=NULL;}
// 全局变量:
HINSTANCE hInst; // 当前实例
TCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
TCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
HDC memDC; //定义一个兼容DC
HBITMAP hBitmapBackground; //保存背景图
HBITMAP hBitmapFlight; //保存飞机图
HBITMAP hBitmapFlightC; //保存飞机遮罩
HBITMAP hBitmapBullet; //保存子弹图
BITMAP bmp;
//在DirectX9.0 SDK中的DirectInput实际是8.0版本
LPDIRECTINPUT8 m_pDInput;
DIMOUSESTATE m_MouseState;
LPDIRECTINPUTDEVICE8 m_pDInputMouse;
DIMOUSESTATE m_MouseStateOld;
int x,y,xlast,ylast;
int cx,cy;
struct bullet
{
int x;
int y;
BOOL exist;
};
bullet b[5];
int bcount=0;
int i;
// 此代码模块中包含的函数的前向声明
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
LONG GetLX() {return m_MouseState.lX;};
LONG GetLY() {return m_MouseState.lY;};
LONG GetLZ() {return m_MouseState.lZ;};
LRESULT Update(void)
{
memcpy(&m_MouseStateOld,&m_MouseState,sizeof(m_MouseState));
if(DI_OK!=m_pDInputMouse->GetDeviceState(sizeof(m_MouseState),
&m_MouseState))
{
memset(&m_MouseState,0,sizeof(m_MouseState)); //清除上一次状态
m_pDInputMouse->Acquire();
}
return DI_OK;
}
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
......
}
// 注册窗口类
ATOM MyRegisterClass(HINSTANCE hInstance)
{
......
}
//保存实例句柄并创建主窗口
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
......
}
// 处理主窗口的消息
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_COMMAND:
......
break;
case WM_CREATE:
hBitmapBackground = NULL;
hBitmapFlightC = NULL;
hBitmapFlight = NULL;
hBitmapBullet = NULL;
//创建DirectInput8对象
if(DI_OK!=DirectInput8Create( hInst,DIRECTINPUT_VERSION,
IID_IDirectInput8,(LPVOID*)&m_pDInput,NULL))
MessageBox(hWnd,"创建DirectInput 对象失败!","ERROR",
MB_ICONERROR|MB_OK);
//创建鼠标设备
if(DI_OK!=m_pDInput->CreateDevice(GUID_SysMouse,
&m_pDInputMouse,NULL))
MessageBox(hWnd,"创建鼠标设备失败!","ERROR",
MB_ICONERROR|MB_OK);
//设置数据格式
if(DI_OK!= m_pDInputMouse->SetDataFormat(&c_dfDIMouse))
MessageBox(hWnd,"设置鼠标数据格式失败!","ERROR",
MB_ICONERROR|MB_OK);
//设置协调层级
m_pDInputMouse->SetCooperativeLevel(hWnd,
DISCL_EXCLUSIVE|DISCL_FOREGROUND);
m_pDInputMouse->Acquire(); //如果失败,获取数据时再尝试获取
memset(&m_MouseState,0,sizeof(m_MouseState));
memset(&m_MouseStateOld,0,sizeof(m_MouseState));
hBitmapBackground = (HBITMAP)LoadImage(
hInst,"background.bmp" ,IMAGE_BITMAP, 756,512,
LR_LOADFROMFILE|LR_CREATEDIBSECTION);//背景图
hBitmapFlight = (HBITMAP)LoadImage(
hInst,"flight.bmp" ,IMAGE_BITMAP, 163,108,
LR_LOADFROMFILE|LR_CREATEDIBSECTION);//飞机图
hBitmapFlightC = (HBITMAP)LoadImage(
hInst,"flightc.bmp" ,IMAGE_BITMAP, 163,108,
LR_LOADFROMFILE|LR_CREATEDIBSECTION);//飞机遮罩图
hBitmapBullet = (HBITMAP)LoadImage(
hInst,"bullet.bmp" ,IMAGE_BITMAP, 32,7,
LR_LOADFROMFILE|LR_CREATEDIBSECTION);//子弹图
memDC = CreateCompatibleDC(GetDC(hWnd));//创建内存DC
SelectObject(memDC,hBitmapBackground);
RECT rect;
GetClientRect(hWnd,&rect);
//ClientToScreen(hWnd,&rect);
ClipCursor(&rect);
x = ((rect.right-rect.left) - 100)/2;
y = ((rect.bottom-rect.top) - 100)/2;
cx = x + 81/2;
cy = y + 81/2;
POINT p;
p.x = cx;
p.y = cy;
ClientToScreen(hWnd,&p);
ShowCursor(false);
SetCursorPos(p.x,p.y);
SetTimer(hWnd,1,100,NULL);
break;
case WM_TIMER:
GetClientRect(hWnd, &rect);
Update();
switch(wParam)
{
case 1:
x += GetLX();
y += GetLY();
if(x<rect.left)
x = rect.left;
if(x>rect.right-163)
x = rect.right-163;
if(y<rect.top)
y = rect.top;
if(y>rect.bottom-108)
y = rect.bottom-108;
SelectObject(memDC,hBitmapBackground);
BitBlt(GetDC(hWnd),0,0,rect.right,rect.bottom,
memDC,0,0,SRCCOPY);
SelectObject(memDC,hBitmapFlightC);
BitBlt(GetDC(hWnd),x,y,163,108,memDC,0,0,SRCAND);
SelectObject(memDC,hBitmapFlight);
BitBlt(GetDC(hWnd),x,y,163,108,memDC,0,0,SRCPAINT);
xlast = x;
ylast = y;
if(m_MouseState.rgbButtons[0] & 0x80)
{
for(i=0;i<5;i++)
{
if(b[i].exist == false)
{
b[i].x = x;
b[i].y = y+30;
b[i].exist = true;
bcount++;
break;
}
}
}
if(bcount != 0)
for(i=0;i<5;i++)
{
SelectObject(memDC,hBitmapBullet);
BitBlt(GetDC(hWnd),b[i].x,b[i].y,32,7,
memDC,0,0,SRCCOPY);
b[i].x -=17;
if(b[i].x<-30)
{
b[i].exist = false;
bcount--;
}
}
InvalidateRect(hWnd,&rect,FALSE);
break;
}
break;
case WM_KEYDOWN:
if(wParam == VK_ESCAPE) PostMessage(hWnd,WM_CLOSE,NULL,NULL);
break;
case WM_DESTROY:
ReleaseDC(hWnd,memDC);
m_pDInputMouse->Unacquire();
SAFE_RELEASE(m_pDInputMouse);
SAFE_RELEASE(m_pDInput);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}







