diff --git a/modules/input/CMakeLists.txt b/modules/input/CMakeLists.txt index 063def2..9620f46 100644 --- a/modules/input/CMakeLists.txt +++ b/modules/input/CMakeLists.txt @@ -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) diff --git a/modules/input/private/input.cpp b/modules/input/private/input.cpp deleted file mode 100644 index de4ffab..0000000 --- a/modules/input/private/input.cpp +++ /dev/null @@ -1,159 +0,0 @@ -#include -#include -#include - -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(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(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(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(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(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(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(inputEvent); - io.AddInputCharacter(event.get_character()); - } - - return; - } - default: log_trc("Dropped event"); - } -} - -} // namespace lt diff --git a/modules/input/private/system.cpp b/modules/input/private/system.cpp index 0f9319b..ca6ae37 100644 --- a/modules/input/private/system.cpp +++ b/modules/input/private/system.cpp @@ -1,5 +1,138 @@ +#include #include namespace lt::input { +template +struct overloads: Ts... +{ + using Ts::operator()...; +}; + +System::System(Ref registry): m_registry(std::move(registry)) +{ + ensure(m_registry, "Failed to initialize input system: null registry"); +} + +auto System::tick() -> bool +{ + m_registry->view().each([&](const entt::entity, + surface::SurfaceComponent &surface) { + for (const auto &event : surface.peek_events()) + { + handle_event(event); + } + }); + + m_registry->view().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 diff --git a/modules/input/private/system.test.cpp b/modules/input/private/system.test.cpp new file mode 100644 index 0000000..d3b9e4c --- /dev/null +++ b/modules/input/private/system.test.cpp @@ -0,0 +1,169 @@ +#include +#include +#include +#include + +// 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 + { + return m_registry; + } + + auto add_input_component() -> ecs::Entity + { + auto entity = m_registry->create_entity(""); + entity.add_component(); + + return entity; + } + + auto add_surface_component() -> ecs::Entity + { + auto entity = m_registry->create_entity(""); + entity.add_component(surface::SurfaceComponent::CreateInfo {}); + + return entity; + } + +private: + Ref m_registry = create_ref(); +}; + +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().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().size(), 0); + }; +}; + +Suite registry_events = [] { + Case { "on_construct" } = [] { + auto fixture = Fixture {}; + auto system = System { fixture.registry() }; + + const auto &entity = fixture.add_input_component(); + expect_eq(fixture.registry()->view().size(), 1); + }; + + Case { "on_destrroy" } = [] { + auto fixture = Fixture {}; + auto system = create_scope(fixture.registry()); + + auto entity_a = fixture.add_input_component(); + auto entity_b = fixture.add_input_component(); + expect_eq(fixture.registry()->view().size(), 2); + + entity_a.remove_component(); + expect_eq(fixture.registry()->view().size(), 1); + + system.reset(); + expect_eq(fixture.registry()->view().size(), 1); + + entity_b.remove_component(); + expect_eq(fixture.registry()->view().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(); + auto &input = fixture.add_input_component().get_component(); + + 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(); + auto &input = fixture.add_input_component().get_component(); + + auto action_key = input.add_action( + { + .name { "test" }, + .trigger = { .mapped_keycode = 69 }, + } + ); + }; +}; diff --git a/modules/input/public/components.hpp b/modules/input/public/components.hpp new file mode 100644 index 0000000..fb4d94b --- /dev/null +++ b/modules/input/public/components.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include + +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 m_actions; +}; + +} // namespace lt::input diff --git a/modules/input/public/events.hpp b/modules/input/public/events.hpp new file mode 100644 index 0000000..102be84 --- /dev/null +++ b/modules/input/public/events.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include + +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 diff --git a/modules/input/public/input.hpp b/modules/input/public/input.hpp deleted file mode 100644 index d612f19..0000000 --- a/modules/input/public/input.hpp +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include -#include - -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 m_keyboad_keys {}; - - std::array 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 diff --git a/modules/input/public/system.hpp b/modules/input/public/system.hpp index 829193a..fa5e5ed 100644 --- a/modules/input/public/system.hpp +++ b/modules/input/public/system.hpp @@ -1,63 +1,46 @@ #pragma once -#include +#include +#include +#include +#include +#include namespace lt::input { -template -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(event)); - }); - }; + System(Ref 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 m_registry; std::array m_keys {}; + + std::array m_buttons {}; + + math::vec2 m_pointer_position; };