zhouqijie

一、各种人体学接口设备(HID)

人体学输入设备(human interface device, HID)种类繁多,包括摇杆、手柄、键盘、鼠标、方向盘等。

  1. 轮询。
  2. 事件驱动。
  3. 中断驱动。





二、人体学接口设备接口技术

1.轮询(Poll)

一些简单设备例如老式摇杆,可通过定期轮询(poll)硬件来读取输入。(通常在主循环里每次迭代轮询一次)

查询设备状态,方法一是直接读取硬件寄存器,二是读取经内存映射的I/O端口。或者通过较高级的软件接口(软件接口再用这两种方法)。

微软的XBOX360手柄XInputAPI是轮询的例子。游戏每帧调用XInputGetState()来和硬件/驱动通信,读取数据结果并包装为XINPUT_STATE结构。

2.中断(Interrupt)

有些HID只有当状态改变时才把数据传至游戏引擎。例如鼠标只有在移动/按下/松开时才需要传送数据,并不需要在静止时还不断传输数据。

这类设备通常和主机以硬件中断方式(hardware interrupt)通信。所谓中断就是硬件生成的信号,能让CPU暂停主程序,并执行一段称为中断服务程序(interrupt service routine, ISR)的代码。

中断一般用来读取设备状态,把状态储存然后交还CPU给主程序,以供后续游戏引擎在合适时提取。

3.蓝牙设备(Bluetooth)

蓝牙设备例如Wii遥控器和Xbox无线手柄,并不能简单通过寄存器或者内存映射IO去读写。软件必须以蓝牙协议(bluetooth protocol)来和设备交流。

这种通信一般会由主线程外的其他线程负责处理,或者至少封装为相对简单接口供主循环调用。





三、输入类型

数字式按钮

很多HID设备都有至少几个数字式按钮(digtal button)。这些按钮只有按着和释放两种状态。

软件中,数字式按钮的状态通常用一个位表示,0表示没按着1表示按着。

有时候设备上所有按钮会串成一个无符号整数值(例如XInputAPI)。要读取某个按钮状态,直接用掩码进行按位并(&)运算即可。

模拟式轴

模拟式输入(analog input)或称为轴(axis)可获取一个范围内的数值。

模拟输入到达引擎要被数字化(digitize),再被表示为整数。

相对性轴

有些设备的输入是相对的,例如鼠标移动和鼠标滚轮。零值代表位置没变动,非零值代表自上次读取输入至今的增量。

其他

其他的输入类型有加速度计、三维定向、摄像机等。





四、输出类型

震动设备

一些手柄提供震动反馈(rumble)功能,震动通常由一个或多个马达驱动。

力反馈

原理是通过由马达驱动的actuator,用其产生的力对抗玩家施于HID上的力,常见于赛车游戏的方向盘。

音频

有些HID能提供音频输出,供音频系统使用。





五、游戏引擎的HID系统

功能需求:死区(dead zone)

当玩家未触碰输入轴时,我们需要获得稳定清晰的未扰动(undisturbed)输入值。但是HID本质是模拟设备,其产生的电压含有噪声,以致于实际度量的输入会在0附近浮动。

此问题的常见解决办法是引入一个围绕零输入的死区,任何位于死区内的输入值都可以简单限制为零。

死区必须足够大来容纳扰动噪声,但也不能太大以免影响手感。

功能需求:模拟信号过滤

控制器不在死区内也会有噪声问题,在游戏中会造成抖动。许多游戏会过滤来自HID的原始信号。

噪声频率通常比玩家产生得要高。所以解决办法之一就是,先利用低通滤波器(low pass filter)过滤原始输入,然后再传入游戏使用。另一个过滤HID输入数据的方法是计算移动平均(moving average)。

低通滤波器实现:

由于噪声信号的频率通常比玩家产生的要高,所以解决办法之一是用低通滤波器(low pass filter)过滤原始信号。

$f(t) = (1 - a)f(t - Δt) + au(t)$

Creedon: 和current = lerp(current, rawInput, a)等同。

参数a满足$a = {{Δt} \over {RC - Δt}}$

