feat: reimplement input system

This commit is contained in:
light7734 2025-09-18 19:16:54 +03:30
parent 21e9933a42
commit d69315c6aa
Signed by: light7734
GPG key ID: 8C30176798F1A6BA
8 changed files with 432 additions and 283 deletions

View file

@ -1,2 +1,4 @@
add_library_module(input input.cpp)
target_link_libraries(input PUBLIC surface math imgui::imgui logger)
add_library_module(input system.cpp)
target_link_libraries(input PUBLIC surface math logger)
add_test_module(input system.test.cpp)

View file

@ -1,159 +0,0 @@
#include <imgui.h>
#include <input/input.hpp>
#include <logger/logger.hpp>
namespace lt {
Input::Input(): m_mouse_position {}, m_mouse_delta {}
{
restart_input_state();
}
void Input::receive_user_interface_events_impl(bool receive, bool toggle /* = false */)
{
m_user_interface_events = toggle ? !m_user_interface_events : receive;
}
void Input::receieve_game_events_impl(bool receive, bool toggle /*= false*/)
{
auto prev = m_game_events;
m_game_events = toggle ? !m_user_interface_events : receive;
if (m_game_events != prev)
{
restart_input_state();
}
}
void Input::restart_input_state()
{
m_keyboad_keys.fill(false);
m_mouse_buttons.fill(false);
m_mouse_position = math::vec2(0.0f);
m_mouse_delta = math::vec2(0.0f);
m_mouse_wheel_delta = 0.0f;
}
void Input::on_event(const Event &inputEvent)
{
auto &io = ImGui::GetIO();
switch (inputEvent.get_event_type())
{
//** MOUSE_EVENTS **//
case EventType::MouseMoved:
{
const auto &event = dynamic_cast<const MouseMovedEvent &>(inputEvent);
if (m_game_events)
{
m_mouse_delta = event.get_position() - m_mouse_position;
m_mouse_position = event.get_position();
}
if (m_user_interface_events)
{
io.MousePos = ImVec2(event.get_x(), event.get_y());
}
return;
}
case EventType::ButtonPressed:
{
const auto &event = dynamic_cast<const ButtonPressedEvent &>(inputEvent);
if (m_game_events)
{
m_mouse_buttons[event.get_button()] = true;
}
if (m_user_interface_events)
{
io.MouseDown[event.get_button()] = true;
}
return;
}
case EventType::ButtonReleased:
{
const auto &event = dynamic_cast<const ButtonReleasedEvent &>(inputEvent);
if (m_game_events)
{
m_mouse_buttons[event.get_button()] = false;
}
if (m_user_interface_events)
{
io.MouseDown[event.get_button()] = false;
}
return;
}
case EventType::WheelScrolled:
{
const auto &event = dynamic_cast<const WheelScrolledEvent &>(inputEvent);
if (m_game_events)
{
m_mouse_wheel_delta = event.get_offset();
}
if (m_user_interface_events)
{
io.MouseWheel = event.get_offset();
}
return;
}
//** KEYBOARD_EVENTS **//
case EventType::KeyPressed:
{
const auto &event = dynamic_cast<const KeyPressedEvent &>(inputEvent);
if (m_game_events)
{
m_keyboad_keys[event.get_key()] = true;
}
if (m_user_interface_events)
{
// io.AddKeyEvent(event.get_key(), true);
// if (event.get_key() == Key::BackSpace)
// io.AddInputCharacter(Key::BackSpace);
}
return;
}
case EventType::KeyReleased:
{
const auto &event = dynamic_cast<const KeyReleasedEvent &>(inputEvent);
if (m_game_events)
{
m_keyboad_keys[event.get_key()] = false;
}
if (m_user_interface_events)
{
// io.AddKeyEvent(event.get_key(), false);
}
return;
}
case EventType::SetChar:
{
if (m_user_interface_events)
{
const auto &event = dynamic_cast<const SetCharEvent &>(inputEvent);
io.AddInputCharacter(event.get_character());
}
return;
}
default: log_trc("Dropped event");
}
}
} // namespace lt

View file

