人体学接口设备(HID)

游戏是有互动性的计算机模拟,为游戏而设的人体学接口设备(Human Interface Device,HID)种类繁多,包括摇杆、手柄、键盘、鼠标、Wii遥控器,以及方向盘、跳舞毯、电子吉他等等专用输入设备。本文探讨游戏引擎如何自入体学接口设备读取输入,处理输入,以及向玩家反馈输出。

1. HID的接口技术

  • 轮询
    • 像手柄这样的简单设备,可通过定期轮询硬件来读取输入(通常在主游戏循环里每次迭代轮询一次)
    • 要明确查询(输出)设备的状态,可直接读取硬件寄存器;读取经内存映射的I/O端口,或通过较高级的软件接口间接读取
    • 微软为Xbox 360手柄而设的XInput API是简单轮询的好例子。游戏在每帧调用XInputGetState()函数,它与硬件/驱动通信,适当地读取数据,并把所有结果包装成XINPUT_STATE结构,此结构含有手柄设备上所有输入的当前状态
  • 中断:像鼠标这样的设备没必要在静止时还不断发送数据。这类设备通常以硬件中断方式来通信。中断服务程序用来读取设备状态,把状态储存以供后续处理,然后交还CPU给主程序
  • 无线设备:对于蓝牙设备,软件必须以蓝牙协议和它们通信。这种通信一般由引擎主线程以外的线程负责处理,或至少被封装为简单接口供主循环调用。从程序员的角度来说,蓝牙基本和其他传统轮询设备的状态一样

2. HID的输入输出类型

2.1 输入类型

2.1.1 数字式按钮

数字式按钮只有两个状态——按下(down或press)或释放(up或release),软件中则以1或0表示。有时设备上所有按钮的状态会结合为一个无符号整数值,如XBox 360手柄的状态是以XINPUT_GAMEPAD结构传回。这个结构体第一个字段是一个16位无符号整数变量wButtons,存放所有按钮的状态。每个按钮都会定义一个掩码,实际使用时将wButtons和掩码做&运算来判断是否被按下。

2.1.2 模拟式轴和相对性轴

模拟式输入是指可获取一个范围以内的数值。此类输入通常用来代表摇杆的二维位置(使用两个模拟输入, 一个x轴一个y轴)。模拟式输入信号通常都要被数字化,表示为软件中的整数,再送入引擎处理。像Xbox 360手柄拇指摇杆的偏转量(sThumbLX/sThumbLY/sThumbRX/sThumbRY)取值范围是-32768~32767,而左右扳机(bLeftTrigger/bRightTrigger)取值范围是0(没扣压)~255(完全扣压)。

上述模拟式轴的位置都是绝对的,而有些输入是相对性的。这类设备不能界定在哪个位置的输入值为0,相反,输入为0代表设备的位置没变动,非零值代表自上次读取输入至今的增量,如鼠标、鼠标滚轮和轨迹球等等。

2.1.3 加速计及三维定向

Wii遥控器、PS3的Sixaxis及智能手机都包含加速传感器,能感应xyz三个主轴的加速度。Wii的一些游戏会利用3个加速计去估算控制器在玩家手上的定向。其原理是基于我们在地球表面上玩这些游戏,而地球的1g引力能对物体产生固定的向下加速度。

若把控制器完美地水平放置,并指向电视方向,那么垂直方向(z)的加速计应量度到大约-1g。若垂直握着控制器,使其指向上方,则可以预期z应为0,而y应为1g( 因为y传感器会感受到完整的引力效果)。当我们校准加速计得知每个轴的零点,就可以使用逆正弦和逆余弦,轻松求得偏航角、俯仰角和滚动角。

2.2 输出类型

  • 震动反馈:模拟游戏角色在游戏中受到扰动或撞击等感觉。震动通常由若干个马达驱动,每个马达带有稍不平衡的负重,以不同速度旋转。游戏可开关这些马达,并通过调节其旋转速度来向玩家双手产生不同的触觉效果
  • 力反馈:通过由马达驱动的制动器,以其产生的力对抗玩家施于HID上的力。常见于街机赛车游戏——当玩家尝试转方向盘时,方向盘会产生阻力,其输出原理同震动反馈
  • 其他输入/输出:有些HID设备含扬声器、麦克风等音频接口;较老的像Dreamcast的手柄支持插入记忆卡;Xbox 360手柄和Wii遥控器带有4个软件控制的LED灯;乐器、跳舞毯等特殊设备有其专门的输入/输出类型;近年发展的姿势界面(如Kinect)和VR设备,也是非常独特的HID

3. 游戏引擎的HID系统

