1. 程式人生 > >Windows程式設計 DirectInput 滑鼠和鍵盤的輸入

Windows程式設計 DirectInput 滑鼠和鍵盤的輸入

版本:VS2015 語言:C++

書的第八章是一些數學的知識,以及一個圖形庫的建立。數學知識是有必要看一看的,我這裡就不做多的介紹了,圖形庫的話反正你現在的win7+系統上也執行不了,看看就好。因為雖然這本書(《Windows遊戲程式設計大師技巧》)非常的經典,但是程式碼都是比較老的,很多都已經過時了不能執行,所以我們要明確我們的目的,學好基礎知識,編寫一下程式練練手,熟悉熟悉Direct的流程以及原理,至於正真的想要運用的話,憑著這些知識學習最新的dx,或者直接上引擎,研究引擎中的程式碼。

好了,說了這麼多,其實這本書就是為了入門。

今天講的是第九章的內容,主要實現使用鍵盤和滑鼠控制。

首先是基礎的程式碼:

#define KEYDOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)	//判斷當前的按鍵是否被按下
#define DDRAW_INIT_STRUCT(ddsd) { memset(&ddsd, 0, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); }
#define _RGB32BIT(a, r, g, b) ((b) + (g << 8) + (r << 16) + (a << 24))

HWND main_window_handle = NULL;	//當前視窗
LPDIRECTDRAW7 lpdd = NULL;	//Direct7物件,下稱d7

LPDIRECTDRAWSURFACE7 lpddsprimary = NULL;	//主顯示錶面指標
LPDIRECTDRAWSURFACE7 lpddsback = NULL;	//後備顯示錶面
DDSURFACEDESC2 ddsd;	//主顯示錶面的描述

int SCREEN_WIDTH = 640;	//顯示寬度
int SCREEN_HEIGHT = 480;	//顯示高度
int SCREEN_BPP = 32;	//色深,現在的機子只能設定為32位,書上可能還是8位的

int CharPosX = 200;	//當前顯示人物的x座標
int CharPosY = 200;	//當前任務顯示的y座標

// 彈出訊息
void popMessage(LPWSTR str)
{
	MessageBox(main_window_handle, str, TEXT("提示"), MB_OK);
}

// 32位畫素上色
void Plot_Pixel_Fast32_2(int x, int y, int red, int green, int blue, int alpha, UINT* video_buffer, int lpitch)
{
	video_buffer[x + y * (lpitch >> 2)] = (UINT)(_RGB32BIT(alpha, red, green, blue));	//使用巨集直接寫,有點區別的是lpitch需要除以4,因為lpitch算的是橫向的位元組數,而我們把主介面的記憶體弄成UINT型,是32位、4個位元組的,上一節中是我理解的不夠深刻
}


// 遊戲初始化
int Game_Init(void* params = NULL)
{
	// 基礎設定
	if (FAILED(DirectDrawCreateEx(NULL, (void**)&lpdd, IID_IDirectDraw7, NULL)))	//獲取d7物件
		return 0;
	if (FAILED(lpdd->SetCooperativeLevel(main_window_handle,
		DDSCL_FULLSCREEN | DDSCL_ALLOWMODEX | DDSCL_EXCLUSIVE | DDSCL_ALLOWREBOOT
		)))	//跟windows協作等級設定為全屏,這是最常用的引數
		return 0;

	if (FAILED(lpdd->SetDisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, 0, 0)))	//設定顯示模式,如果設定為8位會直接出錯
		return 0;

	// 開始建立顯示主介面
	memset(&ddsd, 0, sizeof(ddsd));
	ddsd.dwSize = sizeof(ddsd);
	ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;	//表明ddsCaps是個有效成員,並且擁有後備的緩衝
	ddsd.dwBackBufferCount = 2;	//表明有一個緩衝
	ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | //表明該介面是主介面
							DDSCAPS_COMPLEX | //表明擁有緩衝鏈
							DDSCAPS_FLIP;	//表明是反正結構的一部分,上面的引數相當於是有緩衝,而這個引數表明可以切換緩衝

	if (FAILED(lpdd->CreateSurface(&ddsd, &lpddsprimary, NULL)))	//根據介面描述建立主介面
	{
		popMessage(TEXT("主表面創建出錯"));
		return 0;
	}

	// 開始建立後備介面
	ddsd.ddsCaps.dwCaps = DDSCAPS_BACKBUFFER;	//表明該介面是後備介面
	if (FAILED(lpddsprimary->GetAttachedSurface(&ddsd.ddsCaps, &lpddsback)))	//通過主介面創建出備用表面
	{
		popMessage(TEXT("建立備用表面出錯了"));
		return 0;
	}

	return 1;
}

