Skip to content

Commit d63f54f

Browse files
committed
feat(input): add configurable action contexts and rebinding
1 parent f839ceb commit d63f54f

24 files changed

Lines changed: 1500 additions & 63 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
## Scope
44

55
- `Engine/` builds the C++17 `PyramidEngine` library.
6-
- Active modules are Core, Graphics, Math, Platform, and Utils.
6+
- Active modules are Core, Graphics, Input, Math, Platform, and Utils.
77
- `Examples/BasicGame` and `Examples/BasicRendering` are the graphical references.
8-
- `Tests/PublicApiLinkage.cpp` protects selected public symbols. Focused tests cover platform input state, resize behavior, camera/frustum logic, framebuffers, texture loading, spatial updates/queries/configuration/compaction, render bounds, resources/caches, registry handles/manifests, the authoritative entity scene, and scene serialization. `Tests/EntitySceneTests.cpp` protects stable entity IDs, cycle-safe hierarchy transforms, inherited visibility, component attachment, generated renderer/light proxies, and recursive destruction. `Tests/SceneSerializationTests.cpp` protects deterministic version-2 entity/component round trips, exact manifest references, hierarchy validation, and missing/stale resource diagnostics.
8+
- `Tests/PublicApiLinkage.cpp` protects selected public symbols. Focused tests cover platform input state, generic action mapping, resize behavior, camera/frustum logic, framebuffers, texture loading, spatial updates/queries/configuration/compaction, render bounds, resources/caches, registry handles/manifests, the authoritative entity scene, and scene serialization. `Tests/EntitySceneTests.cpp` protects stable entity IDs, cycle-safe hierarchy transforms, inherited visibility, component attachment, generated renderer/light proxies, and recursive destruction. `Tests/SceneSerializationTests.cpp` protects deterministic version-2 entity/component round trips, exact manifest references, hierarchy validation, and missing/stale resource diagnostics.
99
- `Tests/Consumer` validates the installed CMake package.
1010
- `vendor/glad` is a bundled public dependency. libjpeg-turbo is an external open-source dependency resolved through CMake `FindJPEG`.
1111
- The supported Windows toolchain is MSYS2 UCRT64 with MinGW-w64 GCC; Clang is also validated. Visual Studio is not required.
12-
- Real Win32 keyboard/mouse polling is implemented. Action mapping, audio, physics, editor, scripting, DirectX, Vulkan, Linux, and macOS are not yet supported. The first product target is a Windows RTS vertical slice; Linux follows it, a full editor is planned, and Baa is the long-term scripting and rewrite target.
12+
- Real Win32 keyboard/mouse polling and engine-generic action mapping are implemented. Controllers, audio, physics, editor, scripting, DirectX, Vulkan, Linux, and macOS are not yet supported. The first product target is a Windows RTS vertical slice; Linux follows it, a full editor is planned, and Baa is the long-term scripting and rewrite target.
1313

1414
## Build and test
1515

@@ -32,7 +32,7 @@ ctest --preset test-gcc-release
3232

3333
- C++17, four spaces, braces on new lines.
3434
- Types/public methods use `PascalCase`; locals/parameters use `camelCase`; fields use `m_`.
35-
- Prefer RAII, explicit ownership, and `PYRAMID_LOG_*` diagnostics. Geometry passed to scenes must use `Mesh`; do not reintroduce raw vertex-array fields on `RenderObject`. Shared reusable geometry should be acquired through the game-owned `ResourceRegistry::Meshes()` cache; do not create parallel uploads for byte-identical mesh specifications. Shared shader programs should be acquired through `ResourceRegistry::Shaders()`; do not mutate cached `ShaderProgram` instances directly or compile identical stage source repeatedly. Shared sampled textures should use `ResourceRegistry::Textures()`; cached `TextureResource` instances are immutable, color space is part of identity, and file changes must be published through transactional reload. Scene renderables should acquire immutable `Material` resources through `ResourceRegistry::Materials()` rather than ad-hoc shader/texture fields; per-draw matrices belong in command-buffer uniforms, not material identity. Platform input must remain backend-neutral at the public API boundary: native messages feed `InputState`, games poll it through `Game::GetInput()`, and focus loss must release held controls. Long-lived scene and serialized references should use typed registry handles. Handles must stay non-owning, every alias bind/remap/removal must advance its generation, direct cache mutation must invalidate existing handles, and stale handles must resolve to null rather than a replacement resource.
35+
- Prefer RAII, explicit ownership, and `PYRAMID_LOG_*` diagnostics. Geometry passed to scenes must use `Mesh`; do not reintroduce raw vertex-array fields on `RenderObject`. Shared reusable geometry should be acquired through the game-owned `ResourceRegistry::Meshes()` cache; do not create parallel uploads for byte-identical mesh specifications. Shared shader programs should be acquired through `ResourceRegistry::Shaders()`; do not mutate cached `ShaderProgram` instances directly or compile identical stage source repeatedly. Shared sampled textures should use `ResourceRegistry::Textures()`; cached `TextureResource` instances are immutable, color space is part of identity, and file changes must be published through transactional reload. Scene renderables should acquire immutable `Material` resources through `ResourceRegistry::Materials()` rather than ad-hoc shader/texture fields; per-draw matrices belong in command-buffer uniforms, not material identity. Platform input must remain backend-neutral at the public API boundary: native messages feed `InputState`, `Game` evaluates generic `InputActionSystem` contexts before updates, and focus loss must release held controls. Never hard-code RTS or other game-specific action names into the input module; reference profiles belong in examples or games. Long-lived scene and serialized references should use typed registry handles. Handles must stay non-owning, every alias bind/remap/removal must advance its generation, direct cache mutation must invalidate existing handles, and stale handles must resolve to null rather than a replacement resource.
3636
- Scene authoring must use `Entity` plus components; do not reintroduce `SceneNode` or make `RenderObject` transforms authoritative. Renderer/light proxies are generated from the scene. Stable entity IDs and hierarchy invariants must remain serialization-safe.
3737
- Do not add required interface methods with silent no-op defaults.
3838
- Do not expose source-tree absolute paths through installed target interfaces.

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to Pyramid Engine are documented here. The project is pre-al
44

