前言
一个用户交互的程序一定有键盘输入,不同的平台按键对应的编码也不一样,我们怎么知道用户按了什么键?
这节,我们实现一个输入(Input)模块,解决这个问题,并可以拓展到不同的平台。
实现跨平台,最直接的思想就是抽象,提炼出通用接口,由各个平台各自去实现。
这节没有复杂的逻辑,可以学习C++中如何做好抽象、多态,提升C++工程能力。
跨平台的Input
我们要在在ExampleLayer层通过Input判断是否按下了TAB键。
c++
class ExampleLayer : public Hazel::Layer {
public:
...
void OnUpdate() override{
if (Hazel::Input::IsKeyPressed(HZ_KEY_TAB)) {
HZ_TRACE("TAB key is pressed(poll)!");
}
}
参考下图,捋下事件传递的流程:
Input并没有对事件做什么,就是个工具类,命名成InputUtils可能更明确。
Input抽象
为了方便,Input是个单例,对外提供了一套静态的方法,然后声明了一组同样功能的virtual方法,由子类实现。如下图,WindowsInput实现了Input的virtual方法。
基类Input
Sandbox/Hazel/src/Hazel/Input.h
c++
#pragma once
#include <utility>
namespace Hazel {
class 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 std::pair<float, float> GetMousePositionImpl() = 0;
virtual bool IsMouseButtonPressedImpl(int button) = 0;
virtual float GetMouseXImpl() = 0;
virtual float GetMouseYImpl() = 0;
private:
static Input* s_Instance;
};
}
Input实现类WindowsInput
子类的声明与实现 Sandbox/Hazel/src/Hazel/Platform/Windows/WindowsInput.h
c++
#pragma once
#include "Input.h"
namespace Hazel {
class WindowsInput : public Input{
protected:
virtual bool IsKeyPressedImpl(int keycode) override;
virtual std::pair<float, float> GetMousePositionImpl()override;
virtual bool IsMouseButtonPressedImpl(int button) override;
virtual float GetMouseXImpl() override;
virtual float GetMouseYImpl() override;
};
}
WindowsInput.cpp中封装了基于GLFW的输入处理。WindowsInput很容易替换成别的实现,比如基于SDL的WindowsSDLInput,做到接口层隔离。
Sandbox/Hazel/src/Hazel/Platform/Windows/WindowsInput.cpp
c++
#include "WindowsInput.h"
#include "Application.h"
#include <GLFW/glfw3.h>
namespace Hazel {
Input* Input::s_Instance = new WindowsInput();
bool WindowsInput::IsKeyPressedImpl(int keycode) {
auto window = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
auto state = glfwGetKey(window, keycode);
return state == GLFW_PRESS || state == GLFW_REPEAT;
}
std::pair<float, float> WindowsInput::GetMousePositionImpl() {
auto window = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
double xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
return {(float)xpos, (float)ypos};
}
bool WindowsInput::IsMouseButtonPressedImpl(int button) {
auto window = static_cast<GLFWwindow*>(Application::Get().GetWindow().GetNativeWindow());
auto state = glfwGetMouseButton(window, button);
return state == GLFW_PRESS;
}
float WindowsInput::GetMouseXImpl() {
auto[x, y] = GetMousePositionImpl();
return x;
}
float WindowsInput::GetMouseYImpl() {
auto[x, y] = GetMousePositionImpl();
return y;
}
}
注意,在子类中对Input::s_Instance给了定义,s_Instance指向的是子类WindowsInput,实现了多态
注意,这里用到了C++17的语法auto[x, y] = GetMousePositionImpl(),auto绑定. 需要在CMake中升级C++版本,下面有讲到。
辅助类
按键码和鼠标事件码定义,代码较长,这里只附录部分定义.这个没有特殊的地方,从glfw里抠出来的。
也可以定义一套自己的编码,再写一套和各个平台编码转换的逻辑,这里图简单,就从glfw扣出来用了。
Sandbox/Hazel/src/Hazel/KeyCodes.h
c++
#pragma once
// From glfw3.h
#define HZ_KEY_SPACE 32
#define HZ_KEY_APOSTROPHE 39 /* ' */
#define HZ_KEY_COMMA 44 /* , */
#define HZ_KEY_MINUS 45 /* - */
#define HZ_KEY_PERIOD 46 /* . */
#define HZ_KEY_SLASH 47 /* / */
#define HZ_KEY_0 48
#define HZ_KEY_1 49
#define HZ_KEY_2 50
#define HZ_KEY_3 51
...
注意,数字和字母对应的数值和标准的ascii是对应的,这很方便转换成字符或打印,但是TAB、ENTER等按键的定义就不知到是基于什么了.
Sandbox/Hazel/src/Hazel/MouseButtonCodes.h
c++
#pragma once
// From glfw3.h
#define HZ_MOUSE_BUTTON_1 0
#define HZ_MOUSE_BUTTON_2 1
#define HZ_MOUSE_BUTTON_3 2
#define HZ_MOUSE_BUTTON_4 3
#define HZ_MOUSE_BUTTON_5 4
#define HZ_MOUSE_BUTTON_6 5
#define HZ_MOUSE_BUTTON_7 6
#define HZ_MOUSE_BUTTON_8 7
#define HZ_MOUSE_BUTTON_LAST HZ_MOUSE_BUTTON_8
#define HZ_MOUSE_BUTTON_LEFT HZ_MOUSE_BUTTON_1
#define HZ_MOUSE_BUTTON_RIGHT HZ_MOUSE_BUTTON_2
#define HZ_MOUSE_BUTTON_MIDDLE HZ_MOUSE_BUTTON_3
其他的处理
- 更新Hazel.h中的include项
Sandbox/Hazel/src/Hazel.h
c++
#include "Hazel/Input.h"
#include "Hazel/KeyCodes.h"
#include "Hazel/MouseButtonCodes.h"
#include "Hazel/Events/KeyEvent.h"
- 更新CMake文件
Hazel引擎的CMake: Sandbox/Hazel/CMakeLists.txt
scss
cmake_minimum_required(VERSION 3.20)
set(CMAKE_CXX_STANDARD 17)
find_package(OpenGL REQUIRED)
project(hazel)
...
src/Hazel/Input.h
src/Hazel/Platform/Windows/WindowsInput.cpp
src/Hazel/Platform/Windows/WindowsInput.h
src/Hazel/KeyCodes.h
src/Hazel/MouseButtonCodes.h
)
SandBox应用的CMake: Sandbox/CMakeLists.txt
scss
cmake_minimum_required(VERSION 3.20)
set(CMAKE_CXX_STANDARD 17)
...
注意,CMake中,更新了C++编译器版本,从14更新到17。这是为了使用c++17中auto[x, y]绑定的语法。
SandBoxApp中使用Input
在ExampleLayer中,解析事件,打印日志,这里写了两种获取按键的处理,基于Input的处理明显更简洁。
c++
class ExampleLayer : public Hazel::Layer {
public:
...
void OnUpdate() override{
if (Hazel::Input::IsKeyPressed(HZ_KEY_TAB)) {
HZ_TRACE("TAB key is pressed(poll)!");
}
}
void OnEvent(Hazel::Event& event) override {
if (event.GetEventType() == Hazel::EventType::KeyPressed) {
auto& e = dynamic_cast<Hazel::KeyPressedEvent&>(event);
if (e.GetKeyCode() == HZ_KEY_TAB) {
HZ_TRACE("Tab key is pressed(event)!");
}
HZ_TRACE("{0}", (char)e.GetKeyCode());
}
}
};
如果没有问题,运行起来,点击按键和鼠标事件,能看到对应的日志
完整代码参考
总结
这节的代码不多,照着敲应该很快就能实现。对于C++新手,建议花点时间理解Input抽象的实现,简单的Input用到了单例、代理、多态等编程思想。