多数游戏引擎不会直接使用HID的原始输入数据,而是引入至少一个在HID和游戏之间的间接层,将输入以多种形式抽象化。下面会介绍一些HID系统的典型需求。

3.1 死区

假定模拟轴的输入值范围是$I_{min}$到$I_m$,未触碰模拟轴时,稳定及清晰的“未扰动”输入值为$I_0$。

HID本质上是模拟设备,其产生的电压含有噪声,以致实际上量度到的输入会轻微$I_0$附近浮动。解决办法是引入一个围绕$I_0$的死区。对于摇杆,死区可以定义为$[I_0-\sigma, I_0+\sigma]$;对于扳机,则定义为$[I_0, I_0+\sigma]$。任何位于死区的输入值都可以简单地被钳制为$I_0$。死区必须足够大以容纳未扰动控制的最大噪声,同时也必须足够小以免影响玩家对HID的反应手感。

3.2 模拟信号过滤

即使控制器不在死区范围,其输入仍会有信号噪声问题,这些噪声有时候会导致游戏中的行为显得抖动或不自然。由于噪声信号的频率通常比玩家产生的要高。所以,解决办法之一是,先利用低通滤波器过滤原始输入,再把结果传送至游戏中使用。

结合未过滤输入的时变函数$u(t)$,己过滤输入为$f(t)=(1-a)f(t-\Delta t)+au(t)$,其中参数$a$由帧持续时间$\Delta t$和过滤常数$RC$所确定,即$a=\frac{\Delta t}{RC+\Delta t}$。公式转换C++代码如下:

F32 lowPassFilter(F32 unfilteredInput, F32 lastFramesFilteredInput, F32 rc, F32 dt) {
    F32 a = dt / (rc + dt);
    return (1 - a) * lastFramesFilteredInput + a * unfilteredInput;
}

另一个过滤HID输入的方法是计算移动平均。例如,若要计算3帧时间范围内的输入数据平均,只需把原始输入数据简单地储存于3个元素大小的循环缓冲区里,把此数组的值求和除3,就是过滤后的输入值。因为初始时该数组并未填满有效数据,要注意处理前两帧的输入。

3.3 输入事件检测

3.3.1 按下和释放按钮

假设按钮的输入位在释放时为0,按下时为1。可以记录上一帧的状态(32位位整型),和本帧的状态位异或,为1的位表示状态发生变化。再审视每个按钮的当前状态,若某按钮的状态有改变,而当前的状态是按下,则产生按下事件,否则产生释放事件。

3.3.2 弦(chord)

弦是指一组按钮,当同时被按下时,会在游戏中产生一个独特行为。一般通过检测两个或以上的按钮状态,当该组按钮全部同时被按下才执行操作。但弦有许多细节值得注意:

  • 小心避免同时产生个别按钮的动作和弦的动作。要在检测个别按钮的时候,同时检查弦里的其他按键并没有被按下
  • 弦的检测代码必须健壮,防止人们按下弦中的某一按钮稍早于其他按钮。有几种方法可以处理这些情况
    • 从策划的角度,将按钮输入设计为,弦总是作用于某个按钮的动作再加上额外的动作。例如,若按L1是主武器开火,按L2投射手榴弹,可能L1+L2的弦是令主要武器开火、投射手榴弹,并发送能量波使这些武器的伤害力加倍。这样从玩家的角度来说游戏表现出的行为没有不同
    • 在个别按钮按下后,加入一段延迟时间,然后才算作是一个有效的游戏事件。在延迟期间(如2或3帧),若检测到一个弦,那么那个弦就会凌驾个别按钮产生事件
    • 按下单个按钮时立即执行动作,但容许这些动作被之后弦的动作抢占
    • 按下按钮时检测弦,但之后释放按钮时才产生效果

3.3.3 序列检测

序列指玩家通过HID,在一段时间内完成一串动作,最常用于格斗游戏,如在0.5-1秒内连续按下“左右左ABA”。序列检测的基本原理是:保留HID输入的动作短期记录,当检测到序列第一个成分,就会把该成分及其时间戳记录在历史缓冲区中。之后,检测到每个后续成分时,需要检查距上一个成分所经过的时间,若时间仍在容许范围内,就把该成分加入缓冲区中。若整个序列于限定时间内完成,就会产生对应的事件。若在过程中检测到无效输入,或超过规定事件,那么整个历史缓冲区会被重置。

要检测连打按钮频率,只须记录该按钮上一次被按下事件的时间Tlast和两次按下按钮的时间间隔。若该间隔超过了给定的阈值,则不更新Tlast。那么,在有一对新的够迅速的按钮按下事件产生之前,序列会一直判定为无效。

