1. 为什么要有 Input 轮询
在游戏引擎中,输入处理是核心功能之一。玩家的操作(键盘、鼠标)直接驱动游戏逻辑和交互。Cherno 在 Hazel 中把输入系统拆分成两部分:
事件系统(Event)
通过事件回调处理一次性动作,例如按键按下、释放。
优点:可以精确捕捉瞬间事件。
缺点:如果你想实时获取某个按键的持续状态,事件系统不方便。
轮询系统(Polling / Input)
每一帧主动查询键盘或鼠标的状态。
通过
IsKeyPressed、IsMouseButtonPressed获取当前状态。优点:方便处理“持续按键、持续鼠标点击、拖动”等操作。
总结:事件系统适合瞬时响应,轮询系统适合持续状态检测。两者结合,才能满足游戏开发的灵活性。
例子:玩家控制角色移动
假设你在做一个简单的游戏,玩家用 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)
轮询系统 → 持续状态(移动、瞄准、拖拽)
结合使用 → 游戏输入处理更灵活、可控
两者处理方式本质不同
核心区别:事件系统是瞬时响应,轮询系统是状态检测
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 实例,避免状态混乱。
设计意图:
跨平台:不同平台可以有不同实现(GLFW、Win32、SDL)。
简洁接口:上层逻辑不关心平台,直接调用静态函数即可。
全局状态统一:键盘、鼠标状态在全局唯一,防止重复或冲突。
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;
}
特点:
利用 GLFW API 获取底层键鼠状态。
鼠标坐标使用 double → float 转换。
IsKeyPressedImpl支持连续按压状态(GLFW_PRESS+GLFW_REPEAT)。
4. Hazel Input 轮询的优势
总结:事件系统 + 轮询系统组合,让 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. 小结
Input 轮询是 Hazel 游戏引擎中关键模块,用于实时查询键盘和鼠标状态。
设计思想:
静态接口 → 易用性
虚函数 + 单例 → 平台适配 + 全局状态
作用:
支持持续按键/鼠标检测
与事件系统配合,提高游戏逻辑响应能力
提供跨平台统一接口
实践:WindowsInput 使用 GLFW API 实现,返回键状态和鼠标位置。