// 遊戲結束
int Game_Shutdown(void* params = NULL)
{
	//// 釋放初始化時建立的物件

	if (NULL != lpddsprimary)
	{
		lpddsprimary->Release();
		lpddsprimary = NULL;
	}

	if (NULL != lpdd)	//d7物件不為空的情況下釋放
	{
		lpdd->Release();
		lpdd = NULL;
	}

	return 1;
}

// 遊戲主迴圈
int Game_Main(void* params = NULL)
{
	// 判斷是否要退出
	if (KEYDOWN(VK_ESCAPE))
		PostMessage(main_window_handle, WM_CLOSE, 0, 0);

	// 初始化主介面描述
	DDRAW_INIT_STRUCT(ddsd);

	if (FAILED(lpddsback->Lock(NULL, &ddsd, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL)))	//有備用表面時用備用表面加鎖
	{
		popMessage(TEXT("LOCK 出錯了"));
	}

	// 白色的背景
	UINT *video_buffer = (UINT*)ddsd.lpSurface;
	for (int x = 0; x < 640; ++x)
		for (int y = 0; y < 480; ++y)
			Plot_Pixel_Fast32_2(x, y, 255, 255, 255, 128, video_buffer, ddsd.lPitch);

	//畫人物
	//UCHAR *video_buffer = (UCHAR*)ddsd.lpSurface;
	for (int x = 0+CharPosX; x < 64+CharPosX; ++x)
		for (int y = 0 + CharPosY; y < 64 + CharPosY; ++y)
		{
			if (x<0 || x>SCREEN_WIDTH - 1 || y<0 || y>SCREEN_HEIGHT-1)	//超出螢幕邊緣的時候不畫
				continue;
			Plot_Pixel_Fast32_2(x, y, 0, 255, 0, 128, video_buffer, ddsd.lPitch);
		}

	if (FAILED(lpddsback->Unlock(NULL)))	//解鎖
	{
		popMessage(TEXT("UNLOCK 出錯了"));
	}

	while (FAILED(lpddsprimary->Flip(NULL, DDFLIP_WAIT)));	//切換介面,這邊的while不是很懂,應該每次只會呼叫一次

	return 1;
}

// 訊息處理函式
LRESULT CALLBACK WindowProc(HWND hwnd,
	UINT msg,
	WPARAM wParam,
	LPARAM IParam)
{
	switch (msg)
	{
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		DefWindowProc(hwnd, msg, wParam, IParam);	//自動處理其他的訊息
		break;
	}
	return (1);
}