要检测诸如“在1s内连续按下ABA的序列”,可以参照下面的伪代码。其中ButtonsJustWentDown()函数来检查若干个按钮的按下事件。当成功检测到序列,就会广播指定事件,令整个游戏都能接收到。

class ButtonSequenceDetector {
    U32 m_aButtonIds; // 检测的序列
    U32 m_buttonCount; // 序列中的按钮数目
    F32 m_dtMax; // 整个序列的最大时限
    EventId m_eventId ; // 完成序列的事件
    U32 m_iButton; // 要检测的下一个按钮
    F32 m_tStart; // 序列的开始时间,以秒为单位

public:
    void Update() {
        ASSERT(m_iButton < m_buttonCount);
        // 计算下个预期的按钮,以位掩码表示(把1左移至正确的位索引)
        U32 buttonMask = (1U << m_aButtonid[m_iButton]);
        // 若玩家按下预期以外的按钮,废止现时的序列(使用位取反运算检测所有其他按钮)
        if (ButtonsJustWentDown(~buttonMask)) {
            m_iButton = 0; // 重置
        }
        // 否则,若预期按钮刚被按下,检查dt及适当更新状态
        else if (ButtonsJustWentDown(buttonMask)) {
            // 序列中第一个按钮
            if (m_iButton == 0) {
                m_tStart = CurrentTime();
                ++m_iButton;
            } else {
                F32 dt = CurrentTime() - m_tStart;
                // 时间间隔符合要求,序列仍然有效
                if (dt < m_dtMax) {
                    ++m_iButton;
                    // 判断序列是否完成
                    if (m_iButton == m_buttonCount) {
                        // 广播事件并重置
                        BroadcastEvent(m_eventId);
                        m_iButton = 0;
                    }
                }
                // 按得不够快,重置
                else {
                    m_iButton = 0;
                }
            }
        }
    }
}

再复杂的序列包含了摇杆方向,例如检测左拇指摇杆沿顺时针方向旋转一周。可以把遥杆位置的二维范围分割成4个象限。顺时针方向旋转时,经过的象限顺序是左上,右上,右下,左下。只要把象限检测当作按钮处理,就可稍修改上文按钮序列检测代码来完成任务。

3.4 跨平台HID系统

引擎处理多平台的HID数据时,应该提供某形式的硬件抽象层,使游戏代码和硬件相关细节隔离。此抽象层能把目标硬件的原始控制标识符转化为抽象的控制索引。例如Xbox 360及PS3的两款手柄的控制布局几乎相同,所以可以设立一套抽象标识符来屏蔽它们的差异。

3.5 输入的重映射

许多游戏提供给玩家修改键位的选项,这就需要把原始输入映射到最终的游戏功能上。可以给每个游戏功能一个唯一标识符,然后加一个简单的表,把每个抽象的控制索引映射至游戏中的逻辑功能。要改变映射,可以更换整个表,或是让玩家设置该表中的个别条目。

但要小心不同的输入种类和取值范围,像某个游戏逻辑需要轴,就不能改用按钮操控。为了允许合理的输入映射,可以把所有输入分类并归一化:

  • 数字式按钮:按钮状态打包成32位字,每一位代表一个按钮的状态
  • 单向绝对轴(如扳机、模拟式按钮):产生[O, 1]的浮点数
  • 双向绝对轴(如摇杆):产生[-1, 1]的浮点数
  • 相对轴(如鼠标滚轮、轨迹球):产生[-1, 1]的浮点数,其中±1代表单帧内最大的相对偏移值

3.6 上下文相关控制

许多游戏里一个物理控制会根据上下文有着不同功能,例如若角色站在门前,按“使用”按钮会开门,若角色附近有一个物体,按“使用”按钮会拾起该物体。上下文相关控制可简单地采用状态机来实现,即根据当前状态个别HID控制可能有不同用途。要注意有时还需要实现优先系统,为不同物体赋予权值,来决定同等条件下优先让哪个物体(状态)生效。

3.7 禁用输入

在某些场合可能需要禁用玩家的输入,例如过场动画禁用所有输入,玩家经过窄巷暂停自由旋转摄像机。一个较差的方法是使用位掩码来禁用设备上的某些控制,这种方法缺陷是如果忘记重置掩码,很可能使玩家持续失去控制。所以应该小心处理游戏逻辑,并加入一些防故障机制。

另一个更好的做法是,把禁用某玩家动作及行为的逻辑写进玩家或摄像机的代码里。这样,若摄像机某时刻决定要忽略右拇指轴的输入,游戏引擎内其他系统仍然能自由读取该输入做其他用途。

参考文献:电子工业出版社《游戏引擎架构》第8章

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器