55
## [Unreleased]
66

7+
### Generic input action mapping
8+
9+
- Added engine-generic button, one-dimensional, and two-dimensional named actions above the platform `InputState` snapshot.
10+
- Added prioritized input contexts with enable/disable state, active-control consumption, deterministic ordering, and non-consuming observation contexts.
11+
- Added keyboard, mouse-button, mouse-delta, and wheel bindings with scaling, axis selection, optional key/mouse-button chords, and runtime rebinding/removal.
12+
- Integrated one `InputActionSystem` into `Game`; actions are evaluated after native message processing and before every `onUpdate()`.
13+
- Migrated both examples to named actions. `BasicRendering` provides an RTS-style reference profile without placing RTS semantics in the engine module.
14+
- Added `Input.ActionMapping`; CTest now registers 31 tests.
15+
716
### Win32 input foundation
817

918
- Added platform-neutral keyboard and mouse enums plus per-frame `InputState` polling.

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ install(
5353
Engine/Core/include/
5454
Engine/Graphics/include/
5555
Engine/Platform/include/
56+
Engine/Input/include/
5657
Engine/Math/include/
5758
Engine/Utils/include/
5859
vendor/glad/include/

Engine/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ add_library(Pyramid::Engine ALIAS PyramidEngine)
44
add_subdirectory(Core)
55
add_subdirectory(Graphics)
66
add_subdirectory(Platform)
7+
add_subdirectory(Input)
78
add_subdirectory(Math)
89
add_subdirectory(Utils)
910

@@ -19,6 +20,7 @@ target_include_directories(PyramidEngine
1920
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/Engine/Core/include>
2021
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/Engine/Graphics/include>
2122
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/Engine/Platform/include>
23+
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/Engine/Input/include>
2224
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/Engine/Math/include>
2325
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/Engine/Utils/include>
2426
$<INSTALL_INTERFACE:include>

Engine/Core/include/Pyramid/Core/Game.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22
#include <Pyramid/Graphics/GraphicsDevice.hpp>
33
#include <Pyramid/Platform/Window.hpp> // Added for std::unique_ptr<Window>
4+
#include <Pyramid/Input/InputActions.hpp>
45
#include <memory>
56

67
namespace Pyramid {
@@ -108,6 +109,15 @@ class Game
108109
*/
109110
const InputState& GetInput() const;
110111

112+
/**
113+
* @brief Engine-generic named action and input-context system.
114+
*
115+
* Configure contexts and bindings during onCreate(). Game evaluates them
116+
* once per frame after native input messages and before onUpdate().
117+
*/
118+
InputActionSystem& GetInputActions() { return m_inputActions; }
119+
const InputActionSystem& GetInputActions() const { return m_inputActions; }
120+
111121
/**
112122
* @brief Register the camera whose projection follows the window client size.
113123
* @param camera Non-owning camera pointer, or nullptr to detach it.
@@ -144,6 +154,7 @@ class Game
144154
std::unique_ptr<Window> m_window;
145155
std::unique_ptr<IGraphicsDevice> m_graphicsDevice;
146156
std::unique_ptr<ResourceRegistry> m_resourceRegistry;
157+
InputActionSystem m_inputActions;
147158
Camera* m_activeCamera;
148159
Renderer::RenderSystem* m_renderSystem;
149160
bool m_isRunning;

Engine/Core/source/Game.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,10 @@ namespace Pyramid
282282
// Clamp delta time to prevent large jumps (e.g., when debugging or system lag)
283283
deltaTime = (std::min)(deltaTime, maxDeltaTime);
284284

285+
// Evaluate engine-generic action contexts from the completed native
286+
// input snapshot before game logic reads named actions.
287+
m_inputActions.Update(GetInput());
288+
285289
// Update game logic
286290
onUpdate(deltaTime);
287291

Engine/Input/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
target_sources(PyramidEngine
2+
PRIVATE
3+
source/InputActions.cpp
4+
PRIVATE
5+
include/Pyramid/Input/InputActions.hpp
6+
)
7+
8+
target_include_directories(PyramidEngine
9+
PUBLIC
10+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
11+
$<INSTALL_INTERFACE:include>
12+
PRIVATE
13+
${CMAKE_CURRENT_SOURCE_DIR}/source
14+
)
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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

Comments
 (0)