【Visual C++】遊戲開發筆記十二 遊戲輸入消息處理(一) 鍵盤消息處理

相信大家都熟悉《仙劍奇俠傳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:

  1. LRESULT CALLBACK WndProc(HWND hWnd,   
  2. UINT message,   
  3. WPARAM wParam,   
  4. LPARAM lParam)   


當鍵盤消息觸發時,wParam的值爲按下按鍵的虛擬鍵碼,Windows中所定義的虛擬鍵碼是以“VK_”開頭的,lParam則儲存按鍵的相關狀態信息,因此,如果程序要對使用者的鍵盤輸入操作進行處理,那麼消息處理函數的內容可以定義如下:


  1. LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)  
  2. {  
  3.     switch (message)  
  4.     {  
  5.         case WM_KEYDOWN:                  //按下鍵盤消息  
  6.             switch (wParam)   
  7.             {  
  8.                 case VK_ESCAPE:           //按下【Esc】鍵  
  9.                 //定義消息處理程序  
  10.                     break;  
  11.                 case VK_UP:               //按下【↑】鍵  
  12.                 //定義消息處理程序  
  13.                     break;  
  14.         case WM_DESTROY:                    //窗口結束消息  
  15.             PostQuitMessage(0);  
  16.             break;  
  17. default:                            //其他消息  
  18.         return DefWindowProc(hWnd, message, wParam, lParam);  
  19.    }  
  20.    return 0;  
  21. }  


針對這個消息處理函數中鍵盤消息處理的程序關鍵說明如下:

<1>第5行:定義處理“WM_KEYDOWN”消息。

<2>第6行:以“switch”敘述判斷“wParam”的值來得知哪個按鍵被按下,並運行對應“case”中的按鍵消息處理程序。



同樣的,我們用一個實例來讓大家熟悉和實踐一下本節的知識。

這個範例會讓玩家以【↑】【↓】【←】【→】鍵進行輸入,控制畫面中人物的移動,這裏使用了人物在4個不同方向上走動的連續圖案









廢話也不多說了,直接上詳細註釋的代碼:


  1. #include "stdafx.h"  
  2. #include <stdio.h>  
  3.   
  4. //全局變量聲明  
  5. HINSTANCE hInst;  
  6. HBITMAP girl[4],bg;  
  7. HDC     hdc,mdc,bufdc;  
  8. HWND    hWnd;  
  9. DWORD   tPre,tNow;  
  10. int     num,dir,x,y;       //x,y變量爲人物貼圖座標,dir爲人物移動方向,這裏我們中以0,1,2,3代表人物上,下,左,右方向上的移動:num爲連續貼圖中的小圖編號  
  11.   
  12. //全局函數聲明  
  13. ATOM                MyRegisterClass(HINSTANCE hInstance);  
  14. BOOL                InitInstance(HINSTANCEint);  
  15. LRESULT CALLBACK    WndProc(HWNDUINTWPARAMLPARAM);  
  16. void                MyPaint(HDC hdc);  
  17.   
  18. //****WinMain函數,程序入口點函數***********************  
  19. int APIENTRY WinMain(HINSTANCE hInstance,  
  20.                      HINSTANCE hPrevInstance,  
  21.                      LPSTR     lpCmdLine,  
  22.                      int       nCmdShow)  
  23. {  
  24.     MSG msg;  
  25.   
  26.     MyRegisterClass(hInstance);  
  27.   
  28.     //初始化  
  29.     if (!InitInstance (hInstance, nCmdShow))   
  30.     {  
  31.         return FALSE;  
  32.     }  
  33.   
  34.      GetMessage(&msg,NULL,NULL,NULL);            //初始化msg    
  35.     //消息循環  
  36.     while( msg.message!=WM_QUIT )  
  37.     {  
  38.         if( PeekMessage( &msg, NULL, 0,0 ,PM_REMOVE) )  
  39.         {  
  40.             TranslateMessage( &msg );  
  41.             DispatchMessage( &msg );  
  42.         }  
  43.         else  
  44.         {  
  45.             tNow = GetTickCount();  
  46.             if(tNow-tPre >= 40)  
  47.                 MyPaint(hdc);  
  48.         }  
  49.     }  
  50.   
  51.     return msg.wParam;  
  52. }  
  53.   
  54. //****設計一個窗口類,類似填空題,使用窗口結構體*******************  
  55. ATOM MyRegisterClass(HINSTANCE hInstance)  
  56. {  
  57.     WNDCLASSEX wcex;  
  58.   
  59.     wcex.cbSize = sizeof(WNDCLASSEX);   
  60.     wcex.style          = CS_HREDRAW | CS_VREDRAW;  
  61.     wcex.lpfnWndProc    = (WNDPROC)WndProc;  
  62.     wcex.cbClsExtra     = 0;  
  63.     wcex.cbWndExtra     = 0;  
  64.     wcex.hInstance      = hInstance;  
  65.     wcex.hIcon          = NULL;  
  66.     wcex.hCursor        = NULL;  
  67.     wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);  
  68.     wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);  
  69.     wcex.lpszMenuName   = NULL;  
  70.     wcex.lpszClassName  = "canvas";  
  71.     wcex.hIconSm        = NULL;  
  72.   
  73.     return RegisterClassEx(&wcex);  
  74. }  
  75.   
  76. //****初始化函數*************************************  
  77. // 加載位圖並設定各種初始值  
  78. BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)  
  79. {  
  80.     HBITMAP bmp;  
  81.     hInst = hInstance;  
  82.   
  83.     hWnd = CreateWindow("canvas""繪圖窗口" , WS_OVERLAPPEDWINDOW,  
  84.         CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);  
  85.   
  86.     if (!hWnd)  
  87.     {  
  88.         return FALSE;  
  89.     }  
  90.   
  91.     MoveWindow(hWnd,10,10,640,480,true);  
  92.     ShowWindow(hWnd, nCmdShow);  
  93.     UpdateWindow(hWnd);  
  94.   
  95.     hdc = GetDC(hWnd);  
  96.     mdc = CreateCompatibleDC(hdc);  
  97.     bufdc = CreateCompatibleDC(hdc);  
  98.   
  99.   
  100.     //建立空的位圖並置入mdc中  
  101.     bmp = CreateCompatibleBitmap(hdc,640,480);  
  102.     SelectObject(mdc,bmp);  
  103.   
  104.   
  105.     //設定人物貼圖初始位置和移動方向  
  106.     x = 300;  
  107.     y = 250;  
  108.     dir = 0;  
  109.     num = 0;  
  110.   
  111.     //載入各連續移動位圖及背景圖  
  112.     girl[0] = (HBITMAP)LoadImage(NULL,"girl0.bmp",IMAGE_BITMAP,440,148,LR_LOADFROMFILE);  
  113.     girl[1] = (HBITMAP)LoadImage(NULL,"girl1.bmp",IMAGE_BITMAP,424,154,LR_LOADFROMFILE);  
  114.     girl[2] = (HBITMAP)LoadImage(NULL,"girl2.bmp",IMAGE_BITMAP,480,148,LR_LOADFROMFILE);  
  115.     girl[3] = (HBITMAP)LoadImage(NULL,"girl3.bmp",IMAGE_BITMAP,480,148,LR_LOADFROMFILE);  
  116.     bg = (HBITMAP)LoadImage(NULL,"bg.bmp",IMAGE_BITMAP,640,480,LR_LOADFROMFILE);  
  117.   
  118.     MyPaint(hdc);  
  119.   
  120.     return TRUE;  
  121. }  
  122.   
  123. //****自定義繪圖函數*********************************  
  124. // 人物貼圖座標修正及窗口貼圖  
  125. void MyPaint(HDC hdc)  
  126. {  
  127.     int w,h;  
  128.   
  129.     //先在mdc中貼上背景圖  
  130.     SelectObject(bufdc,bg);  
  131.     BitBlt(mdc,0,0,640,480,bufdc,0,0,SRCCOPY);  
  132.   
  133.     //按照目前的移動方向取出對應人物的連續走動圖,並確定截取人物圖的寬度與高度  
  134.     SelectObject(bufdc,girl[dir]);  
  135.     switch(dir)  
  136.     {  
  137.         case 0:  
  138.             w = 55;  
  139.             h = 74;  
  140.             break;  
  141.         case 1:  
  142.             w = 53;  
  143.             h = 77;  
  144.             break;  
  145.         case 2:  
  146.             w = 60;  
  147.             h = 74;  
  148.             break;  
  149.         case 3:  
  150.             w = 60;  
  151.             h = 74;  
  152.             break;  
  153.     }  
  154.     //按照目前的X,Y的值在mdc上進行透明貼圖,然後顯示在窗口畫面上  
  155.     BitBlt(mdc,x,y,w,h,bufdc,num*w,h,SRCAND);  
  156.     BitBlt(mdc,x,y,w,h,bufdc,num*w,0,SRCPAINT);  
  157.       
  158.     BitBlt(hdc,0,0,640,480,mdc,0,0,SRCCOPY);  
  159.   
  160.     tPre = GetTickCount();         //記錄此次繪圖時間  
  161.   
  162.     num++;  
  163.     if(num == 8)  
  164.         num = 0;  
  165.   
  166. }  
  167.   
  168. //****消息處理函數***********************************  
  169. // 1.按下【Esc】鍵結束程序  
  170. // 2.按下方向鍵重設貼圖座標  
  171. LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)  
  172. {  
  173.     switch (message)  
  174.     {  
  175.         case WM_KEYDOWN:         //按下鍵盤消息  
  176.             //判斷按鍵的虛擬鍵碼  
  177.             switch (wParam)   
  178.             {  
  179.                 case VK_ESCAPE:           //按下【Esc】鍵  
  180.                     PostQuitMessage( 0 );  //結束程序  
  181.                     break;  
  182.                 case VK_UP:               //按下【↑】鍵  
  183.                     //先按照目前的移動方向來進行貼圖座標修正,並加入人物往上移動的量(每次按下一次按鍵移動10個單位),來決定人物貼圖座標的X與Y值,接着判斷座標是否超出窗口區域,若有則再次修正  
  184.                     switch(dir)  
  185.                     {  
  186.                         case 0:   
  187.                             y -= 10;  
  188.                             break;  
  189.                         case 1:  
  190.                             x -= 1;  
  191.                             y -= 8;  
  192.                             break;  
  193.                         case 2:   
  194.                             x += 2;  
  195.                             y -= 10;  
  196.                             break;  
  197.                         case 3:  
  198.                             x += 2;  
  199.                             y -= 10;  
  200.                             break;  
  201.                     }  
  202.                     if(y < 0)  
  203.                         y = 0;  
  204.                     dir = 0;  
  205.                     break;  
  206.                 case VK_DOWN:             //按下【↓】鍵  
  207.                     switch(dir)  
  208.                     {  
  209.                         case 0:  
  210.                             x += 1;  
  211.                             y += 8;  
  212.                             break;  
  213.                         case 1:  
  214.                             y += 10;  
  215.                             break;  
  216.                         case 2:  
  217.                             x += 3;  
  218.                             y += 6;  
  219.                             break;  
  220.                         case 3:  
  221.                             x += 3;  
  222.                             y += 6;  
  223.                             break;  
  224.                     }  
  225.   
  226.                     if(y > 375)  
  227.                         y = 375;  
  228.                     dir = 1;  
  229.                     break;  
  230.                 case VK_LEFT:             //按下【←】鍵  
  231.                     switch(dir)  
  232.                     {  
  233.                         case 0:  
  234.                             x -= 12;  
  235.                             break;  
  236.                         case 1:  
  237.                             x -= 13;  
  238.                             y += 4;  
  239.                             break;  
  240.                         case 2:  
  241.                             x -= 10;  
  242.                             break;  
  243.                         case 3:  
  244.                             x -= 10;  
  245.                             break;  
  246.                     }  
  247.                     if(x < 0)  
  248.                         x = 0;  
  249.                     dir = 2;  
  250.                     break;  
  251.                 case VK_RIGHT:             //按下【→】鍵  
  252.                     switch(dir)  
  253.                     {  
  254.                         case 0:  
  255.                             x += 8;  
  256.                             break;  
  257.                         case 1:  
  258.                             x += 7;  
  259.                             y += 4;  
  260.                             break;  
  261.                         case 2:  
  262.                             x += 10;  
  263.                             break;  
  264.                         case 3:  
  265.                             x += 10;  
  266.                             break;  
  267.                     }  
  268.                     if(x > 575)  
  269.                         x = 575;  
  270.                     dir = 3;  
  271.                     break;  
  272.             }  
  273.             break;  
  274.         case WM_DESTROY:                    //窗口結束消息  
  275.             int i;  
  276.   
  277.             DeleteDC(mdc);  
  278.             DeleteDC(bufdc);  
  279.             for(i=0;i<4;i++)  
  280.                 DeleteObject(girl[i]);  
  281.             DeleteObject(bg);  
  282.             ReleaseDC(hWnd,hdc);  
  283.   
  284.             PostQuitMessage(0);  
  285.             break;  
  286.         default:                            //其他消息  
  287.             return DefWindowProc(hWnd, message, wParam, lParam);  
  288.    }  
  289.    return 0;  
  290. }  

程序運行結果如下圖,我們可以用鍵盤操作這個小人的上下左右移動,用Esc退出:








這樣,一個簡單的小遊戲就完成了。

我們也可以通過在消息處理函數中取得按鍵虛擬鍵碼的方式,很簡單地對鍵盤輸入操作進行處理。





筆記十二到這裏就結束了。


本節源代碼請點擊這裏下載:  【Visual C++】Code_Note_12


感謝一直支持【Visual C++】遊戲開發筆記系列專欄的朋友們,也請大家繼續關注我的博客,我一有空就會把自己的學習心得,覺得比較好的知識點寫出來和大家一起分享。

精通遊戲開發的路還很長很長,非常希望能和大家一起交流,共同學習和進步。

大家看過後覺得有啓發的話可以頂一下這篇文章,讓更多的朋友有機會看到它。也希望大家可以多留言來和我探討編程相關的問題。最後,謝謝大家一直的支持~~~


The end


發佈了14 篇原創文章 · 獲贊 3 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章