// 主函式,程式入口
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPWSTR    lpCmdLine,
	_In_ int       nCmdShow)
{
	// 建立視窗類
	WNDCLASSEX wndclass;
	wndclass.cbSize = sizeof(WNDCLASSEX);
	wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_DBLCLKS;	//視窗的樣式:改變寬度重新整理、改變高度重新整理、分配裝置描述表、雙擊資訊
	wndclass.lpfnWndProc = WindowProc;	//回撥函式
	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = 0;
	wndclass.hInstance = hInstance;
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);	//工作列上的圖示
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);	//游標的讀取
	wndclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);	//視窗背景
	wndclass.lpszMenuName = NULL;
	wndclass.lpszClassName = TEXT("MyManyTimesWindow");	//視窗的名字
	wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);	//應用上的圖示
	if (!RegisterClassEx(&wndclass))
		return 0;

	// 建立視窗,上面的視窗類是一個模版,可以根據上面的模版建立多個視窗,但請注意第二個引數
	HWND hwnd = CreateWindowEx(NULL,//WS_EX_TOPMOST,	//視窗特性,註釋裡設定為永遠在最上方顯示
		TEXT("MyManyTimesWindow"),	//視窗名稱,一定要和視窗類的lpszClassName對應
		TEXT("我與DDraw已經很多次了"),	//標題
		WS_POPUP | WS_VISIBLE,	//無邊框樣式配合下面的尺寸實現全屏顯示
		0, 0,	//左上角座標
		SCREEN_WIDTH, SCREEN_HEIGHT,
		NULL,	//父視窗控制代碼,如果是桌面則為NULL
		NULL,	//選單視窗控制代碼
		hInstance,	//應用程式例項
		NULL	//高階特性
		);
	if (!hwnd)	//建立失敗返回
		return 0;

	main_window_handle = hwnd;

	ShowWindow(hwnd, nCmdShow);	//顯示視窗
	UpdateWindow(hwnd);	//重新整理視窗

	MSG msg;	//訊息快取

	srand(GetTickCount());	//隨機一個種子

	if (0 == Game_Init())	//遊戲初始化
		return 0;

	// 進入主迴圈
	while (true)
	{
		DWORD start_time = GetTickCount();	//獲取當前時間

		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))	//有訊息事件,注意最後一個引數,如果設定為PM_NOREMOVE的話不會銷燬訊息佇列中的訊息
		{
			if (msg.message == WM_QUIT)
				break;
			TranslateMessage(&msg);	//轉譯訊息
			DispatchMessage(&msg);	//將訊息傳送給WindowProc函式處理

		}
		else	//沒有訊息
		{
			//遊戲主迴圈
			Game_Main();

			// 延時程式碼,鎖定30幀
			while ((GetTickCount() - start_time) < 33);

		}
	}

	Game_Shutdown();	//遊戲結束

	return msg.wParam;
}

注意匯入庫檔案。嘛,都是以前的知識,複製貼上過來就行了,效果:

顯示了一個綠色的方型,這就是我們要操控的勇士了。來吧,接下來要達到的效果就是使用wasd,控制左右移動,首先我們加上速度的全域性變數(放在角色位置變數的下放):

int CharSpdX = 0;	//當前顯示人物x方向的速度
int CharSpdY = 0;	//當前顯示人物y方向的速度

然後匯入檔案input.lib和input8.lib,幷包含dinput.h標頭檔案。

在檔案的全域性處加上巨集和變數:

#define DIKEYDOWN(data, n) (data[n] & 0x80)
HINSTANCE h_instance = NULL;	//當前應用程式的控制代碼,玩家自己在main函式中設定一下
LPDIRECTINPUT8 lpdi;	//輸入物件
LPDIRECTINPUTDEVICE8W lpdikey = NULL;	//鍵盤裝置
UCHAR keyboard_state[256];	//鍵盤當前的狀態

初始化函式中新增獲取輸入物件和輸入裝置等的程式碼,錯誤處理去掉了,玩家自己新增一下:

// 開始建立輸入物件
FAILED(DirectInput8Create(h_instance, DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&lpdi, NULL));
FAILED(lpdi->CreateDevice(GUID_SysKeyboard, &lpdikey, NULL));	//建立鍵盤裝置FAILED(lpdikey->SetCooperativeLevel(main_window_handle, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE));	//設定協作等級,鍵盤和滑鼠設定為這裡的可以後臺接受和非獨佔,而遊戲手柄則要設定為獨佔
FAILED(lpdikey->SetDataFormat(&c_dfDIKeyboard)));	//設定鍵盤的資料格式FAILED(lpdikey->Acquire());	//獲取鍵盤

