游戏引擎从零开始(10)-输入(Input)

前言

一个用户交互的程序一定有键盘输入,不同的平台按键对应的编码也不一样,我们怎么知道用户按了什么键?

这节,我们实现一个输入(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

其他的处理

  1. 更新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"
  1. 更新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());
        }
    }
};

如果没有问题,运行起来,点击按键和鼠标事件,能看到对应的日志

完整代码参考

add Input

总结

这节的代码不多,照着敲应该很快就能实现。对于C++新手,建议花点时间理解Input抽象的实现,简单的Input用到了单例、代理、多态等编程思想。

相关推荐
刃神太酷啦27 分钟前
数据结构(蓝桥杯常考点)
数据结构·c++·蓝桥杯c++组
17´37 分钟前
Qt从入门到入土(八) -打包Qt程序
开发语言·c++·qt
誓约酱1 小时前
(每日一题) 力扣 860 柠檬水找零
linux·c语言·c++·算法·leetcode·职场和发展
城西往事2 小时前
DeepSeek 解释C语言函数memset
c++
孞㐑¥2 小时前
C++vector类
开发语言·c++·经验分享·笔记
chenyuhao20243 小时前
非常重要的动态内存错误和柔性数组1
c语言·c++·算法·柔性数组
坚定学代码4 小时前
Qt C++ 实际开发中宏编译的运用
c++·qt
香菇滑稽之谈4 小时前
装饰器模式的C++实现示例
c++·算法·设计模式·装饰器模式
小赵起名困难户4 小时前
蓝桥杯备赛-差分-重新排序
c++·算法·蓝桥杯
牵牛老人4 小时前
C++设计模式-简单工厂模式:从原理、应用、实践指南与常见问题和解决方案深度解析
c++·设计模式·简单工厂模式