@ -1,5 +1,138 @@
#include <input/components.hpp>
#include <input/system.hpp>
namespace lt::input {
template<class... Ts>
struct overloads: Ts...
{
using Ts::operator()...;
};
System::System(Ref<ecs::Registry> registry): m_registry(std::move(registry))
{
ensure(m_registry, "Failed to initialize input system: null registry");
}
auto System::tick() -> bool
{
m_registry->view<surface::SurfaceComponent>().each([&](const entt::entity,
surface::SurfaceComponent &surface) {
for (const auto &event : surface.peek_events())
{
handle_event(event);
}
});
m_registry->view<InputComponent>().each([&](const entt::entity, InputComponent &input) {
// TODO(Light): instead of iterating over all actions each frame,
// make a list of "dirty" actions to reset
// and a surface_input->input_action mapping to get to action through input
// instead of brute-force checking all of them.
for (auto &action : input.m_actions)
{
auto code = action.trigger.mapped_keycode;
if (code < m_keys.size() && m_keys[code])
{
if (action.state == InputAction::State::triggered)
{
action.state = InputAction::State::active;
}
else if (action.state == InputAction::State::inactive)
{
action.state = InputAction::State::triggered;
}
}
else
{
action.state = InputAction::State::inactive;
}
}
});
return false;
}
void System::on_register()
{
}
void System::on_unregister()
{
}
void System::handle_event(const surface::SurfaceComponent::Event &event)
{
const auto visitor = overloads {
[this](const surface::ClosedEvent &) { on_surface_lost_focus(); },
[this](const surface::LostFocusEvent &) { on_surface_lost_focus(); },
[this](const surface::KeyPressedEvent &event) { on_key_press(event); },
[this](const surface::KeyReleasedEvent &event) { on_key_release(event); },
[this](const surface::MouseMovedEvent &event) { on_pointer_move(event); },
[this](const surface::ButtonPressedEvent &event) { on_button_press(event); },
[this](const surface::ButtonReleasedEvent &event) { on_button_release(event); },
[this](auto) {},
};
std::visit(visitor, event);
}
void System::on_surface_lost_focus()
{
for (auto &key : m_keys)
{
key = false;
}
for (auto &button : m_buttons)
{
button = false;
}
}
void System::on_key_press(const lt::surface::KeyPressedEvent &event)
{
if (event.get_key() > m_keys.size())
{
log_dbg(
"Key code larger than key container size, implement platform-dependant "
"key-code-mapping!"
);
return;
}
m_keys[event.get_key()] = true;
}
void System::on_key_release(const lt::surface::KeyReleasedEvent &event)
{
if (event.get_key() > m_keys.size())
{
log_dbg(
"Key code larger than key container size, implement platform-dependant "
"key-code-mapping!"
);
return;
}
m_keys[event.get_key()] = false;
}
void System::on_pointer_move(const lt::surface::MouseMovedEvent &event)
{
m_pointer_position = event.get_position();
}
void System::on_button_press(const lt::surface::ButtonPressedEvent &event)
{
m_buttons[event.get_button()] = true;
}
void System::on_button_release(const lt::surface::ButtonReleasedEvent &event)
{
m_buttons[event.get_button()] = false;
}
} // namespace lt::input

View file

