From 507ac1447c771dc33310058d8455d9b7be59ae5f Mon Sep 17 00:00:00 2001 From: Natalie Martin Date: Tue, 17 Sep 2024 18:08:23 -0700 Subject: [PATCH] Add keyboard input class Add the KeyboardState class. This class is designed to be injected by a mock so that input code can be handled in tests. This is the first part of the input system, which will also include mouse and gamepad handlers as well as an abstract "actions" handler. --- CMakeLists.txt | 3 +- src/input/keyboard/KeyboardHandler.h | 22 ++++++ src/input/keyboard/KeyboardState.cpp | 83 ++++++++++++++++++++++ src/input/keyboard/KeyboardState.h | 47 +++++++++++++ test/input/keyboard.test.cpp | 101 +++++++++++++++++++++++++++ 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/input/keyboard/KeyboardHandler.h create mode 100644 src/input/keyboard/KeyboardState.cpp create mode 100644 src/input/keyboard/KeyboardState.h create mode 100644 test/input/keyboard.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a02b53..886e8b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,11 +3,12 @@ project(gl-adagio) set(CMAKE_CXX_STANDARD 17) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/bin") -file(GLOB_RECURSE SOURCES "${CMAKE_SOURCE_DIR}/src/*.cpp" "${CMAKE_SOURCE_DIR}/src/literals/*.cpp" "${CMAKE_SOURCE_DIR}/src/math/*.cpp" "${CMAKE_SOURCE_DIR}/src/event/*.cpp" "${CMAKE_SOURCE_DIR}/src/audio/*.cpp" "${CMAKE_SOURCE_DIR}/src/graphics/*.cpp" "${CMAKE_SOURCE_DIR}/src/state/*.cpp" "${CMAKE_SOURCE_DIR}/src/game/systems/*.cpp" +file(GLOB_RECURSE SOURCES "${CMAKE_SOURCE_DIR}/src/*.cpp" "${CMAKE_SOURCE_DIR}/src/literals/*.cpp" "${CMAKE_SOURCE_DIR}/src/input/*.cpp" "${CMAKE_SOURCE_DIR}/src/math/*.cpp" "${CMAKE_SOURCE_DIR}/src/event/*.cpp" "${CMAKE_SOURCE_DIR}/src/audio/*.cpp" "${CMAKE_SOURCE_DIR}/src/graphics/*.cpp" "${CMAKE_SOURCE_DIR}/src/state/*.cpp" "${CMAKE_SOURCE_DIR}/src/game/systems/*.cpp" "${CMAKE_SOURCE_DIR}/src/game/components/**/*.cpp" "${CMAKE_SOURCE_DIR}/src/game/states/*.cpp" "${CMAKE_SOURCE_DIR}/src/game/*.cpp") file(GLOB_RECURSE TESTS "${CMAKE_SOURCE_DIR}/src/literals/*.cpp" "${CMAKE_SOURCE_DIR}/test/literals/*.cpp" + "${CMAKE_SOURCE_DIR}/src/input/*.cpp" "${CMAKE_SOURCE_DIR}/test/input/**/*.cpp" "${CMAKE_SOURCE_DIR}/src/math/*.cpp" "${CMAKE_SOURCE_DIR}/test/math/*.cpp" "${CMAKE_SOURCE_DIR}/src/audio/*.cpp" "${CMAKE_SOURCE_DIR}/test/audio/**/*.cpp" "${CMAKE_SOURCE_DIR}/src/game/factories/*.cpp" "${CMAKE_SOURCE_DIR}/src/game/systems/*.cpp" "${CMAKE_SOURCE_DIR}/src/game/components/**/*.cpp" "${CMAKE_SOURCE_DIR}/test/game/**/*.cpp" diff --git a/src/input/keyboard/KeyboardHandler.h b/src/input/keyboard/KeyboardHandler.h new file mode 100644 index 0000000..ae38e51 --- /dev/null +++ b/src/input/keyboard/KeyboardHandler.h @@ -0,0 +1,22 @@ +#ifndef GL_ADAGIO_KEYBOARDHANDLER_H +#define GL_ADAGIO_KEYBOARDHANDLER_H + +#include + +namespace Adagio { + + typedef std::uint16_t keycode; + + struct KeyboardHandler { + virtual keycode getNextKey() = 0; + + virtual char getNextChar() = 0; + + virtual bool isKeyDown(keycode) = 0; + + virtual bool isKeyUp(keycode) = 0; + }; + +} + +#endif //GL_ADAGIO_KEYBOARDHANDLER_H diff --git a/src/input/keyboard/KeyboardState.cpp b/src/input/keyboard/KeyboardState.cpp new file mode 100644 index 0000000..0170c0b --- /dev/null +++ b/src/input/keyboard/KeyboardState.cpp @@ -0,0 +1,83 @@ +#include "KeyboardState.h" + +void Adagio::KeyboardState::setHandler(Adagio::KeyboardHandler *handle) { + handler = handle; +} + +bool Adagio::KeyboardState::isKeyDown(Adagio::keycode key) const { + auto it = keys.find(key); + if (it != keys.end()) { + return it->second.keyDown; + } + return false; +} + +bool Adagio::KeyboardState::isKeyUp(Adagio::keycode key) const { + return !isKeyDown(key); +} + +bool Adagio::KeyboardState::hasKeyPressStarted(Adagio::keycode key) const { + auto it = keys.find(key); + if (it != keys.end()) { + return it->second.keyPressed; + } + return false; +} + +bool Adagio::KeyboardState::hasKeyPressEnded(Adagio::keycode key) const { + auto it = keys.find(key); + if (it != keys.end()) { + return it->second.keyReleased; + } + return false; +} + +void Adagio::KeyboardState::update() { + updateTextBuffer(); + checkKnownKeys(); + scanForNewKeyPresses(); +} + +void Adagio::KeyboardState::updateTextBuffer() { + for (auto &c: textBuffer) { + if (c == 0) { + break; + } + c = 0; + } + char bufferPos = 0; + char c = handler->getNextChar(); + while (c != 0) { + textBuffer[bufferPos++] = c; + if (bufferPos > 8) { + break; + } + c = handler->getNextChar(); + } +} + +void Adagio::KeyboardState::checkKnownKeys() { + for (auto &keyState: keys) { + keyState.second.keyPressed = false; + keyState.second.keyReleased = keyState.second.keyDown && handler->isKeyUp(keyState.first); + keyState.second.keyDown = handler->isKeyDown(keyState.first); + } +} + +void Adagio::KeyboardState::scanForNewKeyPresses() { + keycode newKey = handler->getNextKey(); + while (newKey != 0) { + keys[newKey].keyDown = true; + keys[newKey].keyPressed = true; + keys[newKey].keyReleased = false; + newKey = handler->getNextKey(); + } +} + +Adagio::KeyboardState::KeyboardState() { + keys.reserve(256); +} + +const char *Adagio::KeyboardState::readTextBuffer() const { + return textBuffer; +} diff --git a/src/input/keyboard/KeyboardState.h b/src/input/keyboard/KeyboardState.h new file mode 100644 index 0000000..7fc45de --- /dev/null +++ b/src/input/keyboard/KeyboardState.h @@ -0,0 +1,47 @@ +#ifndef GL_ADAGIO_KEYBOARDSTATE_H +#define GL_ADAGIO_KEYBOARDSTATE_H + +#include +#include +#include "KeyboardHandler.h" + +namespace Adagio { + class KeyboardState { + public: + KeyboardState(); + + void setHandler(KeyboardHandler *h); + + bool isKeyDown(keycode) const; + + bool isKeyUp(keycode) const; + + bool hasKeyPressStarted(keycode) const; + + bool hasKeyPressEnded(keycode) const; + + const char *readTextBuffer() const; + + void update(); + + private: + struct KeyBitState { + bool keyDown: 1; + bool keyPressed: 1; + bool keyReleased: 1; + }; + + char textBuffer[8]{0, 0, 0, 0, 0, 0, 0, 0}; + KeyboardHandler *handler{nullptr}; + std::unordered_map keys; + + void scanForNewKeyPresses(); + + void checkKnownKeys(); + + void updateTextBuffer(); + }; +} + + +#endif //GL_ADAGIO_KEYBOARDSTATE_H diff --git a/test/input/keyboard.test.cpp b/test/input/keyboard.test.cpp new file mode 100644 index 0000000..1c819d4 --- /dev/null +++ b/test/input/keyboard.test.cpp @@ -0,0 +1,101 @@ +#include +#include "../../src/input/keyboard/KeyboardState.h" + +class MockKeyboard : public Adagio::KeyboardHandler { +public: + std::unordered_map keys; + std::vector pressedKeys; + std::string buffer; + + void writeChars(const std::string &input) { + buffer += input; + } + + void pressKey(Adagio::keycode key) { + keys[key] = true; + pressedKeys.push_back(key); + } + + void releaseKey(Adagio::keycode key) { + keys[key] = false; + } + + Adagio::keycode getNextKey() override { + if (!pressedKeys.empty()) { + Adagio::keycode key = pressedKeys.back(); + pressedKeys.pop_back(); + return key; + } + return 0; + } + + char getNextChar() override { + if (buffer.empty()) { + return 0; + } + char c = buffer[0]; + buffer.erase(buffer.begin()); + return c; + } + + bool isKeyDown(Adagio::keycode key) override { + if (keys.find(key) != keys.end()) { + return keys[key]; + } + return false; + } + + bool isKeyUp(Adagio::keycode key) override { + if (keys.find(key) != keys.end()) { + return !keys[key]; + } + return true; + } +}; + +TEST_CASE("KeyboardState", "[input][keyboard]") { + const Adagio::keycode TEST_KEY_1 = 128; + MockKeyboard mockKeyboard; + Adagio::KeyboardState keyboard; + keyboard.setHandler(&mockKeyboard); + + SECTION("It can query an empty keyboard state") { + REQUIRE_FALSE(keyboard.isKeyDown(TEST_KEY_1)); + REQUIRE(keyboard.isKeyUp(TEST_KEY_1)); + REQUIRE_FALSE(keyboard.hasKeyPressStarted(TEST_KEY_1)); + REQUIRE_FALSE(keyboard.hasKeyPressEnded(TEST_KEY_1)); + } + + SECTION("It can detect a key press") { + mockKeyboard.pressKey(TEST_KEY_1); + keyboard.update(); + REQUIRE(keyboard.isKeyDown(TEST_KEY_1)); + REQUIRE(keyboard.hasKeyPressStarted(TEST_KEY_1)); + keyboard.update(); + REQUIRE(keyboard.isKeyDown(TEST_KEY_1)); + REQUIRE_FALSE(keyboard.hasKeyPressStarted(TEST_KEY_1)); + } + + SECTION("It can detect a key release") { + mockKeyboard.pressKey(TEST_KEY_1); + keyboard.update(); + REQUIRE_FALSE(keyboard.isKeyUp(TEST_KEY_1)); + mockKeyboard.releaseKey(TEST_KEY_1); + keyboard.update(); + REQUIRE(keyboard.isKeyUp(TEST_KEY_1)); + REQUIRE(keyboard.hasKeyPressEnded(TEST_KEY_1)); + keyboard.update(); + REQUIRE(keyboard.isKeyUp(TEST_KEY_1)); + REQUIRE_FALSE(keyboard.hasKeyPressEnded(TEST_KEY_1)); + } + + SECTION("It can read from a text input buffer") { + mockKeyboard.writeChars("potato"); + keyboard.update(); + std::string input = keyboard.readTextBuffer(); + REQUIRE(input == "potato"); + keyboard.update(); + input = keyboard.readTextBuffer(); + REQUIRE(input.empty()); + } +}