相信大家都熟悉《仙劍奇俠傳98柔情版》的人機交互方式,用的僅僅是鍵盤。在那個物質並不充裕的時代,一臺配置並不高的電腦,一款名叫《仙劍奇俠傳》的遊戲,卻能承載一代人對夢想的追逐。雖然在這十幾年間,各種新潮的遊戲層出不窮,但是《仙劍奇俠傳98柔情版》,作爲國產單機遊戲無法被超越的傳奇,已經永遠留在了我們這代人的心中。那是一個永遠無法被取代的,最最唯美的夢。
從這節筆記開始,我們就開始講解遊戲輸入消息的處理,開始人機交互,開始真正意義上的遊戲開發。
這一節裏我們主要講解鍵盤消息的處理。
鍵盤作爲基本的輸出裝置,在每一款優秀的遊戲研發中都有着至關重要的地位(當然我們在這裏暫時不討論ios和android平臺)。
首先我們對Windows系統下鍵盤的基本概念及鍵盤消息的處理方式做一個簡單介紹。
1.虛擬鍵碼
所有鍵盤的按鍵都被定義出一組通用的“虛擬鍵碼”,也就是說在Windows系統下所有按鍵都會被視爲虛擬鍵(包含鼠標鍵在內),而每一個虛擬鍵都有其對應的一個虛擬鍵碼。
2.鍵盤消息
Windows系統是一個消息驅動的環境,一旦使用者在鍵盤上進行輸入操作,那麼系統便會接收到對應的鍵盤消息,下面我們列出最常見的3種鍵盤消息:
WM_KEYDOWN 按下按鍵的消息
WM_KEYUP 鬆開按鍵消息
WM_CHAR 字符消息
當某一按鍵被按下時,伴隨着這個操作所產生的是以虛擬鍵碼類型傳送的WM_KEYDOWN與WM_KEYUP消息。當程序接收到這些消息時。便可由虛擬鍵碼的信息來得知是哪個按鍵被按下。
此外,WM_CHAR則是當按下的按鍵爲定義於ASCⅡ中的可打印字符時,便發出此字符消息。
3.系統鍵
Windows系統本身定義了一組“系統鍵”,這些按鍵通常都是【Alt】與其他按鍵的組合,系統鍵對於Windows系統本身有一些特定的作用,Windows中也特別針對系統鍵定出了下面的相關消息
WM_SYSKEYDOWN 按下系統鍵消息
WM_SYSKEYUP 松下系統鍵消息
消息代號中加入“SYS”代表系統鍵按下消息,然而實際上程序中很少處理系統鍵消息,因爲當這類消息發生時Windows會自行處理並進行相應的工作。
以上便是鍵盤在Windows系統下關於其定義及輸出處理的一些基本概念。
下面我們來詳細講解這節筆記的主角——鍵盤消息處理。
鍵盤消息同樣是在消息處理函數中加來以定義處理的,按下按鍵事件一定會緊隨着一個鬆開按鍵的事件,因此WM_KEYDOWN與WM_KEYUP兩種消息必須是成對發生的。但通常僅在程序中對WM_KEYDOWN消息進行處理,而忽略WM_KEYUP消息。
我們觀察消息處理函數中所輸入的兩個參數wParam和lParam:
- LRESULT CALLBACK WndProc(HWND hWnd,
- UINT message,
- WPARAM wParam,
- LPARAM lParam)
當鍵盤消息觸發時,wParam的值爲按下按鍵的虛擬鍵碼,Windows中所定義的虛擬鍵碼是以“VK_”開頭的,lParam則儲存按鍵的相關狀態信息,因此,如果程序要對使用者的鍵盤輸入操作進行處理,那麼消息處理函數的內容可以定義如下:
- LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
- {
- switch (message)
- {
- case WM_KEYDOWN: //按下鍵盤消息
- switch (wParam)
- {
- case VK_ESCAPE: //按下【Esc】鍵
- //定義消息處理程序
- break;
- case VK_UP: //按下【↑】鍵
- //定義消息處理程序
- break;
- case WM_DESTROY: //窗口結束消息
- PostQuitMessage(0);
- break;
- default: //其他消息
- return DefWindowProc(hWnd, message, wParam, lParam);
- }
- return 0;
- }
針對這個消息處理函數中鍵盤消息處理的程序關鍵說明如下:
<1>第5行:定義處理“WM_KEYDOWN”消息。
<2>第6行:以“switch”敘述判斷“wParam”的值來得知哪個按鍵被按下,並運行對應“case”中的按鍵消息處理程序。
同樣的,我們用一個實例來讓大家熟悉和實踐一下本節的知識。
這個範例會讓玩家以【↑】【↓】【←】【→】鍵進行輸入,控制畫面中人物的移動,這裏使用了人物在4個不同方向上走動的連續圖案
廢話也不多說了,直接上詳細註釋的代碼:
- #include "stdafx.h"
- #include <stdio.h>
- //全局變量聲明
- HINSTANCE hInst;
- HBITMAP girl[4],bg;
- HDC hdc,mdc,bufdc;
- HWND hWnd;
- DWORD tPre,tNow;
- int num,dir,x,y; //x,y變量爲人物貼圖座標,dir爲人物移動方向,這裏我們中以0,1,2,3代表人物上,下,左,右方向上的移動:num爲連續貼圖中的小圖編號
- //全局函數聲明
- ATOM MyRegisterClass(HINSTANCE hInstance);
- BOOL InitInstance(HINSTANCE, int);
- LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
- void MyPaint(HDC hdc);
- //****WinMain函數,程序入口點函數***********************
- int APIENTRY WinMain(HINSTANCE hInstance,
- HINSTANCE hPrevInstance,
- LPSTR lpCmdLine,
- int nCmdShow)
- {
- MSG msg;
- MyRegisterClass(hInstance);
- //初始化
- if (!InitInstance (hInstance, nCmdShow))
- {
- return FALSE;
- }
- GetMessage(&msg,NULL,NULL,NULL); //初始化msg
- //消息循環
- while( msg.message!=WM_QUIT )
- {
- if( PeekMessage( &msg, NULL, 0,0 ,PM_REMOVE) )
- {
- TranslateMessage( &msg );
- DispatchMessage( &msg );
- }
- else
- {
- tNow = GetTickCount();
- if(tNow-tPre >= 40)
- MyPaint(hdc);
- }
- }
- return msg.wParam;
- }
- //****設計一個窗口類,類似填空題,使用窗口結構體*******************
- ATOM MyRegisterClass(HINSTANCE hInstance)
- {
- WNDCLASSEX wcex;
- wcex.cbSize = sizeof(WNDCLASSEX);
- wcex.style = CS_HREDRAW | CS_VREDRAW;
- wcex.lpfnWndProc = (WNDPROC)WndProc;
- wcex.cbClsExtra = 0;
- wcex.cbWndExtra = 0;
- wcex.hInstance = hInstance;
- wcex.hIcon = NULL;
- wcex.hCursor = NULL;
- wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
- wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
- wcex.lpszMenuName = NULL;
- wcex.lpszClassName = "canvas";
- wcex.hIconSm = NULL;
- return RegisterClassEx(&wcex);
- }
- //****初始化函數*************************************
- // 加載位圖並設定各種初始值
- BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
- {
- HBITMAP bmp;
- hInst = hInstance;
- hWnd = CreateWindow("canvas", "繪圖窗口" , WS_OVERLAPPEDWINDOW,
- CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
- if (!hWnd)
- {
- return FALSE;
- }
- MoveWindow(hWnd,10,10,640,480,true);
- ShowWindow(hWnd, nCmdShow);
- UpdateWindow(hWnd);
- hdc = GetDC(hWnd);
- mdc = CreateCompatibleDC(hdc);
- bufdc = CreateCompatibleDC(hdc);
- //建立空的位圖並置入mdc中
- bmp = CreateCompatibleBitmap(hdc,640,480);
- SelectObject(mdc,bmp);
- //設定人物貼圖初始位置和移動方向
- x = 300;
- y = 250;
- dir = 0;
- num = 0;
- //載入各連續移動位圖及背景圖
- girl[0] = (HBITMAP)LoadImage(NULL,"girl0.bmp",IMAGE_BITMAP,440,148,LR_LOADFROMFILE);
- girl[1] = (HBITMAP)LoadImage(NULL,"girl1.bmp",IMAGE_BITMAP,424,154,LR_LOADFROMFILE);
- girl[2] = (HBITMAP)LoadImage(NULL,"girl2.bmp",IMAGE_BITMAP,480,148,LR_LOADFROMFILE);
- girl[3] = (HBITMAP)LoadImage(NULL,"girl3.bmp",IMAGE_BITMAP,480,148,LR_LOADFROMFILE);
- bg = (HBITMAP)LoadImage(NULL,"bg.bmp",IMAGE_BITMAP,640,480,LR_LOADFROMFILE);
- MyPaint(hdc);
- return TRUE;
- }
- //****自定義繪圖函數*********************************
- // 人物貼圖座標修正及窗口貼圖
- void MyPaint(HDC hdc)
- {
- int w,h;
- //先在mdc中貼上背景圖
- SelectObject(bufdc,bg);
- BitBlt(mdc,0,0,640,480,bufdc,0,0,SRCCOPY);
- //按照目前的移動方向取出對應人物的連續走動圖,並確定截取人物圖的寬度與高度
- SelectObject(bufdc,girl[dir]);
- switch(dir)
- {
- case 0:
- w = 55;
- h = 74;
- break;
- case 1:
- w = 53;
- h = 77;
- break;
- case 2:
- w = 60;
- h = 74;
- break;
- case 3:
- w = 60;
- h = 74;
- break;
- }
- //按照目前的X,Y的值在mdc上進行透明貼圖,然後顯示在窗口畫面上
- BitBlt(mdc,x,y,w,h,bufdc,num*w,h,SRCAND);
- BitBlt(mdc,x,y,w,h,bufdc,num*w,0,SRCPAINT);
- BitBlt(hdc,0,0,640,480,mdc,0,0,SRCCOPY);
- tPre = GetTickCount(); //記錄此次繪圖時間
- num++;
- if(num == 8)
- num = 0;
- }
- //****消息處理函數***********************************
- // 1.按下【Esc】鍵結束程序
- // 2.按下方向鍵重設貼圖座標
- LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
- {
- switch (message)
- {
- case WM_KEYDOWN: //按下鍵盤消息
- //判斷按鍵的虛擬鍵碼
- switch (wParam)
- {
- case VK_ESCAPE: //按下【Esc】鍵
- PostQuitMessage( 0 ); //結束程序
- break;
- case VK_UP: //按下【↑】鍵
- //先按照目前的移動方向來進行貼圖座標修正,並加入人物往上移動的量(每次按下一次按鍵移動10個單位),來決定人物貼圖座標的X與Y值,接着判斷座標是否超出窗口區域,若有則再次修正
- switch(dir)
- {
- case 0:
- y -= 10;
- break;
- case 1:
- x -= 1;
- y -= 8;
- break;
- case 2:
- x += 2;
- y -= 10;
- break;
- case 3:
- x += 2;
- y -= 10;
- break;
- }
- if(y < 0)
- y = 0;
- dir = 0;
- break;
- case VK_DOWN: //按下【↓】鍵
- switch(dir)
- {
- case 0:
- x += 1;
- y += 8;
- break;
- case 1:
- y += 10;
- break;
- case 2:
- x += 3;
- y += 6;
- break;
- case 3:
- x += 3;
- y += 6;
- break;
- }
- if(y > 375)
- y = 375;
- dir = 1;
- break;
- case VK_LEFT: //按下【←】鍵
- switch(dir)
- {
- case 0:
- x -= 12;
- break;
- case 1:
- x -= 13;
- y += 4;
- break;
- case 2:
- x -= 10;
- break;
- case 3:
- x -= 10;
- break;
- }
- if(x < 0)
- x = 0;
- dir = 2;
- break;
- case VK_RIGHT: //按下【→】鍵
- switch(dir)
- {
- case 0:
- x += 8;
- break;
- case 1:
- x += 7;
- y += 4;
- break;
- case 2:
- x += 10;
- break;
- case 3:
- x += 10;
- break;
- }
- if(x > 575)
- x = 575;
- dir = 3;
- break;
- }
- break;
- case WM_DESTROY: //窗口結束消息
- int i;
- DeleteDC(mdc);
- DeleteDC(bufdc);
- for(i=0;i<4;i++)
- DeleteObject(girl[i]);
- DeleteObject(bg);
- ReleaseDC(hWnd,hdc);
- PostQuitMessage(0);
- break;
- default: //其他消息
- return DefWindowProc(hWnd, message, wParam, lParam);
- }
- return 0;
- }
程序運行結果如下圖,我們可以用鍵盤操作這個小人的上下左右移動,用Esc退出:
這樣,一個簡單的小遊戲就完成了。
我們也可以通過在消息處理函數中取得按鍵虛擬鍵碼的方式,很簡單地對鍵盤輸入操作進行處理。
筆記十二到這裏就結束了。
本節源代碼請點擊這裏下載: 【Visual C++】Code_Note_12
感謝一直支持【Visual C++】遊戲開發筆記系列專欄的朋友們,也請大家繼續關注我的博客,我一有空就會把自己的學習心得,覺得比較好的知識點寫出來和大家一起分享。
精通遊戲開發的路還很長很長,非常希望能和大家一起交流,共同學習和進步。
大家看過後覺得有啓發的話可以頂一下這篇文章,讓更多的朋友有機會看到它。也希望大家可以多留言來和我探討編程相關的問題。最後,謝謝大家一直的支持~~~
The end