游戏引擎从零开始(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用到了单例、代理、多态等编程思想。

相关推荐
ragnwang1 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
lqqjuly4 小时前
特殊的“Undefined Reference xxx“编译错误
c语言·c++
冰红茶兑滴水4 小时前
云备份项目--工具类编写
linux·c++
刘好念4 小时前
[OpenGL]使用 Compute Shader 实现矩阵点乘
c++·计算机图形学·opengl·glsl
酒鬼猿5 小时前
C++进阶(二)--面向对象--继承
java·开发语言·c++
姚先生975 小时前
LeetCode 209. 长度最小的子数组 (C++实现)
c++·算法·leetcode
小王爱吃月亮糖6 小时前
QT开发【常用控件1】-Layouts & Spacers
开发语言·前端·c++·qt·visual studio
aworkholic6 小时前
opencv sdk for java中提示无stiching模块接口的问题
java·c++·opencv·jni·opencv4android·stiching
程序员老冯头6 小时前
第十六章 C++ 字符串
开发语言·c++
Xenia2237 小时前
复习篇~第二章程序设计基础
c++·算法