RC是传统以阻容电路实现的模拟低通滤波器中电阻和电容的积。

代码示例:

float LowPassFilter(float rawInput, float lastFrameFilteredInput, float rcm float dt)
{
    float a = dt / (rc + dt);
    return ((1 - a) * lastFrameFilteredInput) + (a * rawInput);
}

计算移动平均实现:

另一个过滤HID输入的方法是计算移动平均(moving average)。例如如果要计算3帧时间内的输入数据平均,只需把原始输入数据简单地储存在3个元素的数组中,把此数组的值求和除以3就是过滤后的值。



功能需求:输入事件检测

按下和释放:

检测按钮状态改变的最简单方法就是,记录上一帧的状态,用以和这一帧的状态比较,如果两状态不同,便可得知按钮事件发生了。此过程可以用简单的位运算符检测按下和释放事件。

假设有一个32位字段(buttonStates),它最多包含32个按钮的当前状态。如果已知上一帧的(prevButtonStates)和这一帧的(buttonStates),就可以用位运算计算出本帧按下的按钮(buttonDowns)和本帧释放的按钮(buttonUps)两个32位字段。

代码示例:

#define U32 uint32_t
class ButtonState
{
    U32 buttonStates_current;
    U32 buttonStates_prev;

    U32 buttonDowns;
    U32 buttonUps;

    void DetectButtonUpDownEvents()
    {
        U32 buttonChanges = buttonsStates_current ^ buttonStates_prev;

        buttonDowns = buttonChanges & buttonStates_current;

        buttonUps = buttonChanges & (~buttonStates_current);
    }
}

异或(exclusive or, xor)对相同的输入产生结果0,对两个不同结果产生结果1。

弦:

弦(chord)是指同时按下多个按钮的行为。

弦的检测在理论上很简单-检查多个按钮状态是否全部同时被按下。但是问题是人们在一帧内同时按下多个按钮是比较难的,通常会先按下一个按钮。所以要代码要设计得足够健壮,以下提供几种办法。

  1. 按下一个按钮后,延迟触发事件,在延迟期间(2帧或3帧)如果检测到弦,则弦会凌驾这个按钮按下事件。
  2. 在释放按钮时才产生效果,在此之前检测弦。
  3. 允许按下单个按钮后立即执行动作,但是这个动作可以被弦所抢占。

序列和手势检测:

手势是指玩家通过HID,在一段时间内完成一串动作。

一般检测手势的方法是,保留玩家输入的动作短期记录。

Creedon:即从按下第一个按钮开始把符合条件的后续按钮事件暂存在一个历史缓冲区里面,按错或者超时就重置缓冲区。

功能需求:多玩家和多HID管理:

多数游戏机允许接上两个或者更多HID,引擎需要追踪目前连接了哪些设备,并把每个设备的输入发送给游戏中适当的玩家。所以需要我们以某种方式映射控制器至玩家。

代码必须足够健壮,能够处理多种情形例如控制器掉线等。

功能需求:跨平台:

一个可行的办法是在HID相关的代码段都加上条件编译指令。

更好的方法是用抽象层隔离游戏代码和硬件细节。(需要抽象出不同平台HID的共同特征)

功能需求:输入的重新映射:

游戏通常需要允许玩家自定义控制设置。有些游戏允许玩家在多套预设之间选择,而有些游戏则允许玩家以完全控制权定义每个输入的功能。

实现方法是增加一个中间层。例如使用一个表,把控制索引映射至游戏中的逻辑功能。

功能需求:上下文相关控制

CRE:“上下文相关控制”是指不同情况下的按钮有不同的功能。比如一个按键,玩家在门前对应开门动作,而在物品旁边时对应拾取动作。

上下文相关(context-sensitive)的控制可简单地采用状态机来实现。(有时需要判断”优先级”或者”权值”)

功能需求:禁止输入

在多数游戏中,有时需要禁止玩家控制其角色。

直接禁用HID的某些输入,对游戏来说可能是过大的限制。另一个可能更好的做法是,把禁用某些玩家动作及行为的逻辑写进玩家或者摄像机代码中。

(END)