C#對游戲手柄的編程開發-API篇(2)
回顧“被動方式”開發
在C#對游戲手柄的編程開發-API篇(1)這篇文章中我們介紹了“被動方式”的開發。在此方式下,我們的程序只扮演一個消息接收者。系統會定時告訴我們某個游戲手柄當前的狀態,我們的程序接收到后再按實際需要進行處理即可。但如果你是一個細心的人,你會發現如果直接按消息事件處理的話會存在一個問題,如我們按下某個鍵(比如向上的方向鍵)然后放開時,對于我們“人”來說,我們按下與彈起的這兩個動作應該只是說明我們只點擊這個按鈕一次。但對于系統來說,它只是機械地定時通知我們的程序在某個時間內游戲手柄的各個按鈕的狀態,而在我們按下到彈起這段時間內,系統有可能已經傳遞了N次的消息通知(N值根據捕捉時設置的uPeriod值與你的按鍵速度來決定),通知手柄有按鈕處于被按下狀態,而如果我們就根據消息包直接處理點擊事件的話,就會導致問題出現(比如在某個游戲中,我們設計的是當點擊一次手柄的右鍵,就將角色向前移動一步。但從我們按下按鈕到彈開此按鈕這段時間,由于人的反應速度遠遠慢于電腦的處理速度,所以這段很短的時間內,系統可能已通知了10次以上的消息包表明游戲手柄右鍵已被按下,這就導致我們按一次右鍵,游戲中的角色卻有可能已移動了十步之多,這可不是我們想要的結果)。那我們要怎樣處理這個“點擊”事件才可以避免重復通知呢?這就是本篇最后要重點講解的內容了……
在講解這個問題的解決方法之前我們再來講解一下上文還提到的一種開發方式。
“主動方式”的開發
主動方式即我們不需要向系統申請注冊捕捉某個游戲手柄,我們只是根據自己的需要按時去獲取游戲手柄的狀態信息。
這時我們就要用到以下的API函數。
/// /// 獲取操縱桿位置和按鈕狀態 /// /// /// /// [DllImport("winmm.dll")] public static extern int joyGetPos(int uJoyID, ref JOYINFO pji); /// /// 獲取操縱桿位置和按鈕狀態 /// /// /// /// [DllImport("winmm.dll")] public static extern int joyGetPosEx(int uJoyID, ref JOYINFOEX pji); |
上面的兩個API函數,我們可以從中任選一個,但joyGetPos函數只能取得1,2,3,4號四個按鈕的狀態。所以建議不用,下面只重講解joyGetPosEx函數!
JOYINFO 與 JOYINFOEX 是屬于結構體,它們的定義如下:
Code #region 游戲手柄的位置與按鈕狀態 /// <summary> /// 游戲手柄的位置與按鈕狀態 /// </summary> [StructLayout(LayoutKind.Sequential)] public struct JOYINFO { public int wXpos; public int wYpos; public int wZpos; public int wButtons; } /// <summary> /// 游戲手柄的位置與按鈕狀態 /// </summary> [StructLayout(LayoutKind.Sequential)] public struct JOYINFOEX { /// <summary> /// Size, in bytes, of this structure. /// </summary> public int dwSize; /// <summary> /// Flags indicating the valid information returned in this structure. Members that do not contain valid information are set to zero. /// </summary> public int dwFlags; /// <summary> /// Current X-coordinate. /// </summary> public int dwXpos; /// <summary> /// Current Y-coordinate. /// </summary> public int dwYpos; /// <summary> /// Current Z-coordinate. /// </summary> public int dwZpos; /// <summary> /// Current position of the rudder or fourth joystick axis. /// </summary> public int dwRpos; /// <summary> /// Current fifth axis position. /// </summary> public int dwUpos; /// <summary> /// Current sixth axis position. /// </summary> public int dwVpos; /// <summary> /// Current state of the 32 joystick buttons. The value of this member can be set to any combination of JOY_BUTTONn flags, where n is a value in the range of 1 through 32 corresponding to the button that is pressed. /// </summary> public int dwButtons; /// <summary> /// Current button number that is pressed. /// </summary> public int dwButtonNumber; /// <summary> /// Current position of the point-of-view control. Values for this member are in the range 0 through 35,900. These values represent the angle, in degrees, of each view multiplied by 100. /// </summary> public int dwPOV; /// <summary> /// Reserved; do not use. /// </summary> public int dwReserved1; /// <summary> /// Reserved; do not use. /// </summary> public int dwReserved2; } #endregion
如我們使用joyGetPosEx獲取游戲設備的狀態時,必須先初始化JOYINFOEX結構實例,并要設置dwSize參數的值,也即是JOYINFOEX結構體所占用的內存空間大小(其值可通過Marshal.SizeOf求得)。而如果要取得游戲設備的其它參數,則還必須要設置dwFlags參數的值!否則只能獲取坐標值(dwXPos)。如對游戲手柄來說我們需要獲取其它按鈕的狀態,則設置dwFlags的值為JOY_RETURNBUTTONS,用于指示我們需要返回所有按鈕的狀態。
示例代碼:
JoystickAPI.JOYINFOEX infoEx = new JoystickAPI.JOYINFOEX(); infoEx.dwSize = Marshal.SizeOf(typeof(JoystickAPI.JOYINFOEX)); infoEx.dwFlags = (int)JoystickAPI.JOY_RETURNBUTTONS; int result = JoystickAPI.joyGetPosEx(this.Id, ref infoEx); |
如果joyGetPosEx函數獲取手柄狀態數據成功,則返回JOYERR_NOERROR(值為0),否則返回其它值的話表示獲取失敗。
當數據獲取成功后,對應的游戲手柄的狀態數據都已存儲在JOYINFOEX結構實例中了。如要判斷是否按下了方向鍵,則可判斷dwXPos與dwYPos的值;而判斷是否按了其它按鈕,則可判斷dwButtons的值。判斷方法在上一章中有講,這里就不再細說,或者也可以看后面提供的源碼。
因為“主動方式”的“時效性”只有一次,所以為了能夠隨時監視到游戲手柄的按鍵事件,就必須進行“輪循”獲取,當監視到游戲手柄有按鍵發生時就進行事件通知(噫?好像“被動方式”?嗯,其實當我們向系統申請捕捉某個游戲手柄時,系統最后也是在幫我們進行“輪循”操作!)。而實現“輪循”的方式則可以有多種方式,比如采用獨立的線程進行一個死循環;或者采用Timer進行定時執行。
但當我們的操作進入“輪循”后,如果也是直接joyGetPostEx就處理的話也一樣會碰到篇頭所說的那個糟糕問題 !因為不管是“主動方式”還是“被動方式”都是一樣只能得到游戲手柄按鈕當前的狀態(按下或未按下)。那怎么解決呢?
解決按鈕重復狀態的問題
解決這個問題,如果理清了思路,其實也是很簡單的方法。
我們通過API得到的是游戲手柄按鈕當前的狀態(被按下或未按下)。因此我們可以在“輪循”里,每當監視到游戲手柄在某次時間有某些按鈕是處于“按下”狀態時,就記錄此次被按下的按鈕號,這樣當下一次“輪循”操作時,如果也監視到有按鈕按下,則通過與上一次按下的按鈕對比,如果還是相同的按鈕,則表明本次按鈕還是繼續上次的按下狀態,那就不再需要向程序里發出消息通知了。而如果不相同,則發出新的按鈕按鍵通知,并記錄本次按下的按鈕號。
偽代碼如下:
previousButtons = 無; //死循環,進入輪循 while(true){ if(joyGetPosEx(手柄號,ref joyInfo) == 成功){ JoyButtons buttons = 取得當前按下的按鈕(joyInfo); if(buttons != 無){ if(buttons != previousButtons){ //本次按下的按鈕不同于上次按下的按鈕.所以進行通知 OnClick(buttons); //記錄本次按下的按鈕 previousButtons = buttons; } } } 暫停uPeriod毫秒; } |
經過這樣的處理后,每按一次手柄的按鈕我們的程序也只收到一次按鍵通知,看來我們的目的似乎達到了 。但在平常玩游戲中,我們同時按下的鍵不單單只有一個,比如邊走邊砍殺敵人,就有可能按住右方向鍵不放,然后拼命的按A或B鍵,那這樣的話又會出現怎樣的情況呢?這樣的話,在我們的“輪循”中就有可能出現以下的情況(“->”表示先后順序):
取得當前按下的是“右方向鍵”(1) –> 取得當前按下的是“右方向鍵”(2) –> 取得當前按下的是“右方向鍵”與A鍵(3) –> 取得當前按下的是“右方向鍵”(4) –> 取得當前按下的是“右方向鍵”與B鍵(5)–> 取得當前按下的是“右方向鍵”(6) ……
在上面中,(1)與(2)可通過上面的解決辦法合并為一次,但到第3步時,因為當前按下的鍵有兩個,而前一次按下的按鈕只有一個,所以因(2)按鍵的不同,又重新發出一次按鍵通知。如此類推,從(1)到(6)步,程序就認為“右方向鍵”共按了5次!但對于我們“人”來說,這不是我們想要的結果,因為我們只是一直按住“右方向鍵”不放,所以應該只算按一次。那看來上面的解決方法并不完美 。
讓我們再仔細再看一下上面的那個流程中的(2)與(3)中的差別,明眼的你應該看出來了,它們之間只是多了一個A鍵。而如果“右方向鍵”在第一步時已發出了按鍵消息通知,那么在(3)步時,如果我們只發出“A鍵”的按鍵消息通知,也就說每次只發出本次按下的按鍵集合與上一次按下的按鍵集合的差的按鍵消息通知的話,那么在上面的流程中,發出的消息通知就只有:在(1)步時發出“右方向鍵”的按鍵通知、(3)步時發出A鍵的按鍵通知、(5)步時發出B鍵的按鍵通知。這樣篇頭中的問題就可以完美的解決了 !!
(范例代碼可參考源碼中OnTimerCallback函數)
到此,“C#對游戲手柄的編程開發”的文章就講解完了,下一篇我們會講解一下怎么去實現第一篇中說的“用游戲手柄模擬鍵盤或鼠標”的軟件 。很簡單的說,有興趣的朋友希望能回貼支持一下我