zoukankan      html  css  js  c++  java
  • 《游戏引擎架构》笔记八

    人体学接口设备(HID)

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

    HID的接口技术

    轮询

    • 像手柄这样的简单设备,可通过定期轮询硬件来读取输入(通常在主游戏循环里每次迭代轮询一次)
    • 要明确查询(输出)设备的状态,可直接读取硬件寄存器;读取经内存映射的I/O端口,或通过较高级的软件接口间接读取
    • 微软为Xbox 360手柄而设的XInput API是简单轮询的好例子。游戏在每帧调用XInputGetState()函数,它与硬件/驱动通信,适当地读取数据,并把所有结果包装成XINPUT_STATE结构,此结构含有手柄设备上所有输入的当前状态

    中断:像鼠标这样的设备没必要在静止时还不断发送数据。这类设备通常以硬件中断方式来通信。中断服务程序用来读取设备状态,把状态储存以供后续处理,然后交还CPU给主程序

    无线设备:对于蓝牙设备,软件必须以蓝牙协议和它们通信。这种通信一般由引擎主线程以外的线程负责处理,或至少被封装为简单接口供主循环调用。从程序员的角度来说,蓝牙基本和其他传统轮询设备的状态一样

    HID的输入输出类型

    输入类型

    数字式按钮

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

    模拟式轴和相对性轴

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

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

    加速计及三维定向

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

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

    输出类型

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

    游戏引擎的HID系统

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

    死区

    假定模拟轴的输入值范围是[Imin,Imax],未触碰模拟轴时,稳定及清晰的“未扰动”输入值为I0

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

    模拟信号过滤

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

    离散低通滤波器的实现方法之一:结合目前未过滤输入值和上一帧的已过滤输入。设未过滤输入为时变函数u(t),己过滤输入为f(t)t为时间,则

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

    其中参数a由帧持续时间Δt和过滤常数RC所确定,即a=Δt/(RC+Δ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,就是过滤后的输入值。因为初始时该数组并未填满有效数据,要注意处理前两帧的输入。

    template<typename TYPE,int SIZE>
    class MovingAverage{
        TYPE m_sample[SIZE];
        TYPE m_sum;
        U32 m_curSample;
        U32 m_sampleCount;
    public:
        MovingAverage() :m_sum(static_cast<TYPE>(0)), m_curSample(0), m_sampleCount(0){}
    
        void addSample(TYPE data){
            if (m_sampleCount == SIZE){
                m_sum -= m_sample[m_curSample];
            }
            else{
                ++m_sampleCount;
            }
            m_sample[m_curSample] = data;
            m_sum += data;
            ++m_curSample;
    
            if (m_curSample >= SIZE){
                m_curSample = 0;
            }
        }
    
        F32 getCurrentAverage()const{
            if (m_sampleCount != 0){
                return static_cast<F32>(m_sum) / static_cast<F32>(m_sampleCount);
            }
            return 0.0f;
        }
    };

    输入事件检测

    按下和释放按钮

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

    (chord)

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

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

    序列检测

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

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

    class ButtonTapDetector{
        U32 m_buttonMask;    //需检测的按钮(位掩码)
        F32 m_dtMax;        //按下事件之前的最长容许时限
        F32 m_tLast;        //最后按下按钮的时间,以秒为单位
    public:
        //构建对象,用于检测快速连打指定的按钮(索引标识)
        ButtonTapDetector(U32 buttonId, F32 dtMax) :m_buttonMask(1U << buttonId),m_dtMax(dtMax),m_tLast(CurrentTime() - dtMax){}
    
        //调用该函数查询玩家是否做出该手势
        void isGestureValid()const {
            F32 t = CurrentTime();
            F32 dt = t - m_tLast;
            return (dt < m_dtMax);
        }
    
        //每帧调用该函数
        void update(){
            //ButtonJustWentDown()函数用来侦测本帧刚被按下的按钮,若按位掩码指定的按钮中有任意一个按钮刚才按下,此函数返回非零值
            if (ButtonJustWentDown(m_buttonMask)){
                m_tLast = CurrentTime();
            }
        }
    };

    如果要检测多按钮序列,首先使用一个变量记录要预期要按下的按钮序列,例如用数组记录序列;然后当接收到一个合乎序列目前预期的按钮事件时,就把事件时间戳和Tstart比较,若在有效时间窗内,则移动到序列的下一个按键处,并更新Tstart;若不符合序列或或时间超过,则序列索引重置,并且Tstart设为无效值。

    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个象限。顺时针方向旋转时,经过的象限顺序是左上,右上,右下,左下。只要把象限检测当作按钮处理,就可稍修改上文按钮序列检测代码来完成任务。

    跨平台HID系统

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

    输入的重映射

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

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

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

    上下文相关控制

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

    禁用输入

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

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

  • 相关阅读:
    c# 判断网络是否连接
    有关TSQL的10个好习惯
    相同文件只能一个进程读取
    我的单元测试方案
    又用了一把VBA
    深入理解字符串和字节数组转换
    如何清除应用程序承载 WebBrowser 控件时缓存
    VB也绿色
    ASP.Net网站开发的单元测试方案
    Nunit使用心得
  • 原文地址:https://www.cnblogs.com/yeqluofwupheng/p/7711361.html
Copyright © 2011-2022 走看看