How do I do a proper input class in GLFW for a game engine
Asked Answered
I

1

5

I'm doing a little project of mine where I'm basically creating my own c++ game engine / framework for creating graphics and or simple games. I use OpenGL with GLFW. My goal is to have something similar to various graphics frameworks, like raylib or openFrameworks (but stripped down of course)

Actually so far everything works fine, but I can't figure out how to properly separate the input from the window class, as having the window handle input seems rather clunky to me and just clutters the window class.

This is a quick oversimplified recreation of my window class. (I didn't include the enum class with the keycodes.)

#pragma once
#include "../extern/GLFW/glfw3.h"
#include <string>
class Window {
private:
    GLFWwindow* mWindow;
    int mWidth;
    int mHeight;
    std::string mTitle;

public:
    Window();
    ~Window();

    void createWindow(std::string title, int width, int height);
    void mainLoop();

    GLFWwindow* getWindow() const { return mWindow; }


// Input
private:

    bool Window::getKeyStatus(KEY key) {
    static void keyCallback(GLFWwindow* mWindow, int key, int scancode, int action, int mods);
    bool isKeyDown(KEY key);
};

And this is the implementation plus

#include "Window.h"
#include <iostream>

Window::Window() {}
Window::~Window() {}

void Window::createWindow(std::string title, int width, int height) {
    if (!glfwInit());
    mWindow = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);

    if (!getWindow()) {
        glfwTerminate();
    }


    glfwSetWindowUserPointer(getWindow(), this);
    glfwMakeContextCurrent(getWindow());


    glfwSetKeyCallback(mWindow, keyCallback);
}

void Window::mainLoop() {
    while (!glfwWindowShouldClose(getWindow())) {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);

        /* Swap front and back buffers */
        glfwSwapBuffers(getWindow());

        /* Poll for and process events */
        glfwPollEvents();


        if (isKeyDown(KEY::A)) {
            std::cout << "A down" << std::endl;
        }
        if (isKeyDown(KEY::B)) {
            std::cout << "B down" << std::endl;
        }

    }

    glfwTerminate();
}

void Window::keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
    Window* win = (Window*)glfwGetWindowUserPointer(window);

    if (key == (int)KEY::ESCAPE && action == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, GL_TRUE);
    } else {
        win->currentKeyState[key] = action;
    }
}

bool Window::getKeyStatus(KEY key) {
    return glfwGetKey(mWindow, (int)key);
}
bool Window::isKeyDown(KEY key) {
    bool down = false;
    if (getKeyStatus(key) == 1) {
        down = true;
    }
    return down;
}

How do I proceed from that? My main problem is that I don't seem to be able to connect my window and the input class. Should I use inheritance or friend classes. Should I have glfw's callbacks in the window class (which I assume) or should I move them to the input class? How can I connect those two classes so that I don't have to always use window pointers, like "isKeyDown(GLFWwindow* window, Key keycode)", but instead only "isKeyDown(Key keycode)". If it's not too much too ask can somebody write a simplistic input class?

Thanks in advance

Immigration answered 8/4, 2019 at 12:13 Comment(0)
D
13

One thing to point out is that GLFW provides both polled and callback-based input handling. In your example you are using both: polling for handling the A and B keys and a callback for handling the escape key. I believe that you'll find the callback to be easier to work with when creating a separate input class (for both keyboard and mouse).

There are many ways to go about creating a KeyInput class using GLFW's callback method. Here is my preferred technique (written to fit your code). It allows for multiple KeyInput instances (unlike a singleton) meaning that you could have an instance of KeyInput for the UI keys, an instance for in-game keys, etc.

In summary: each KeyInput instance monitors the pressed-state of a list of keys, user-defined at construction. It has a getter and a setter to access the pressed-state for any key (though the getter and setter only work with the monitored keys). Whenever the GLFW key-input-callback (static) is called it calls that setter for all instances of KeyInput. Instances are stored in a static vector that is added to at construction and removed from at destruction.

Input.h

#include "../extern/GLFW/glfw3.h"
#include <map>
#include <vector>

class KeyInput {
  // Main KeyInput functionality
  public:
    // Takes a list of which keys to keep state for
    KeyInput(std::vector<int> keysToMonitor);
    ~KeyInput();
    // If this KeyInput is enabled and the given key is monitored,
    // returns pressed state.  Else returns false.
    bool getIsKeyDown(int key);
    // See _isEnabled for details
    bool getIsEnabled() { return _isEnabled; }
    void setIsEnabled(bool value) { _isEnabled = value; }
  private:
    // Used internally to update key states.  Called by the GLFW callback.
    void setIsKeyDown(int key, bool isDown);
    // Map from monitored keyes to their pressed states
    std::map<int, bool> _keys;
    // If disabled, KeyInput.getIsKeyDown always returns false
    bool _isEnabled;

  // Workaround for C++ class using a c-style-callback
  public:
    // Must be called before any KeyInput instances will work
    static void setupKeyInputs(Window& window);
  private:
    // The GLFW callback for key events.  Sends events to all KeyInput instances
    static void callback(
      GLFWwindow* window, int key, int scancode, int action, int mods);
    // Keep a list of all KeyInput instances and notify them all of key events
    static std::vector<KeyInput*> _instances;
};

Input.cpp

#include "KeyInput.h"
#include <algorithm>

std::vector<KeyInput*> KeyInput::_instances;

KeyInput::KeyInput(std::vector<int> keysToMonitor) : _isEnabled(true) {
  for (int key : keysToMonitor) {
    _keys[key] = false;
  }
  // Add this instance to the list of instances
  KeyInput::_instances.push_back(this);
}

KeyInput::~KeyInput() {
  // Remove this instance from the list of instances
  _instances.erase(std::remove(_instances.begin(), _instances.end(), this), _instances.end());
}

bool KeyInput::getIsKeyDown(int key) {
  bool result = false;
  if (_isEnabled) {
    std::map<int,bool>::iterator it = _keys.find(key);
    if (it != _keys.end()) {
      result = _keys[key];
    }
  }
  return result;
}

void KeyInput::setIsKeyDown(int key, bool isDown) {
  std::map<int,bool>::iterator it = _keys.find(key);
  if (it != _keys.end()) {
    _keys[key] = isDown;
  }
}

void KeyInput::setupKeyInputs(Window& window) {
  glfwSetKeyCallback(window.getWindow(), KeyInput::callback);
}

void KeyInput::callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
  // Send key event to all KeyInput instances
  for (KeyInput* keyInput : _instances) {
    keyInput->setIsKeyDown(key, action != GLFW_RELEASE);
  }
}
Dorsal answered 8/4, 2019 at 23:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.