回顧“被動方式”開發
在C#對游戲手柄的編程開發-API篇(1)這篇文章中我們介紹了“被動方式”的開發。在此方式下,我們 的程序只扮演一個消息接收者。系統會定時告訴我們某個游戲手柄當前的狀態,我們的程序接收到後再按 實際需要進行處理即可。但如果你是一個細心的人,你會發現如果直接按消息事件處理的話會存在一個問 題,如我們按下某個鍵(比如向上的方向鍵)然後放開時,對於我們“人”來說,我們按下與彈起的這兩 個動作應該只是說明我們只點擊這個按鈕一次。但對於系統來說,它只是機械地定時通知我們的程序在某 個時間內游戲手柄的各個按鈕的狀態,而在我們按下到彈起這段時間內,系統有可能已經傳遞了N次的消 息通知(N值根據捕捉時設置的uPeriod值與你的按鍵速度來決定),通知手柄有按鈕處於被按下狀態,而 如果我們就根據消息包直接處理點擊事件的話,就會導致問題出現(比如在某個游戲中,我們設計的是當 點擊一次手柄的右鍵,就將角色向前移動一步。但從我們按下按鈕到彈開此按鈕這段時間,由於人的反應 速度遠遠慢於電腦的處理速度,所以這段很短的時間內,系統可能已通知了10次以上的消息包表明游戲手 柄右鍵已被按下,這就導致我們按一次右鍵,游戲中的角色卻有可能已移動了十步之多,這可不是我們想 要的結果)。那我們要怎樣處理這個“點擊”事件才可以避免重復通知呢?這就是本篇最後要重點講解的 內容了……
在講解這個問題的解決方法之前我們再來講解一下上文還提到的一種開發方式。
“主動方式”的開發
主動方式即我們不需要向系統申請注冊捕捉某個游戲手柄,我們只是根據自己的需要按時去獲取游戲 手柄的狀態信息。
這時我們就要用到以下的API函數。
/// <summary>
/// 獲取操縱桿位置和按鈕狀態
/// </summary>
/// <param name="uJoyID"></param>
/// <param name="pji"></param>
/// <returns></returns>
[DllImport("winmm.dll")]
public static extern int joyGetPos(int uJoyID, ref JOYINFO pji);
/// <summary>
/// 獲取操縱桿位置和按鈕狀態
/// </summary>
/// <param name="uJoyID"></param>
/// <param name="pji"></param>
/// <returns></returns>
[DllImport("winmm.dll")]
public static extern int joyGetPosEx(int uJoyID, ref JOYINFOEX pji);
上面的兩個API函數,我們可以從中任選一個,但joyGetPos函數只能取得1,2,3,4號四個按鈕的狀態。 所以建議不用,下面只重講解joyGetPosEx函數!
JOYINFO 與 JOYINFOEX 是屬於結構體,它們的定義如下:
#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鍵的按鍵通知。這樣篇頭中的問題就可以完美的解決了 ! !
到此,“C#對游戲手柄的編程開發”的文章就講解完了,下一篇我們會講解一下怎麼去實現第一篇中 說的“用游戲手柄模擬鍵盤或鼠標”的軟件 。很簡單的說,有興趣的朋友希望能回貼支持一下我
本文配套源碼