Hazel 引擎学习笔记 (十九):Input 轮询系统详解
游戏引擎 24

1. 为什么要有 Input 轮询

在游戏引擎中,输入处理是核心功能之一。玩家的操作(键盘、鼠标)直接驱动游戏逻辑和交互。Cherno 在 Hazel 中把输入系统拆分成两部分:

  1. 事件系统(Event)

    • 通过事件回调处理一次性动作,例如按键按下、释放。

    • 优点:可以精确捕捉瞬间事件。

    • 缺点:如果你想实时获取某个按键的持续状态,事件系统不方便。

  2. 轮询系统(Polling / Input)

    • 每一帧主动查询键盘或鼠标的状态。

    • 通过 IsKeyPressedIsMouseButtonPressed 获取当前状态。

    • 优点:方便处理“持续按键、持续鼠标点击、拖动”等操作。

总结:事件系统适合瞬时响应,轮询系统适合持续状态检测。两者结合,才能满足游戏开发的灵活性。

例子:玩家控制角色移动

假设你在做一个简单的游戏,玩家用 WASD 键控制角色移动

用事件系统(Event)处理按键

事件系统捕捉 按下和释放事件

void OnKeyPressed(KeyPressedEvent& e)
{
    if (e.GetKeyCode() == HZ_KEY_W)
        player.Jump(); // 一次性动作
}

特点:

  • 只触发一次,适合瞬时动作(跳跃、开火、攻击一次)

  • 问题:如果玩家按住 W 键不放,事件系统不会持续触发跳跃或移动,除非自己写计时逻辑。

用轮询系统(Polling)处理按键

轮询系统每帧检查按键状态:

if (Input::IsKeyPressed(HZ_KEY_W))
    player.MoveForward(deltaTime);

if (Input::IsKeyPressed(HZ_KEY_S))
    player.MoveBackward(deltaTime);

特点:

  • 持续检测按键状态 → 玩家按住 W 键就会持续前进

  • 非常适合连续动作、摄像机控制、拖动物体等

  • 问题:不适合一次性事件(例如只想开火一次,按下一次即可)

两者结合(最佳实践)

// 按键事件系统处理瞬时动作
void OnKeyPressed(KeyPressedEvent& e)
{
    if (e.GetKeyCode() == HZ_KEY_SPACE)
        player.Jump(); // 一次跳跃
}

// 轮询系统处理持续动作
if (Input::IsKeyPressed(HZ_KEY_W))
    player.MoveForward(deltaTime);
if (Input::IsKeyPressed(HZ_KEY_A))
    player.MoveLeft(deltaTime);

优势:

  • 玩家按 W/A/S/D → 持续移动(轮询)

  • 玩家按空格 → 跳一次(事件系统)

  • 两个系统互不干扰,逻辑清晰


总结

  • 事件系统 → 一次性触发(Jump、Shoot、Open Door)

  • 轮询系统 → 持续状态(移动、瞄准、拖拽)

  • 结合使用 → 游戏输入处理更灵活、可控

  • 两者处理方式本质不同

    系统

    触发方式

    优点

    缺点

    事件系统

    被动,依赖事件回调(KeyPressedEvent, MouseMovedEvent)

    精确捕捉一次性动作、不会浪费 CPU

    不能直接知道“当前按键是否被按住”

    轮询系统

    主动,每帧主动查询状态(IsKeyPressed, GetMousePosition)

    可连续检测按键状态,适合持续移动、拖拽

    对瞬时事件需要自己记录按下/释放变化

    核心区别:事件系统是瞬时响应,轮询系统是状态检测

2. Hazel Input 系统的结构设计

Hazel 的 Input 系统有几个设计特点:

2.1 平台无关抽象

class HAZEL_API Input
{
public:
    inline static bool IsKeyPressed(int keycode) { return s_Instance->IsKeyPressedImpl(keycode); }
    inline static bool IsMouseButtonPressed(int button) { return s_Instance->IsMouseButtonPressedImpl(button); }
    inline static std::pair<float, float> GetMousePosition() { return s_Instance->GetMousePositionImpl(); }
    inline static float GetMouseX() { return s_Instance->GetMouseXImpl(); }
    inline static float GetMouseY() { return s_Instance->GetMouseYImpl(); }
    
protected:
    virtual bool IsKeyPressedImpl(int keycode) = 0;
    virtual bool IsMouseButtonPressedImpl(int button) = 0;
    virtual std::pair<float, float> GetMousePositionImpl() = 0;
    virtual float GetMouseXImpl() = 0;
    virtual float GetMouseYImpl() = 0;

private:
    static Input* s_Instance;
};
  • 静态函数接口:外部使用者只需调用 Input::IsKeyPressed(),无需创建对象。

  • 虚函数实现:底层由平台相关类(例如 WindowsInput)实现。

  • 单例模式:保证整个引擎只有一个 Input 实例,避免状态混乱。

