|
| 1 | +#pragma once |
| 2 | + |
| 3 | +#include <Pyramid/Core/Prerequisites.hpp> |
| 4 | +#include <Pyramid/Platform/Input.hpp> |
| 5 | + |
| 6 | +#include <cstddef> |
| 7 | +#include <memory> |
| 8 | +#include <string> |
| 9 | +#include <string_view> |
| 10 | +#include <vector> |
| 11 | + |
| 12 | +namespace Pyramid |
| 13 | +{ |
| 14 | + /** |
| 15 | + * @brief Shape of the value produced by an input action. |
| 16 | + */ |
| 17 | + enum class InputActionType : u8 |
| 18 | + { |
| 19 | + Button = 0, |
| 20 | + Axis1D, |
| 21 | + Axis2D |
| 22 | + }; |
| 23 | + |
| 24 | + /** |
| 25 | + * @brief Axis selected by a binding on a two-dimensional action. |
| 26 | + */ |
| 27 | + enum class InputAxisComponent : u8 |
| 28 | + { |
| 29 | + X = 0, |
| 30 | + Y |
| 31 | + }; |
| 32 | + |
| 33 | + /** |
| 34 | + * @brief Platform-neutral physical source read from InputState. |
| 35 | + */ |
| 36 | + enum class InputBindingSource : u8 |
| 37 | + { |
| 38 | + Key = 0, |
| 39 | + MouseButton, |
| 40 | + MouseDeltaX, |
| 41 | + MouseDeltaY, |
| 42 | + MouseWheel, |
| 43 | + MouseHorizontalWheel |
| 44 | + }; |
| 45 | + |
| 46 | + struct InputActionVector2 |
| 47 | + { |
| 48 | + f32 x = 0.0f; |
| 49 | + f32 y = 0.0f; |
| 50 | + }; |
| 51 | + |
| 52 | + /** |
| 53 | + * @brief One physical input binding for a named action. |
| 54 | + * |
| 55 | + * A binding can optionally require one keyboard key and/or one mouse |
| 56 | + * button to be held. This supports generic chords such as Alt+Enter and |
| 57 | + * gated analog input such as right-button mouse dragging without exposing |
| 58 | + * native platform codes. |
| 59 | + */ |
| 60 | + struct InputBinding |
| 61 | + { |
| 62 | + InputBindingSource source = InputBindingSource::Key; |
| 63 | + InputAxisComponent component = InputAxisComponent::X; |
| 64 | + Key key = Key::Unknown; |
| 65 | + MouseButton mouseButton = MouseButton::Count; |
| 66 | + Key requiredKey = Key::Unknown; |
| 67 | + MouseButton requiredMouseButton = MouseButton::Count; |
| 68 | + f32 scale = 1.0f; |
| 69 | + |
| 70 | + [[nodiscard]] static InputBinding KeyBinding( |
| 71 | + Key key, |
| 72 | + f32 scale = 1.0f, |
| 73 | + InputAxisComponent component = InputAxisComponent::X); |
| 74 | + [[nodiscard]] static InputBinding MouseButtonBinding( |
| 75 | + MouseButton button, |
| 76 | + f32 scale = 1.0f, |
| 77 | + InputAxisComponent component = InputAxisComponent::X); |
| 78 | + [[nodiscard]] static InputBinding MouseDeltaXBinding( |
| 79 | + f32 scale = 1.0f, |
| 80 | + InputAxisComponent component = InputAxisComponent::X); |
| 81 | + [[nodiscard]] static InputBinding MouseDeltaYBinding( |
| 82 | + f32 scale = 1.0f, |
| 83 | + InputAxisComponent component = InputAxisComponent::Y); |
| 84 | + [[nodiscard]] static InputBinding MouseWheelBinding( |
| 85 | + f32 scale = 1.0f, |
| 86 | + InputAxisComponent component = InputAxisComponent::X); |
| 87 | + [[nodiscard]] static InputBinding MouseHorizontalWheelBinding( |
| 88 | + f32 scale = 1.0f, |
| 89 | + InputAxisComponent component = InputAxisComponent::X); |
| 90 | + |
| 91 | + InputBinding& RequireKey(Key modifier); |
| 92 | + InputBinding& RequireMouseButton(MouseButton modifier); |
| 93 | + |
| 94 | + [[nodiscard]] bool IsValid() const; |
| 95 | + [[nodiscard]] bool IsDigital() const; |
| 96 | + }; |
| 97 | + |
| 98 | + /** |
| 99 | + * @brief Evaluated state of one action for the current frame. |
| 100 | + */ |
| 101 | + struct InputActionState |
| 102 | + { |
| 103 | + InputActionType type = InputActionType::Button; |
| 104 | + InputActionVector2 value{}; |
| 105 | + bool active = false; |
| 106 | + bool pressed = false; |
| 107 | + bool released = false; |
| 108 | + |
| 109 | + [[nodiscard]] f32 GetValue() const { return value.x; } |
| 110 | + [[nodiscard]] InputActionVector2 GetValue2D() const { return value; } |
| 111 | + [[nodiscard]] bool IsDown() const { return active; } |
| 112 | + [[nodiscard]] bool WasPressed() const { return pressed; } |
| 113 | + [[nodiscard]] bool WasReleased() const { return released; } |
| 114 | + }; |
| 115 | + |
| 116 | + /** |
| 117 | + * @brief Named, prioritized collection of input actions and bindings. |
| 118 | + * |
| 119 | + * Contexts are engine-generic. Games can create gameplay, UI, editor, |
| 120 | + * vehicle, console, photo-mode, or any other context without changing the |
| 121 | + * platform backend. Higher-priority consuming contexts prevent active |
| 122 | + * controls from reaching lower-priority contexts. |
| 123 | + */ |
| 124 | + class InputContext final |
| 125 | + { |
| 126 | + public: |
| 127 | + [[nodiscard]] const std::string& GetName() const { return m_name; } |
| 128 | + [[nodiscard]] i32 GetPriority() const { return m_priority; } |
| 129 | + void SetPriority(i32 priority) { m_priority = priority; } |
| 130 | + |
| 131 | + [[nodiscard]] bool IsEnabled() const { return m_enabled; } |
| 132 | + void SetEnabled(bool enabled) { m_enabled = enabled; } |
| 133 | + |
| 134 | + [[nodiscard]] bool ConsumesInput() const { return m_consumeInput; } |
| 135 | + void SetConsumesInput(bool consumeInput) { m_consumeInput = consumeInput; } |
| 136 | + |
| 137 | + [[nodiscard]] bool AddAction(std::string name, InputActionType type); |
| 138 | + [[nodiscard]] bool RemoveAction(std::string_view name); |
| 139 | + [[nodiscard]] bool HasAction(std::string_view name) const; |
| 140 | + [[nodiscard]] std::size_t GetActionCount() const { return m_actions.size(); } |
| 141 | + |
| 142 | + [[nodiscard]] bool AddBinding(std::string_view actionName, const InputBinding& binding); |
| 143 | + [[nodiscard]] bool Rebind( |
| 144 | + std::string_view actionName, |
| 145 | + std::size_t bindingIndex, |
| 146 | + const InputBinding& replacement); |
| 147 | + [[nodiscard]] bool RemoveBinding(std::string_view actionName, std::size_t bindingIndex); |
| 148 | + [[nodiscard]] bool ClearBindings(std::string_view actionName); |
| 149 | + [[nodiscard]] std::size_t GetBindingCount(std::string_view actionName) const; |
| 150 | + [[nodiscard]] const InputBinding* GetBinding( |
| 151 | + std::string_view actionName, |
| 152 | + std::size_t bindingIndex) const; |
| 153 | + |
| 154 | + [[nodiscard]] const InputActionState* FindActionState(std::string_view name) const; |
| 155 | + |
| 156 | + private: |
| 157 | + friend class InputActionSystem; |
| 158 | + |
| 159 | + struct Action |
| 160 | + { |
| 161 | + std::string name; |
| 162 | + InputActionState state{}; |
| 163 | + std::vector<InputBinding> bindings; |
| 164 | + bool previousActive = false; |
| 165 | + }; |
| 166 | + |
| 167 | + InputContext(std::string name, i32 priority, bool consumeInput, u64 insertionOrder); |
| 168 | + |
| 169 | + [[nodiscard]] Action* FindAction(std::string_view name); |
| 170 | + [[nodiscard]] const Action* FindAction(std::string_view name) const; |
| 171 | + void ResetRuntimeState(bool emitRelease); |
| 172 | + |
| 173 | + std::string m_name; |
| 174 | + i32 m_priority = 0; |
| 175 | + bool m_enabled = true; |
| 176 | + bool m_consumeInput = true; |
| 177 | + u64 m_insertionOrder = 0; |
| 178 | + std::vector<Action> m_actions; |
| 179 | + }; |
| 180 | + |
| 181 | + /** |
| 182 | + * @brief Evaluates named actions from the current platform input snapshot. |
| 183 | + */ |
| 184 | + class InputActionSystem final |
| 185 | + { |
| 186 | + public: |
| 187 | + InputActionSystem() = default; |
| 188 | + InputActionSystem(const InputActionSystem&) = delete; |
| 189 | + InputActionSystem& operator=(const InputActionSystem&) = delete; |
| 190 | + |
| 191 | + [[nodiscard]] InputContext* CreateContext( |
| 192 | + std::string name, |
| 193 | + i32 priority = 0, |
| 194 | + bool consumeInput = true); |
| 195 | + [[nodiscard]] bool RemoveContext(std::string_view name); |
| 196 | + void ClearContexts(); |
| 197 | + |
| 198 | + [[nodiscard]] InputContext* FindContext(std::string_view name); |
| 199 | + [[nodiscard]] const InputContext* FindContext(std::string_view name) const; |
| 200 | + [[nodiscard]] std::size_t GetContextCount() const { return m_contexts.size(); } |
| 201 | + |
| 202 | + /** |
| 203 | + * @brief Evaluate every enabled context from one InputState snapshot. |
| 204 | + * |
| 205 | + * Call exactly once after native messages are processed and before game |
| 206 | + * update logic reads action states. Game performs this automatically. |
| 207 | + */ |
| 208 | + void Update(const InputState& input); |
| 209 | + |
| 210 | + [[nodiscard]] const InputActionState* FindActionState( |
| 211 | + std::string_view contextName, |
| 212 | + std::string_view actionName) const; |
| 213 | + |
| 214 | + /** |
| 215 | + * @brief Find an action in the highest-priority enabled context. |
| 216 | + */ |
| 217 | + [[nodiscard]] const InputActionState* FindActionState( |
| 218 | + std::string_view actionName) const; |
| 219 | + |
| 220 | + [[nodiscard]] bool IsActionDown( |
| 221 | + std::string_view contextName, |
| 222 | + std::string_view actionName) const; |
| 223 | + [[nodiscard]] bool WasActionPressed( |
| 224 | + std::string_view contextName, |
| 225 | + std::string_view actionName) const; |
| 226 | + [[nodiscard]] bool WasActionReleased( |
| 227 | + std::string_view contextName, |
| 228 | + std::string_view actionName) const; |
| 229 | + [[nodiscard]] f32 GetActionValue( |
| 230 | + std::string_view contextName, |
| 231 | + std::string_view actionName) const; |
| 232 | + [[nodiscard]] InputActionVector2 GetActionValue2D( |
| 233 | + std::string_view contextName, |
| 234 | + std::string_view actionName) const; |
| 235 | + |
| 236 | + private: |
| 237 | + std::vector<std::unique_ptr<InputContext>> m_contexts; |
| 238 | + u64 m_nextInsertionOrder = 1; |
| 239 | + }; |
| 240 | +} // namespace Pyramid |
0 commit comments