@ -0,0 +1,169 @@
#include <ecs/entity.hpp>
#include <input/components.hpp>
#include <input/system.hpp>
#include <test/test.hpp>
// NOLINTBEGIN
using namespace lt;
using input::InputComponent;
using input::System;
using std::ignore;
using test::Case;
using test::expect_eq;
using test::expect_false;
using test::expect_ne;
using test::expect_not_nullptr;
using test::expect_throw;
using test::Suite;
// NOLINTEND
class Fixture
{
public:
[[nodiscard]] auto registry() -> Ref<ecs::Registry>
{
return m_registry;
}
auto add_input_component() -> ecs::Entity
{
auto entity = m_registry->create_entity("");
entity.add_component<InputComponent>();
return entity;
}
auto add_surface_component() -> ecs::Entity
{
auto entity = m_registry->create_entity("");
entity.add_component<surface::SurfaceComponent>(surface::SurfaceComponent::CreateInfo {});
return entity;
}
private:
Ref<ecs::Registry> m_registry = create_ref<ecs::Registry>();
};
Suite raii = [] {
Case { "happy path won't throw" } = [&] {
System { Fixture {}.registry() };
};
Case { "many won't freeze/throw" } = [&] {
auto fixture = Fixture {};
for (auto idx : std::views::iota(0, 10'000))
{
ignore = System { fixture.registry() };
}
};
Case { "unhappy path throws" } = [] {
expect_throw([] { ignore = System { {} }; });
};
};
Suite system_events = [] {
Case { "on_register won't throw" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
system.on_register();
expect_eq(fixture.registry()->view<InputComponent>().size(), 0);
};
Case { "on_unregister won't throw" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
system.on_register();
system.on_unregister();
expect_eq(fixture.registry()->view<InputComponent>().size(), 0);
};
};
Suite registry_events = [] {
Case { "on_construct<InputComnent>" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
const auto &entity = fixture.add_input_component();
expect_eq(fixture.registry()->view<InputComponent>().size(), 1);
};
Case { "on_destrroy<InputComponent>" } = [] {
auto fixture = Fixture {};
auto system = create_scope<System>(fixture.registry());
auto entity_a = fixture.add_input_component();
auto entity_b = fixture.add_input_component();
expect_eq(fixture.registry()->view<InputComponent>().size(), 2);
entity_a.remove_component<InputComponent>();
expect_eq(fixture.registry()->view<InputComponent>().size(), 1);
system.reset();
expect_eq(fixture.registry()->view<InputComponent>().size(), 1);
entity_b.remove_component<InputComponent>();
expect_eq(fixture.registry()->view<InputComponent>().size(), 0);
};
};
Suite tick = [] {
Case { "Empty tick won't throw" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
expect_false(system.tick());
};
Case { "Tick triggers input action" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
auto &surface = fixture.add_surface_component().get_component<surface::SurfaceComponent>();
auto &input = fixture.add_input_component().get_component<InputComponent>();
auto action_key = input.add_action(
{
.name { "test" },
.trigger = { .mapped_keycode = 69 },
}
);
expect_eq(input.get_action(action_key).state, input::InputAction::State::inactive);
system.tick();
expect_eq(input.get_action(action_key).state, input::InputAction::State::inactive);
surface.push_event(surface::KeyPressedEvent(69));
system.tick();
expect_eq(input.get_action(action_key).state, input::InputAction::State::triggered);
system.tick();
expect_eq(input.get_action(action_key).state, input::InputAction::State::inactive);
system.tick();
system.tick();
system.tick();
expect_eq(input.get_action(action_key).state, input::InputAction::State::inactive);
surface.push_event(surface::KeyPressedEvent(69));
system.tick();
};
Case { "Tick triggers" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
auto &surface = fixture.add_surface_component().get_component<surface::SurfaceComponent>();
auto &input = fixture.add_input_component().get_component<InputComponent>();
auto action_key = input.add_action(
{
.name { "test" },
.trigger = { .mapped_keycode = 69 },
}
);
};
};

View file

@ -0,0 +1,57 @@
#pragma once
#include <vector>
namespace lt::input {
struct Trigger
{
uint32_t mapped_keycode;
};
struct InputAction
{
using Key = size_t;
enum class State : uint8_t
{
inactive,
active,
triggered,
cancelled,
};
std::string name;
State state;
Trigger trigger;
};
class InputComponent
{
public:
InputComponent() = default;
auto add_action(InputAction action) -> size_t
{
m_actions.emplace_back(std::move(action));
return m_actions.size() - 1;
}
auto get_action(auto idx) -> const InputAction &
{
return m_actions[idx];
}
private:
friend class System;
void push_event()
{
}
std::vector<InputAction> m_actions;
};
} // namespace lt::input

View file

@ -0,0 +1,44 @@
#pragma once
#include <math/vec2.hpp>
namespace lt::input {
class AnalogEvent
{
public:
AnalogEvent(uint32_t input_code, math::uvec2 pointer_position)
: m_input_code(input_code)
, m_pointer_position(pointer_position)
{
}
[[nodiscard]] auto get_code() const -> uint32_t
{
return m_input_code;
};
[[nodiscard]] auto get_pointer_position() const -> math::uvec2
{
return m_pointer_position;
}
[[nodiscard]] auto to_string() const -> std::string
{
auto stream = std::stringstream {};
const auto &[x, y] = m_pointer_position;
stream << "input::AnalogEvent: " << m_input_code << " @ " << x << ", " << y;
return stream.str();
}
private:
uint32_t m_input_code;
math::uvec2 m_pointer_position;
};
class AxisEvent
{
};
} // namespace lt::input

View file

@ -1,80 +0,0 @@
#pragma once
#include <array>
#include <math/vec2.hpp>
namespace lt {
class Event;
class Input
{
public:
static auto instance() -> Input &
{
static auto instance = Input {};
return instance;
}
static void receive_user_interface_events(bool receive, bool toggle = false)
{
instance().receive_user_interface_events_impl(receive, toggle);
}
static void receive_game_events(bool receive, bool toggle = false)
{
instance().receieve_game_events_impl(receive, toggle);
}
static auto get_keyboard_key(int code) -> bool
{
return instance().m_keyboad_keys[code];
}
static auto get_mouse_button(int code) -> bool
{
return instance().m_mouse_buttons[code];
}
static auto get_mouse_position(int /*code*/) -> const math::vec2 &
{
return instance().m_mouse_position;
}
void on_event(const Event &inputEvent);
[[nodiscard]] auto is_receiving_input_events() const -> bool
{
return m_user_interface_events;
}
[[nodiscard]] auto is_receiving_game_events() const -> bool
{
return m_game_events;
}
private:
Input();
void receive_user_interface_events_impl(bool receive, bool toggle = false);
void receieve_game_events_impl(bool receive, bool toggle = false);
void restart_input_state();
std::array<bool, 348> m_keyboad_keys {};
std::array<bool, 8> m_mouse_buttons {};
math::vec2 m_mouse_position;
math::vec2 m_mouse_delta;
float m_mouse_wheel_delta {};
bool m_user_interface_events { true };
bool m_game_events { true };
};
} // namespace lt

View file

@ -1,63 +1,46 @@
#pragma once
#include <surface/system.hpp>
#include <app/system.hpp>
#include <ecs/scene.hpp>
#include <surface/components.hpp>
#include <surface/events/keyboard.hpp>
#include <surface/events/mouse.hpp>
namespace lt::input {
template<class... Ts>
struct overloads: Ts...
{
using Ts::operator()...;
};
/**
*
* @note If this system is attached, it will always consume the input events f rom surface.
* Therefore if you want any input detection mechanism, callbacks should be setup with this
* system and not directly with surface.
*/
class System
class System: public app::ISystem
{
public:
System(lt::surface::System &surface_system)
{
surface_system.add_event_listener([this](auto &&event) {
return handle_event(std::forward<decltype(event)>(event));
});
};
System(Ref<ecs::Registry> registry);
auto tick() -> bool override;
void on_register() override;
void on_unregister() override;
private:
auto handle_event(const lt::surface::System::Event &event) -> bool
{
const auto visitor = overloads {
[this](const lt::surface::KeyPressedEvent &event) {
m_keys[event.get_key()] = true;
return true;
},
void handle_event(const surface::SurfaceComponent::Event &event);
[](const lt::surface::KeyRepeatEvent &) { return false; },
void on_surface_lost_focus();
[](const lt::surface::KeyReleasedEvent &) { return false; },
void on_key_press(const lt::surface::KeyPressedEvent &event);
[](const lt::surface::KeySetCharEvent &) { return false; },
void on_key_release(const lt::surface::KeyReleasedEvent &event);
[](const lt::surface::MouseMovedEvent &) { return false; },
void on_pointer_move(const lt::surface::MouseMovedEvent &event);
[](const lt::surface::WheelScrolledEvent &) { return false; },
void on_button_press(const lt::surface::ButtonPressedEvent &event);
[](const lt::surface::ButtonPressedEvent &) { return false; },
void on_button_release(const lt::surface::ButtonReleasedEvent &event);
[](const lt::surface::ButtonReleasedEvent &) { return false; },
[](const auto &) { return false; },
};
return std::visit(visitor, event);
}
void setup_callbacks(GLFWwindow *handle);
Ref<ecs::Registry> m_registry;
std::array<bool, 512> m_keys {};
std::array<bool, 512> m_buttons {};
math::vec2 m_pointer_position;
};