设计意图

  1. 跨平台:不同平台可以有不同实现(GLFW、Win32、SDL)。

  2. 简洁接口:上层逻辑不关心平台,直接调用静态函数即可。

  3. 全局状态统一:键盘、鼠标状态在全局唯一,防止重复或冲突。


2.2 静态接口 + 单例

  • 静态接口提供了简洁 API:

if (Input::IsKeyPressed(HZ_KEY_SPACE)) { ... }
  • 单例内部通过 s_Instance 调用虚函数实现真正的键鼠查询:

return s_Instance->IsKeyPressedImpl(keycode);
  • 这样实现了**“外部像静态类使用,内部多态实现平台适配”**。


2.3 鼠标位置设计

  • GetMousePosition() 返回 (x, y) 对:方便一次获取全局鼠标位置。

  • GetMouseX() / GetMouseY() 分别获取单个坐标:方便只关注一维坐标的逻辑,例如水平滑动条。

总结:这是典型的游戏引擎 API 设计风格,兼顾灵活性和方便性。


3. WindowsInput 平台实现示例

std::pair<float, float> WindowsInput::GetMousePositionImpl()
{
    double x, y;
    glfwGetCursorPos(m_Window, &x, &y);
    return {(float)x, (float)y};
}

float WindowsInput::GetMouseXImpl()
{
    auto [x, y] = GetMousePositionImpl();
    return x;
}

float WindowsInput::GetMouseYImpl()
{
    auto [x, y] = GetMousePositionImpl();
    return y;
}

bool WindowsInput::IsKeyPressedImpl(int keycode)
{
    auto state = glfwGetKey(m_Window, keycode);
    return state == GLFW_PRESS || state == GLFW_REPEAT;
}

bool WindowsInput::IsMouseButtonPressedImpl(int button)
{
    auto state = glfwGetMouseButton(m_Window, button);
    return state == GLFW_PRESS;
}

特点:

  1. 利用 GLFW API 获取底层键鼠状态。

  2. 鼠标坐标使用 double → float 转换。

  3. IsKeyPressedImpl 支持连续按压状态(GLFW_PRESS + GLFW_REPEAT)。


4. Hazel Input 轮询的优势

优势

说明

持续状态检测

可以判断玩家是否持续按下按键或鼠标

跨平台统一接口

上层逻辑不用关心 GLFW / Win32 / SDL 等

高频查询

每帧可查询当前状态,无需事件回调等待

易用性

静态函数调用方便,上层代码简洁

总结:事件系统 + 轮询系统组合,让 Hazel 的输入系统既能响应瞬时事件,也能处理连续输入,满足绝大多数游戏开发需求。


5. 使用示例

if (Input::IsKeyPressed(HZ_KEY_W))
{
    camera.MoveForward(deltaTime);
}

auto [mx, my] = Input::GetMousePosition();
if (Input::IsMouseButtonPressed(HZ_MOUSE_BUTTON_LEFT))
{
    ui.OnClick(mx, my);
}
  • 简洁、直观、可读性高。

  • 不用关心 GLFW 句柄或底层实现细节。


6. 小结

  1. Input 轮询是 Hazel 游戏引擎中关键模块,用于实时查询键盘和鼠标状态

  2. 设计思想

    • 静态接口 → 易用性

    • 虚函数 + 单例 → 平台适配 + 全局状态

  3. 作用

    • 支持持续按键/鼠标检测

    • 与事件系统配合,提高游戏逻辑响应能力

    • 提供跨平台统一接口

  4. 实践:WindowsInput 使用 GLFW API 实现,返回键状态和鼠标位置。

Hazel 引擎学习笔记 (十九):Input 轮询系统详解
http://localhost:8090/archives/hazel-yin-qing-xue-xi-bi-ji-shi-jiu-input-lun-xun-xi-tong-xiang-jie
作者
Zxy
发布于
更新于
许可