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..501f530f 100644 --- a/include/public/Events/Event.hpp +++ b/include/public/Events/Event.hpp @@ -20,43 +20,53 @@ 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) \ - static EventType GetStaticType() \ - { \ - return EventType::type;\ - }\ - virtual EventType GetEventType() const override \ - {\ - return GetStaticType();\ - }\ - virtual const char* GetName() const override \ - {\ - return #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; } \ + virtual EventType GetEventType() const override { \ + return GetStaticType(); \ + } \ + virtual const char* GetName() const override { return #type; } + +#define EVENT_CLASS_CATEGORY(category) \ + virtual int GetCategoryFlags() const override { \ + return static_cast(category); \ } - #define EVENT_CLASS_CATEGORY(category)\ - virtual int GetCategoryFlags() const override \ - { return static_cast(category); } - /// Base for all engine events; carries type/category identity and the /// Handled flag consumers can set to stop further propagation. /// @ingroup events 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..a2868106 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,101 +92,129 @@ 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; + } + } } - } } void Window::SetFullScreen(bool isFullscreen) { 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