From d8b9e01102e37b8506738e11fe22a067fd902b27 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 4 Oct 2026 20:59:53 +0300 Subject: [PATCH 1/3] feat: add keyboard, mouse, gamepad and action polling Window resets and feeds a per-frame input state from its SDL events, with SDL3 gamepad hot plug and a named action map on top. FirstPersonController and FreeLookCameraController read it instead of calling SDL. Focus, window move and text input events are dispatched now, and the two empty headers are gone. --- CMakeLists.txt | 5 + docs/pages/events_and_input.md | 79 ++- include/private/Input/InputBackend.hpp | 24 + .../ECS/Components/FirstPersonController.hpp | 85 +-- .../Components/FreeLookCameraController.hpp | 69 +-- include/public/Events/ApplicationEvent.hpp | 33 ++ include/public/Events/Event.hpp | 56 +- include/public/Events/EventManager.hpp | 0 include/public/Events/GamepadEvent.hpp | 41 ++ include/public/Events/InputEvent.hpp | 3 +- include/public/Events/KeyboardEvent.hpp | 26 +- include/public/Input/Gamepad.hpp | 102 ++++ include/public/Input/InputAction.hpp | 72 ++- include/public/Input/InputConfig.hpp | 0 include/public/Input/InputEvent.hpp | 2 + include/public/Input/InputManager.hpp | 60 +- include/public/Input/Keyboard.hpp | 57 +- include/public/Input/Mouse.hpp | 47 +- src/Core/Window.cpp | 245 ++++---- src/Input/Input.cpp | 524 ++++++++++++++++++ src/Scene/FirstPersonController.cpp | 83 +-- .../FreeLookCameraControllerComponent.cpp | 78 ++- 22 files changed, 1279 insertions(+), 412 deletions(-) create mode 100644 include/private/Input/InputBackend.hpp delete mode 100644 include/public/Events/EventManager.hpp create mode 100644 include/public/Events/GamepadEvent.hpp create mode 100644 include/public/Input/Gamepad.hpp delete mode 100644 include/public/Input/InputConfig.hpp create mode 100644 src/Input/Input.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2212728b..f3ef14a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -177,6 +177,10 @@ set(GRAPHICS_DIRECTX12_SOURCES src/Graphics/DirectX12/DirectX12Texture.cpp ) +set(INPUT_SOURCES + src/Input/Input.cpp +) + set(LIGHTING_SOURCES src/Lighting/AreaLight.cpp src/Lighting/DirectionalLight.cpp @@ -225,6 +229,7 @@ set(ENGINE_SOURCES ${GRAPHICS_COMMON_SOURCES} ${GRAPHICS_OPENGL_SOURCES} ${GRAPHICS_VULKAN_SOURCES} + ${INPUT_SOURCES} ${LIGHTING_SOURCES} ${MATH_SOURCES} ${PHYSICS_SOURCES} diff --git a/docs/pages/events_and_input.md b/docs/pages/events_and_input.md index 0050ebbe..25497cbe 100644 --- a/docs/pages/events_and_input.md +++ b/docs/pages/events_and_input.md @@ -71,16 +71,18 @@ as used throughout `src/Scene/FirstPersonController.cpp`. public `Handled` flag. Real subclasses: - `Sleak::Events` (`Events/ApplicationEvent.hpp`): `WindowResizeEvent`, - `WindowOpenEvent`, `WindowFullScreen`, `WindowCloseEvent`, `TickEvent`, - `UpdateEvent`, `RenderEvent`. + `WindowOpenEvent`, `WindowFullScreen`, `WindowFocusEvent`, + `WindowLostFocusEvent`, `WindowMovedEvent`, `WindowCloseEvent`, + `TickEvent`, `UpdateEvent`, `RenderEvent`. - `Sleak::Events::Input` (`Events/KeyboardEvent.hpp`): `KeyEvent` (base), - `KeyPressedEvent` (has `IsRepeat()`), `KeyReleasedEvent`, `KeyTypedEvent`. + `KeyPressedEvent` (has `IsRepeat()`), `KeyReleasedEvent`, `KeyTypedEvent` + (UTF-8 in `GetText()`, sent while SDL text input is active). - `Sleak::Events::Input` (`Events/MouseEvent.hpp`): `MouseMovedEvent`, `MouseScrolledEvent`, `MouseButtonEvent` (base), `MouseButtonPressedEvent`, `MouseButtonReleasedEvent`. - -The `EventType` enum also lists `WindowFocus`, `WindowLostFocus`, and -`WindowMoved`, but no corresponding `Event` subclasses exist for them yet. +- `Sleak::Events::Input` (`Events/GamepadEvent.hpp`): + `GamepadConnectedEvent`, `GamepadDisconnectedEvent`, both carrying the + `Input::Gamepad` index. Underneath `EventDispatcher`, `Sleak::Delegate` (`include/public/Events/Delegate.hpp`) wraps a `std::function`. @@ -101,30 +103,47 @@ Two macros in `Events/KeyboardEvent.hpp` test a key inside an event handler: ## 2. Input -There is no working hardware-state polling API in the engine. -`Sleak::Input::InputManager` (`include/public/Input/InputManager.hpp`) only -declares `RegisterListener` / `UnregisterListener` for an -`InputEventListener`; it has no key or mouse query methods, no `.cpp` -implementation, and is referenced only by the equally unused -`InputAction.hpp`. `Sleak::Input::Keyboard` -(`include/public/Input/Keyboard.hpp`) declares -`IsKeyPressed` / `IsKeyHold` / `IsKeyReleased(KEY_CODE)`, but also has no -implementation and no call sites anywhere in the engine. -`include/public/Input/Mouse.hpp` is not a class at all; it contains only a -commented-out code snippet. - -In practice, held-key state is tracked ad hoc per component by registering -`OnKeyPressed` / `OnKeyReleased` handlers through `EventDispatcher` and -flipping local booleans, as `FirstPersonController` does. Key and mouse -codes are `Sleak::Input::KEY_CODE` and `Sleak::Input::MOUSE_CODE` -(`include/public/Input/KeyCodes.hpp`). `KEY_CODE` values line up with SDL -scancodes, and `MOUSE_CODE::ButtonLeft` equals SDL's `Button1` / -`SDL_BUTTON_LEFT`. - -Cursor capture has no engine-level API either; controllers toggle it -directly with SDL calls, e.g. -`FirstPersonController::ToggleCursor()` calling `SDL_ShowCursor()` / -`SDL_HideCursor()`. +Next to the events there is a per-frame polling API in +`include/public/Input/`. `Window::Update()` resets it at the start of every +frame and then folds each SDL event into it, so anything that runs in the +scene update sees this frame's input. Game code never needs SDL for it. +Everything is static and main-thread only. + +- `Input::Keyboard`: `IsKeyPressed` / `IsKeyHold` / `IsKeyReleased(KEY_CODE)`. + Pressed and released are edges for this frame and survive a tap that goes + down and up between two frames. OS key repeat is ignored. +- `Input::Mouse`: the same three for `MOUSE_CODE`, plus `GetPosition()`, + `GetDelta()` (motion since last frame, also valid in relative mode) and + `GetWheel()`. `SetCursorVisible()` shows or hides the cursor. +- `Input::Gamepad`: up to `MaxGamepads` (4) controllers through SDL3's + gamepad API, with hot plug. Buttons use `GAMEPAD_BUTTON`, axes use + `GAMEPAD_AXIS`. `GetAxis()` applies a per-axis deadzone (0.15 by default, + `SetDeadzone()`), `GetRawAxis()` does not. +- `Input::InputManager`: named actions. `AddAction("Jump")` returns an + `InputAction` you bind keys, mouse buttons, gamepad buttons and gamepad + axes (with a threshold) to, then query with `IsActionPressed` / + `IsActionHold` / `IsActionReleased` / `GetActionValue`. Pressed and + released are for the action as a whole across all of its bindings. + `RegisterListener` / `UnregisterListener` still exist for raw + `InputEventListener` callbacks. + +```cpp +using namespace Sleak::Input; +InputManager::AddAction("Jump") + .BindKey(KEY_CODE::KEY__SPACE) + .BindGamepadButton(GAMEPAD_BUTTON::SOUTH); + +if (InputManager::IsActionPressed("Jump")) Jump(); +Math::Vector2D look = Mouse::GetDelta(); +``` + +When the window loses focus every held key and mouse button is released, +so nothing stays stuck down. `FirstPersonController` and +`FreeLookCameraController` read their movement keys and mouse delta from +this API. Key and mouse codes are `Sleak::Input::KEY_CODE` and +`Sleak::Input::MOUSE_CODE` (`include/public/Input/KeyCodes.hpp`). +`KEY_CODE` values line up with SDL scancodes, and `MOUSE_CODE::ButtonLeft` +equals SDL's `Button1` / `SDL_BUTTON_LEFT`. --- diff --git a/include/private/Input/InputBackend.hpp b/include/private/Input/InputBackend.hpp new file mode 100644 index 00000000..a57e61ab --- /dev/null +++ b/include/private/Input/InputBackend.hpp @@ -0,0 +1,24 @@ +#ifndef _INPUT_BACKEND_HPP_ +#define _INPUT_BACKEND_HPP_ + +#include + +namespace Sleak { +namespace Input { +namespace Backend { + +/// Clears last frame's edges and deltas; call once before pumping events. +void BeginFrame(); +/// Folds one SDL event into the polling state and raw listeners. +/// Returns the gamepad index for a connect/disconnect event, else -1. +int ProcessEvent(const SDL_Event& event); +/// Releases every key and button, e.g. when the window loses focus. +void ReleaseAll(); +/// Closes all open gamepads; call before SDL_Quit. +void Shutdown(); + +} // namespace Backend +} // namespace Input +} // namespace Sleak + +#endif diff --git a/include/public/ECS/Components/FirstPersonController.hpp b/include/public/ECS/Components/FirstPersonController.hpp index 71b1b140..f9046402 100644 --- a/include/public/ECS/Components/FirstPersonController.hpp +++ b/include/public/ECS/Components/FirstPersonController.hpp @@ -101,52 +101,53 @@ namespace Sleak { void SetPitch(float pitch) { m_pitch = pitch; } void SetYaw(float yaw) { m_yaw = yaw; } - /// Updates translation/sprint input state from a key-down event. + /// No longer used: movement keys are polled in Update(). Kept so + /// existing callers still compile. void OnKeyPressed(const Events::Input::KeyPressedEvent& e); - /// Clears translation/sprint input state from a key-up event. + /// No longer used: movement keys are polled in Update(). void OnKeyReleased(const Events::Input::KeyReleasedEvent& e); private: - /// Reads mouse delta into yaw/pitch, clamped to m_pitchRange. - void UpdateInput(float deltaTime) override; - /// Applies UE-style acceleration/braking to velocity, resolves jump/fly, and moves the camera. - void UpdateCamera(float deltaTime) override; - - // UE-style movement parameters — tuned slower for a calmer pace - float m_maxWalkSpeed = 2.5f; // Casual walking pace - float m_sprintMultiplier = 2.0f; // Sprint speed = walk * multiplier - bool m_sprinting = false; - float m_maxAcceleration = 10.0f; // Gentle ramp-up (~0.25s to full speed) - float m_brakingDeceleration = 12.0f; // Smooth stop - float m_groundFriction = 8.0f; // UE default - float m_airControl = 0.05f; // Minimal air control - - // Jump — gentle hop, ~0.45m height - // v = sqrt(2 * 9.81 * 0.45) ≈ 2.97 - float m_jumpZVelocity = 3.0f; - - // Fly state - bool m_flying = false; - float m_maxFlySpeed = 10.0f; - float m_flySprintMultiplier = 2.5f; - float m_flyVerticalInput = 0.0f; - float m_flyVerticalVelocity = 0.0f; - - // Mouse look - float m_pitch = 0.0f; - float m_yaw = 0.0f; - Math::Vector2D m_pitchRange = Math::Vector2D(-89, 89); - - // Horizontal velocity (managed by controller, separate from rigidbody Y axis) - Math::Vector3D m_velocity; - - // Input state - bool m_firstFrame = true; - - RigidbodyComponent* m_rigidbody = nullptr; - - std::string m_keyPressedHandlerId; - std::string m_keyReleasedHandlerId; + /// Polls movement keys and reads mouse delta into yaw/pitch, clamped to + /// m_pitchRange. + void UpdateInput(float deltaTime) override; + /// Applies UE-style acceleration/braking to velocity, resolves jump/fly, + /// and moves the camera. + void UpdateCamera(float deltaTime) override; + + // UE-style movement parameters — tuned slower for a calmer pace + float m_maxWalkSpeed = 2.5f; // Casual walking pace + float m_sprintMultiplier = 2.0f; // Sprint speed = walk * multiplier + bool m_sprinting = false; + float m_maxAcceleration = 10.0f; // Gentle ramp-up (~0.25s to full speed) + float m_brakingDeceleration = 12.0f; // Smooth stop + float m_groundFriction = 8.0f; // UE default + float m_airControl = 0.05f; // Minimal air control + + // Jump — gentle hop, ~0.45m height + // v = sqrt(2 * 9.81 * 0.45) ≈ 2.97 + float m_jumpZVelocity = 3.0f; + + // Fly state + bool m_flying = false; + float m_maxFlySpeed = 10.0f; + float m_flySprintMultiplier = 2.5f; + float m_flyVerticalInput = 0.0f; + float m_flyVerticalVelocity = 0.0f; + + // Mouse look + float m_pitch = 0.0f; + float m_yaw = 0.0f; + Math::Vector2D m_pitchRange = Math::Vector2D(-89, 89); + + // Horizontal velocity (managed by controller, separate from rigidbody Y + // axis) + Math::Vector3D m_velocity; + + // Input state + bool m_firstFrame = true; + + RigidbodyComponent* m_rigidbody = nullptr; }; } // namespace Sleak diff --git a/include/public/ECS/Components/FreeLookCameraController.hpp b/include/public/ECS/Components/FreeLookCameraController.hpp index 36e68ad0..54b9b0a8 100644 --- a/include/public/ECS/Components/FreeLookCameraController.hpp +++ b/include/public/ECS/Components/FreeLookCameraController.hpp @@ -52,44 +52,45 @@ namespace Sleak { void SetRoll(float roll) { this->roll = roll; } float GetRoll() const { return roll; } - - /// Updates translation input state from a key-down event; doubles speed while LCTRL is held. + /// No longer used: movement keys are polled in Update(). Kept so + /// existing overrides still compile. virtual void OnKeyPressed(const Sleak::Events::Input::KeyPressedEvent& e); - /// Clears translation input state from a key-up event. + /// No longer used: movement keys are polled in Update(). virtual void OnKeyReleased(const Sleak::Events::Input::KeyReleasedEvent& e); private: - /// Reads and smooths mouse delta into yaw/pitch, clamped to PitchRange. - void UpdateInput(float deltaTime) override; - /// Accelerates toward the input-driven target velocity, applies damping, and moves the camera. - void UpdateCamera(float deltaTime) override; - - /// Exponentially decays velocity toward zero when there's no translation input. - void ApplyDamping(float DeltaTime); - /// Accelerates velocity toward the input-driven target velocity. - void ApplyAcceleration(float DeltaTime); - /// Clamps velocity magnitude to maxSpeed. - void ClampVelocity(); - - Math::Vector3D velocity; - float speed = 1.0f; - float sensitivity = 0.1f; - float acceleration; - float damping; - float maxSpeed; - - bool isInvertY = false; - float pitch = 0.0f; - float yaw = 0.0f; - float roll = 0.0f; - Math::Vector2D PitchRange; - Math::Vector2D YawRange; - Math::Vector2D RollRange; - - bool m_firstFrame = true; - - std::string m_keyPressedHandlerId; - std::string m_keyReleasedHandlerId; + /// Polls movement keys, then reads and smooths mouse delta into yaw/pitch, + /// clamped to PitchRange. + void UpdateInput(float deltaTime) override; + /// Accelerates toward the input-driven target velocity, applies damping, + /// and moves the camera. + void UpdateCamera(float deltaTime) override; + + /// Exponentially decays velocity toward zero when there's no translation + /// input. + void ApplyDamping(float DeltaTime); + /// Accelerates velocity toward the input-driven target velocity. + void ApplyAcceleration(float DeltaTime); + /// Clamps velocity magnitude to maxSpeed. + void ClampVelocity(); + + Math::Vector3D velocity; + float speed = 1.0f; + float sensitivity = 0.1f; + float acceleration; + float damping; + float maxSpeed; + + bool isInvertY = false; + float pitch = 0.0f; + float yaw = 0.0f; + float roll = 0.0f; + Math::Vector2D PitchRange; + Math::Vector2D YawRange; + Math::Vector2D RollRange; + + bool m_firstFrame = true; + bool m_boost = false; }; } diff --git a/include/public/Events/ApplicationEvent.hpp b/include/public/Events/ApplicationEvent.hpp index f2b9d4de..620353b7 100644 --- a/include/public/Events/ApplicationEvent.hpp +++ b/include/public/Events/ApplicationEvent.hpp @@ -53,6 +53,39 @@ namespace Sleak { bool bIsFullScreen; }; + /// Fired when the window gains keyboard focus. + /// @ingroup events + class ENGINE_API WindowFocusEvent : public Event { + public: + WindowFocusEvent() = default; + EVENT_CLASS_TYPE(WindowFocus) + EVENT_CLASS_CATEGORY(EventCategory::Application) + }; + + /// Fired when the window loses keyboard focus. Held keys and buttons + /// are released in the polling state at the same time. + /// @ingroup events + class ENGINE_API WindowLostFocusEvent : public Event { + public: + WindowLostFocusEvent() = default; + EVENT_CLASS_TYPE(WindowLostFocus) + EVENT_CLASS_CATEGORY(EventCategory::Application) + }; + + /// Fired when the window moves, carrying its new top-left position on + /// the desktop. + /// @ingroup events + class ENGINE_API WindowMovedEvent : public Event { + public: + WindowMovedEvent(int x, int y) : x(x), y(y) {} + int GetX() const { return x; } + int GetY() const { return y; } + EVENT_CLASS_TYPE(WindowMoved) + EVENT_CLASS_CATEGORY(EventCategory::Application) + private: + int x, y; + }; + /// Fired when the window is about to close. /// @ingroup events class ENGINE_API WindowCloseEvent : public Event { diff --git a/include/public/Events/Event.hpp b/include/public/Events/Event.hpp index 68daaf55..0a89f2ca 100644 --- a/include/public/Events/Event.hpp +++ b/include/public/Events/Event.hpp @@ -20,26 +20,42 @@ namespace Sleak { /// Concrete kind of Event; each Event subclass reports one of these via GetEventType(). /// @ingroup events - enum class EventType { - Unknown = 0, - WindowOpen, WindowClose, WindowResize, WindowFullscreen, WindowFocus, WindowLostFocus, WindowMoved, - Tick, Update, Render, - KeyPressed, KeyReleased, KeyTyped, - MousePressed, MouseReleased, MouseMoved, MouseScrolled - }; - - /// Bitmask groups an Event can belong to, queried via Event::IsInCategory(). - /// @ingroup events - enum class EventCategory { - None = (1 << 0), - Application = (1 << 1), - Input = (1 << 2), - Keyboard = (1 << 3), - Mouse = (1 << 4), - MouseButton = (1 << 5) - }; - - #define EVENT_CLASS_TYPE(type) \ +enum class EventType { + Unknown = 0, + WindowOpen, + WindowClose, + WindowResize, + WindowFullscreen, + WindowFocus, + WindowLostFocus, + WindowMoved, + Tick, + Update, + Render, + KeyPressed, + KeyReleased, + KeyTyped, + MousePressed, + MouseReleased, + MouseMoved, + MouseScrolled, + GamepadConnected, + GamepadDisconnected +}; + +/// Bitmask groups an Event can belong to, queried via Event::IsInCategory(). +/// @ingroup events +enum class EventCategory { + None = (1 << 0), + Application = (1 << 1), + Input = (1 << 2), + Keyboard = (1 << 3), + Mouse = (1 << 4), + MouseButton = (1 << 5), + Gamepad = (1 << 6) +}; + +#define EVENT_CLASS_TYPE(type) \ static EventType GetStaticType() \ { \ return EventType::type;\ diff --git a/include/public/Events/EventManager.hpp b/include/public/Events/EventManager.hpp deleted file mode 100644 index e69de29b..00000000 diff --git a/include/public/Events/GamepadEvent.hpp b/include/public/Events/GamepadEvent.hpp new file mode 100644 index 00000000..b5a0c7d5 --- /dev/null +++ b/include/public/Events/GamepadEvent.hpp @@ -0,0 +1,41 @@ +#ifndef _GAMEPADEVENT_H_ +#define _GAMEPADEVENT_H_ + +#include + +namespace Sleak { +namespace Events { +namespace Input { + +/// Fired when a controller is connected and has been given a Gamepad index. +/// @ingroup events +class ENGINE_API GamepadConnectedEvent : public Event { + public: + explicit GamepadConnectedEvent(int index) : m_Index(index) {} + /// Index to pass to the Input::Gamepad queries. + int GetIndex() const { return m_Index; } + EVENT_CLASS_TYPE(GamepadConnected) + EVENT_CLASS_CATEGORY(EventCategory::Gamepad) + + private: + int m_Index; +}; + +/// Fired after a controller is disconnected; its index is free again. +/// @ingroup events +class ENGINE_API GamepadDisconnectedEvent : public Event { + public: + explicit GamepadDisconnectedEvent(int index) : m_Index(index) {} + int GetIndex() const { return m_Index; } + EVENT_CLASS_TYPE(GamepadDisconnected) + EVENT_CLASS_CATEGORY(EventCategory::Gamepad) + + private: + int m_Index; +}; + +} // namespace Input +} // namespace Events +} // namespace Sleak + +#endif diff --git a/include/public/Events/InputEvent.hpp b/include/public/Events/InputEvent.hpp index 0266cfff..87e661b7 100644 --- a/include/public/Events/InputEvent.hpp +++ b/include/public/Events/InputEvent.hpp @@ -1,2 +1,3 @@ +#include #include -#include \ No newline at end of file +#include diff --git a/include/public/Events/KeyboardEvent.hpp b/include/public/Events/KeyboardEvent.hpp index ced4d197..7447cd17 100644 --- a/include/public/Events/KeyboardEvent.hpp +++ b/include/public/Events/KeyboardEvent.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #define if_key_press(key) if (!e.IsRepeat() && e.GetKeyCode() == Sleak::Input::KEY_CODE::key) #define if_key_down(key) if (e.GetKeyCode() == Sleak::Input::KEY_CODE::key) @@ -67,23 +68,38 @@ namespace Sleak { EVENT_CLASS_TYPE(KeyReleased) }; - - /// Fired for text input, after IME/layout composition rather than raw key-down. + + /// Fired for text input, after IME/layout composition rather than + /// raw key-down. The window sends it while SDL text input is active + /// (ImGui turns that on for its text fields), with the composed + /// UTF-8 in GetText(). /// @ingroup events class ENGINE_API KeyTypedEvent : public KeyEvent { public: KeyTypedEvent(const KeyCode keycode) : KeyEvent(keycode) {} - + + explicit KeyTypedEvent(std::string text) + : KeyEvent(KeyCode::KEY__UNKNOWN), + m_Text(std::move(text)) {} + + /// UTF-8 text produced by the keystroke; empty for the key code + /// form. + const std::string& GetText() const { return m_Text; } + std::string ToString() const override { std::stringstream ss; - ss << "KeyTypedEvent: " << GetKeyStr(); + ss << "KeyTypedEvent: " + << (m_Text.empty() ? GetKeyStr() : m_Text); return ss.str(); } - + EVENT_CLASS_TYPE(KeyTyped) + + private: + std::string m_Text; }; } } diff --git a/include/public/Input/Gamepad.hpp b/include/public/Input/Gamepad.hpp new file mode 100644 index 00000000..3d97ca4f --- /dev/null +++ b/include/public/Input/Gamepad.hpp @@ -0,0 +1,102 @@ +#ifndef _GAMEPAD_HPP_ +#define _GAMEPAD_HPP_ + +#include +#include + +namespace Sleak { +namespace Input { + +/// Gamepad buttons in a standard layout, numbered to match SDL3's +/// SDL_GamepadButton. Face buttons are positional, so SOUTH is Xbox A and +/// PlayStation Cross. +/// @ingroup input +enum class GAMEPAD_BUTTON { + SOUTH = 0, + EAST, + WEST, + NORTH, + BACK, + GUIDE, + START, + LEFT_STICK, + RIGHT_STICK, + LEFT_SHOULDER, + RIGHT_SHOULDER, + DPAD_UP, + DPAD_DOWN, + DPAD_LEFT, + DPAD_RIGHT, + MISC1, + RIGHT_PADDLE1, + LEFT_PADDLE1, + RIGHT_PADDLE2, + LEFT_PADDLE2, + TOUCHPAD, + MISC2, + MISC3, + MISC4, + MISC5, + MISC6, + COUNT +}; + +/// Gamepad axes, numbered to match SDL3's SDL_GamepadAxis. Sticks report +/// -1..1 with +y pointing down, triggers report 0..1. +/// @ingroup input +enum class GAMEPAD_AXIS { + LEFT_X = 0, + LEFT_Y, + RIGHT_X, + RIGHT_Y, + LEFT_TRIGGER, + RIGHT_TRIGGER, + COUNT +}; + +/// Per-frame state for up to MaxGamepads connected controllers, filled from +/// SDL3's gamepad API. Hot plugging is handled: a controller connected +/// mid-session takes the lowest free index, and a disconnected one reads +/// as released with zeroed axes. Main thread only. +/// +/// @code{.cpp} +/// using namespace Sleak::Input; +/// if (Gamepad::IsConnected()) { +/// float strafe = Gamepad::GetAxis(GAMEPAD_AXIS::LEFT_X); +/// if (Gamepad::IsButtonPressed(GAMEPAD_BUTTON::SOUTH)) Jump(); +/// } +/// @endcode +/// @see Keyboard, Mouse, InputManager +/// @ingroup input +class ENGINE_API Gamepad { + public: + static constexpr int MaxGamepads = 4; + + static bool IsConnected(int index = 0); + /// Number of controllers currently connected. + static int GetConnectedCount(); + /// Controller name as reported by SDL, or an empty string. + static std::string GetName(int index = 0); + + /// True on the frame button transitions from up to down. + static bool IsButtonPressed(GAMEPAD_BUTTON button, int index = 0); + /// True while button is held down, including the initial press frame. + static bool IsButtonHold(GAMEPAD_BUTTON button, int index = 0); + /// True on the frame button transitions from down to up. + static bool IsButtonReleased(GAMEPAD_BUTTON button, int index = 0); + + /// Axis value with the deadzone removed and the rest rescaled to full + /// range. + static float GetAxis(GAMEPAD_AXIS axis, int index = 0); + /// Axis value straight from the device, no deadzone. + static float GetRawAxis(GAMEPAD_AXIS axis, int index = 0); + + /// Sets the per-axis deadzone, clamped to [0, 0.95]. Defaults to 0.15. + static void SetDeadzone(float deadzone); + static float GetDeadzone(); +}; + +} // namespace Input +} // namespace Sleak + +#endif diff --git a/include/public/Input/InputAction.hpp b/include/public/Input/InputAction.hpp index 4d619ebe..74355fa8 100644 --- a/include/public/Input/InputAction.hpp +++ b/include/public/Input/InputAction.hpp @@ -1,29 +1,55 @@ #ifndef _INPUTACTION_HPP_ #define _INPUTACTION_HPP_ -#include +#include +#include +#include #include -#include +#include +#include namespace Sleak { - namespace Input { - /// A named input binding across keys, mouse buttons, and gamepad - /// buttons/axes, queried as a single logical action. - /// @ingroup input - class InputAction { - public: - std::string name; - std::vector keyMappings; - std::vector mouseButtonMappings; - std::vector gamepadButtonMappings; - std::vector> gamepadAxisMappings; - - InputAction(const std::string& name) : name(name) {} - - /// True if any bound key, button, or axis is currently active in inputManager. - bool isPressed(const InputManager& inputManager) const; - }; - } -} - -#endif \ No newline at end of file +namespace Input { + +/// A named action bound to any number of keys, mouse buttons, gamepad +/// buttons and gamepad axes. It is down while any binding is down, and the +/// pressed/released edges are for the action as a whole, so holding W and +/// then also pressing Up does not fire a second press. +/// +/// An axis binding is a direction and a threshold: a positive threshold +/// is active when the axis is at or above it, a negative one when it is at +/// or below it. Gamepad bindings match any connected controller. +/// @see InputManager +/// @ingroup input +class ENGINE_API InputAction { + public: + explicit InputAction(std::string name) : name(std::move(name)) {} + + std::string name; + std::vector keys; + std::vector mouseButtons; + std::vector gamepadButtons; + std::vector> gamepadAxes; + + InputAction& BindKey(KEY_CODE key); + InputAction& BindMouseButton(MOUSE_CODE button); + InputAction& BindGamepadButton(GAMEPAD_BUTTON button); + InputAction& BindGamepadAxis(GAMEPAD_AXIS axis, float threshold = 0.5f); + /// Removes every binding but keeps the action registered. + void ClearBindings(); + + /// True on the frame the action goes from inactive to active. + bool IsPressed() const; + /// True while any binding is active. + bool IsHold() const; + /// True on the frame the action goes from active to inactive. + bool IsReleased() const; + /// Strongest binding in [0, 1]: 1 for a held button, the magnitude for an + /// axis past its threshold. + float GetValue() const; +}; + +} // namespace Input +} // namespace Sleak + +#endif diff --git a/include/public/Input/InputConfig.hpp b/include/public/Input/InputConfig.hpp deleted file mode 100644 index e69de29b..00000000 diff --git a/include/public/Input/InputEvent.hpp b/include/public/Input/InputEvent.hpp index 42fb9a5a..c1e5a4d4 100644 --- a/include/public/Input/InputEvent.hpp +++ b/include/public/Input/InputEvent.hpp @@ -1,6 +1,8 @@ #ifndef _INPUTEVENTS_HPP_ #define _INPUTEVENTS_HPP_ +#include + namespace Sleak { namespace Input { diff --git a/include/public/Input/InputManager.hpp b/include/public/Input/InputManager.hpp index 217ee95f..590c2255 100644 --- a/include/public/Input/InputManager.hpp +++ b/include/public/Input/InputManager.hpp @@ -1,22 +1,52 @@ #ifndef _INPUTMANAGER_HPP_ #define _INPUTMANAGER_HPP_ -#include -#include +#include +#include +#include +#include namespace Sleak { - namespace Input { - /// Static registry of InputEventListeners that raw input gets fanned out to. - /// @ingroup input - class InputManager { - public: +namespace Input { - static void RegisterListener(InputEventListener* listener); - static void UnregisterListener(InputEventListener* listener); - private: - static td::vector EventListeners; - }; - } -} +/// Owns the named action map and the raw input listeners. Keyboard, Mouse +/// and Gamepad answer device questions; this answers "is Jump pressed". +/// Main thread only. +/// +/// @code{.cpp} +/// using namespace Sleak::Input; +/// InputManager::AddAction("Jump") +/// .BindKey(KEY_CODE::KEY__SPACE) +/// .BindGamepadButton(GAMEPAD_BUTTON::SOUTH); +/// +/// if (InputManager::IsActionPressed("Jump")) Jump(); +/// @endcode +/// @see InputAction, Keyboard, Mouse, Gamepad +/// @ingroup input +class ENGINE_API InputManager { + public: + /// Creates the action, or returns the existing one with that name. + /// The reference stays valid until the action is removed. + static InputAction& AddAction(std::string_view name); + /// Returns nullptr if no action has that name. + static InputAction* GetAction(std::string_view name); + static void RemoveAction(std::string_view name); + static void ClearActions(); -#endif \ No newline at end of file + /// False for an unknown action. + static bool IsActionPressed(std::string_view name); + static bool IsActionHold(std::string_view name); + static bool IsActionReleased(std::string_view name); + /// 0 for an unknown action. + static float GetActionValue(std::string_view name); + + /// Adds a listener that receives every raw key, mouse and gamepad event. + static void RegisterListener(InputEventListener* listener); + /// Safe to call from inside a listener callback. + static void UnregisterListener(InputEventListener* listener); +}; + +} // namespace Input +} // namespace Sleak + +#endif diff --git a/include/public/Input/Keyboard.hpp b/include/public/Input/Keyboard.hpp index d1cb61fe..4d383d4d 100644 --- a/include/public/Input/Keyboard.hpp +++ b/include/public/Input/Keyboard.hpp @@ -1,35 +1,36 @@ #ifndef _KEYBOARD_HPP_ #define _KEYBOARD_HPP_ -#include -#include - +#include #include namespace Sleak { - namespace Input { -/// Static keyboard state queried by key code; Window feeds it raw key events. +namespace Input { + +/// Per-frame keyboard state, filled by the window from its event queue at +/// the start of every frame. Main thread only. +/// +/// "Pressed" and "Released" are edges for the current frame and survive a +/// tap that goes down and up between two frames. OS key repeat is ignored. +/// +/// @code{.cpp} +/// if (Sleak::Input::Keyboard::IsKeyPressed(KeyCode::KEY__E)) OpenDoor(); +/// float forward = Sleak::Input::Keyboard::IsKeyHold(KeyCode::KEY__W) ? 1.0f : +/// 0.0f; +/// @endcode +/// @see Mouse, Gamepad, InputManager /// @ingroup input -class Keyboard { - friend class Window; - public: - - Keyboard() = default; - ~Keyboard() = default; - - bool Initialize(); - void Shutdown(); - - // Queries key states - /// True on the frame key transitions from up to down. - static bool IsKeyPressed(KEY_CODE key); - /// True while key is held down, including the initial press frame. - static bool IsKeyHold(KEY_CODE key); - /// True on the frame key transitions from down to up. - static bool IsKeyReleased(KEY_CODE key); - - }; -} -} - -#endif \ No newline at end of file +class ENGINE_API Keyboard { + public: + /// True on the frame key transitions from up to down. + static bool IsKeyPressed(KEY_CODE key); + /// True while key is held down, including the initial press frame. + static bool IsKeyHold(KEY_CODE key); + /// True on the frame key transitions from down to up. + static bool IsKeyReleased(KEY_CODE key); +}; + +} // namespace Input +} // namespace Sleak + +#endif diff --git a/include/public/Input/Mouse.hpp b/include/public/Input/Mouse.hpp index 44a9b7d3..fb49d07e 100644 --- a/include/public/Input/Mouse.hpp +++ b/include/public/Input/Mouse.hpp @@ -1,12 +1,41 @@ -/* --------- Unprojecting mouse coordinate (NDC) to world space -float ndcX = (2.0f * mouseX) / screenWidth - 1.0f; -float ndcY = 1.0f - (2.0f * mouseY) / screenHeight; -XMMATRIX viewProjectionMatrix = XMMatrixMultiply(viewMatrix, projectionMatrix); +#ifndef _MOUSE_HPP_ +#define _MOUSE_HPP_ -XMVECTOR mouseNDC = XMVectorSet(ndcX, ndcY, 0.0 , 1.0); // Z value stands for Near/Far plane +#include +#include +#include -XMMATRIX inverseViewProjection = XMMatrixInverse(nullptr, viewProjectionMatrix); +namespace Sleak { +namespace Input { -XMVECTOR worldNear = XMVector3TransformCoord(mouseNear, inverseViewProjection); -*/ \ No newline at end of file +/// Per-frame mouse state: buttons, cursor position, and the motion and +/// wheel accumulated since the previous frame. Main thread only. +/// +/// GetDelta() keeps working in relative mouse mode, where the position no +/// longer moves, so use it for camera look. +/// @see Keyboard, Gamepad, InputManager +/// @ingroup input +class ENGINE_API Mouse { + public: + /// True on the frame button transitions from up to down. + static bool IsButtonPressed(MOUSE_CODE button); + /// True while button is held down, including the initial press frame. + static bool IsButtonHold(MOUSE_CODE button); + /// True on the frame button transitions from down to up. + static bool IsButtonReleased(MOUSE_CODE button); + + /// Cursor position in window pixels. + static Math::Vector2D GetPosition(); + /// Motion since the previous frame, in pixels. + static Math::Vector2D GetDelta(); + /// Wheel movement since the previous frame; positive y scrolls away from + /// the user. + static Math::Vector2D GetWheel(); + + static void SetCursorVisible(bool visible); +}; + +} // namespace Input +} // namespace Sleak + +#endif diff --git a/src/Core/Window.cpp b/src/Core/Window.cpp index e8c18cbf..198c3401 100644 --- a/src/Core/Window.cpp +++ b/src/Core/Window.cpp @@ -1,13 +1,15 @@ #include "../../include/private/Core/Window.hpp" -#include "../../include/private/Graphics/Common/RendererFactory.hpp" -#include -#include #include #include +#include #include #include +#include +#include +#include "../../include/private/Graphics/Common/RendererFactory.hpp" +#include "../../include/private/Input/InputBackend.hpp" namespace Sleak { @@ -24,9 +26,10 @@ Window::Window(int width, int height, std::string name) } Window::~Window() { - if (SDLWindow) { - SDL_DestroyWindow(SDLWindow); - } + Input::Backend::Shutdown(); + if (SDLWindow) { + SDL_DestroyWindow(SDLWindow); + } SDL_Quit(); SLEAK_LOG("The window has been destroyed"); @@ -51,12 +54,16 @@ bool Window::InitializeWindow() { return false; } - SDLWindow = SDL_CreateWindow(WindowName.c_str(), Width, Height, GraphicsAPI | SDL_WINDOW_RESIZABLE); + if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) + SLEAK_WARN("Gamepad support unavailable: {0}", SDL_GetError()); - if (!SDLWindow) { - SLEAK_FATAL("Failed to create window! {0} ", SDL_GetError()); - return false; - } + SDLWindow = SDL_CreateWindow(WindowName.c_str(), Width, Height, + GraphicsAPI | SDL_WINDOW_RESIZABLE); + + if (!SDLWindow) { + SLEAK_FATAL("Failed to create window! {0} ", SDL_GetError()); + return false; + } if (SDL_Surface* icon = SDL_LoadBMP("assets/branding/icon.bmp")) { SDL_SetWindowIcon(SDLWindow, icon); @@ -85,100 +92,128 @@ bool Window::InitializeWindow() { } void Window::Update() { - while (SDL_PollEvent(&event)) { - - if (m_imguiReady) - ImGui_ImplSDL3_ProcessEvent(&event); - - switch (event.type) - { - case SDL_EVENT_WINDOW_RESIZED: - { - uint16_t width = event.window.data1; - uint16_t height = event.window.data2; - - Window::Width = width; - Window::Height = height; - - DispatchEvent(width,height); - - break; - } - case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: - { - DispatchEvent(true); - break; - } - case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: - { - DispatchEvent(false); - break; - } - case SDL_EVENT_QUIT: - { - DispatchEvent(); - EventDispatcher::UnregisterAllEvents(); - bShouldClose = true; - return; - } - - case SDL_EVENT_KEY_DOWN: - { - KeyCode key = static_cast(event.key.scancode); - bool isRepeat = event.key.repeat; - - DispatchEvent(key,isRepeat); - break; - } - - case SDL_EVENT_KEY_UP: - { - KeyCode key = static_cast(event.key.scancode); - - DispatchEvent(key); - break; - } - - case SDL_EVENT_MOUSE_BUTTON_DOWN: - { - MouseCode button = (MouseCode)event.button.button; - int x = event.button.x; - int y = event.button.y; - - DispatchEvent(button,x,y); - break; - } - - case SDL_EVENT_MOUSE_BUTTON_UP: - { - MouseCode button = (MouseCode)event.button.button; - int x = event.button.x; - int y = event.button.y; - - DispatchEvent(button,x,y); - break; - } - - case SDL_EVENT_MOUSE_MOTION: - { - int x = event.motion.x; - int y = event.motion.y; - int dx = event.motion.xrel; - int dy = event.motion.yrel; - - DispatchEvent(x,y); - break; - } - - case SDL_EVENT_MOUSE_WHEEL: - { - int xOffset = event.wheel.x; - int yOffset = event.wheel.y; - - DispatchEvent(xOffset, yOffset); - break; - } - } + Input::Backend::BeginFrame(); + + while (SDL_PollEvent(&event)) { + if (m_imguiReady) ImGui_ImplSDL3_ProcessEvent(&event); + + int padIndex = Input::Backend::ProcessEvent(event); + + switch (event.type) { + case SDL_EVENT_WINDOW_RESIZED: { + uint16_t width = event.window.data1; + uint16_t height = event.window.data2; + + Window::Width = width; + Window::Height = height; + + DispatchEvent(width, height); + + break; + } + case SDL_EVENT_WINDOW_FOCUS_GAINED: { + DispatchEvent(); + break; + } + case SDL_EVENT_WINDOW_FOCUS_LOST: { + DispatchEvent(); + break; + } + case SDL_EVENT_WINDOW_MOVED: { + DispatchEvent( + event.window.data1, event.window.data2); + break; + } + case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: { + DispatchEvent(true); + break; + } + case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: { + DispatchEvent(false); + break; + } + case SDL_EVENT_QUIT: { + DispatchEvent(); + EventDispatcher::UnregisterAllEvents(); + bShouldClose = true; + return; + } + + case SDL_EVENT_KEY_DOWN: { + KeyCode key = static_cast(event.key.scancode); + bool isRepeat = event.key.repeat; + + DispatchEvent(key, + isRepeat); + break; + } + + case SDL_EVENT_TEXT_INPUT: { + if (event.text.text) + DispatchEvent( + std::string(event.text.text)); + break; + } + + case SDL_EVENT_GAMEPAD_ADDED: { + if (padIndex >= 0) + DispatchEvent( + padIndex); + break; + } + + case SDL_EVENT_GAMEPAD_REMOVED: { + if (padIndex >= 0) + DispatchEvent< + Sleak::Events::Input::GamepadDisconnectedEvent>( + padIndex); + break; + } + + case SDL_EVENT_KEY_UP: { + KeyCode key = static_cast(event.key.scancode); + + DispatchEvent(key); + break; + } + + case SDL_EVENT_MOUSE_BUTTON_DOWN: { + MouseCode button = (MouseCode)event.button.button; + int x = event.button.x; + int y = event.button.y; + + DispatchEvent( + button, x, y); + break; + } + + case SDL_EVENT_MOUSE_BUTTON_UP: { + MouseCode button = (MouseCode)event.button.button; + int x = event.button.x; + int y = event.button.y; + + DispatchEvent( + button, x, y); + break; + } + + case SDL_EVENT_MOUSE_MOTION: { + int x = event.motion.x; + int y = event.motion.y; + + DispatchEvent(x, y); + break; + } + + case SDL_EVENT_MOUSE_WHEEL: { + int xOffset = event.wheel.x; + int yOffset = event.wheel.y; + + DispatchEvent( + xOffset, yOffset); + break; + } + } } } diff --git a/src/Input/Input.cpp b/src/Input/Input.cpp new file mode 100644 index 00000000..c8762c6b --- /dev/null +++ b/src/Input/Input.cpp @@ -0,0 +1,524 @@ +#include + +#include +#include