C#对游戏手柄的编程开发-API篇(2)
在C#对游戏手柄的编程开发-API篇(1)这篇文章中我们介绍了“被动方式”的开发。在此方式下,我们的程序只扮演一个消息接收者。系统会定时告诉我们某个游戏手柄当前的状态,我们的程序接收到后再按实际需要进行处理即可。但如果你是一个细心的人,你会发现如果直接按消息事件处理的话会存在一个问题,如我们按下某个键(比如向上的方向键)然后放开时,对于我们“人”来说,我们按下与弹起的这两个动作应该只是说明我们只点击这个按钮一次。但对于系统来说,它只是机械地定时通知我们的程序在某个时间内游戏手柄的各个按钮的状态,而在我们按下到弹起这段时间内,系统有可能已经传递了N次的消息通知(N值根据捕捉时设置的uPeriod值与你的按键速度来决定),通知手柄有按钮处于被按下状态,而如果我们就根据消息包直接处理点击事件的话,就会导致问题出现(比如在某个游戏中,我们设计的是当点击一次手柄的右键,就将角色向前移动一步。但从我们按下按钮到弹开此按钮这段时间,由于人的反应速度远远慢于电脑的处理速度,所以这段很短的时间内,系统可能已通知了10次以上的消息包表明游戏手柄右键已被按下,这就导致我们按一次右键,游戏中的角色却有可能已移
回顾“被动方式”开发
在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键的按键通知。这样篇头中的问题就可以完美的解决了 !!
(范例代码可参考源码中OnTimerCallback函数)
到此,“C#对游戏手柄的编程开发”的文章就讲解完了,下一篇我们会讲解一下怎么去实现第一篇中说的“用游戏手柄模拟键盘或鼠标”的软件 。很简单的说,有兴趣的朋友希望能回贴支持一下我