然後在遊戲Shutdown銷燬各個物件:

if (lpdikey)	//釋放鍵盤相關物件
	lpdikey->Unacquire();
if (lpdikey)
	lpdikey->Release();
if (lpdi)
	lpdi->Release();

最後是遊戲主迴圈,哈哈,激動人心的時候到了:

// 獲取鍵盤狀態
if (lpdikey->GetDeviceState(sizeof(UCHAR[256]), (LPVOID)keyboard_state))
{
	popMessage(TEXT("獲取鍵盤狀態出錯了"));
}

// 處理按鍵
if (DIKEYDOWN(keyboard_state, DIK_W))
	CharSpdY = -3;
else if (DIKEYDOWN(keyboard_state, DIK_S))
	CharSpdY = 3;
else
	CharSpdY = 0;

if (DIKEYDOWN(keyboard_state, DIK_D))
	CharSpdX = 3;
else if (DIKEYDOWN(keyboard_state, DIK_A))
	CharSpdX = -3;
else
	CharSpdX = 0;

// 調整人物的位置
CharPosX += CharSpdX;
CharPosY += CharSpdY;

這段程式碼放在重新整理白色背景的上面,然後可以試試效果了。我們的勇者是不是可以上下左右移動了?嗯,太棒了!

下面是滑鼠資訊獲取的方法。

嗯,首先要說明一下,DirectX中的滑鼠跟Windows裡的滑鼠其實沒有什麼關係,Windows滑鼠就是你白色的指標,它就是在螢幕中的某個位置,而dx中滑鼠裝置的意思是真實裝置操控時所產生的資訊。不是很理解的話,我會在程式中說到這個問題。

上程式碼(初始化和釋放就不說了,跟鍵盤的方法差不多,換成mouse相關的就OK了):

//全域性變數
LPDIRECTINPUTDEVICE8W lpdimouse = NULL;	//滑鼠裝置
DIMOUSESTATE2 mouse_state;	//滑鼠的狀態
int MousePosX = 0;	//dx計算出來的滑鼠位置
int MousePosY = 0;

// 這裡是遊戲主迴圈裡的內容,請放在獲取鍵盤狀態的下面
// 處理滑鼠
if (FAILED(lpdimouse->GetDeviceState(sizeof(DIMOUSESTATE), (LPVOID)&mouse_state)))
{
	popMessage(TEXT("獲取滑鼠狀態出錯了"));
}
bool isMouseProccess = false;
MousePosX += mouse_state.lX;	//計算當前滑鼠的位置,獲得的引數是當前滑鼠位置與上一幀位置的差值
MousePosY += mouse_state.lY;
SetCursorPos(MousePosX, MousePosY);	//設定Windows中滑鼠的位置,如果不設定的話,可能會出現計算出來的位置與當前顯示位置不匹配的情況,一定要記得Windows滑鼠的位置和dx中滑鼠的位置是隔離的	
if (DIKEYDOWN(mouse_state.rgbButtons, 0))	//當滑鼠按下的時候,人物瞬移到對應位置
{
	isMouseProccess = true;
	CharPosX = MousePosX - 32;
	CharPosY = MousePosY - 32;
}

好了,現在按下滑鼠,角色就跟這滑鼠移動了,效果是不是很棒啊,哈哈。

至於手柄,現在手上也沒有手柄,所以暫時就算了,這類其他的裝置也就是比滑鼠鍵盤麻煩了點,思路還是一樣的。

下一節會講聲音相關的內容,而講完聲音,dx的內容貌似就是已經完了,如果有好玩的話寫一點後面章節的知識,一般般的話就算了,下一節就是最後一節了。