Skip to content

Commit

Permalink
Add keyboard input class
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
meisekimiu committed Sep 18, 2024
1 parent c0a871a commit 507ac14
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
22 changes: 22 additions & 0 deletions src/input/keyboard/KeyboardHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#ifndef GL_ADAGIO_KEYBOARDHANDLER_H
#define GL_ADAGIO_KEYBOARDHANDLER_H

#include <cstdint>

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
83 changes: 83 additions & 0 deletions src/input/keyboard/KeyboardState.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
47 changes: 47 additions & 0 deletions src/input/keyboard/KeyboardState.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#ifndef GL_ADAGIO_KEYBOARDSTATE_H
#define GL_ADAGIO_KEYBOARDSTATE_H

#include <unordered_map>
#include <cstdint>
#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<keycode, KeyBitState> keys;

void scanForNewKeyPresses();

void checkKnownKeys();

void updateTextBuffer();
};
}


#endif //GL_ADAGIO_KEYBOARDSTATE_H
101 changes: 101 additions & 0 deletions test/input/keyboard.test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include <catch2/catch.hpp>
#include "../../src/input/keyboard/KeyboardState.h"

class MockKeyboard : public Adagio::KeyboardHandler {
public:
std::unordered_map<Adagio::keycode, bool> keys;
std::vector<Adagio::keycode> 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());
}
}

0 comments on commit 507ac14

Please sign in to comment.