new input system
This commit is contained in:
parent
b3db40ebec
commit
ac2eb7fb0a
31 changed files with 3292 additions and 781 deletions
241
examples/InputSystem/main.cpp
Normal file
241
examples/InputSystem/main.cpp
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
// =====================================================================
|
||||
// InputSystem — guided tour of Crafter::Input
|
||||
// =====================================================================
|
||||
//
|
||||
// Run this with a window focused. The console narrates each event as
|
||||
// it fires; the on-screen window only exists so the OS routes keyboard
|
||||
// and mouse input to us. Plug a gamepad in (Xbox / DualShock /
|
||||
// Switch Pro) and it will be picked up live.
|
||||
//
|
||||
// What this demo demonstrates, in order:
|
||||
//
|
||||
// 1. Three runtime layers stacked end-to-end:
|
||||
// - Crafter::Key(CrafterKeys::X) compile-time abstract → raw
|
||||
// platform KeyCode
|
||||
// - Window::onRawKeyDown raw keyboard channel for UI /
|
||||
// text input (NOT used for game
|
||||
// input — actions handle that)
|
||||
// - Crafter::Input::Map named actions → bindings →
|
||||
// events
|
||||
// - Crafter::Gamepad hot-plug, button + axis state,
|
||||
// rumble
|
||||
//
|
||||
// 2. All four action types: Button, Axis1D, Vector2, with multiple
|
||||
// bindings per action (any-of semantics — press Space OR press
|
||||
// gamepad-A; both trigger Jump).
|
||||
//
|
||||
// 3. Composite bindings: WASDBind (4 keys → Vector2),
|
||||
// GamepadStickBind (analog stick → Vector2), MouseDeltaBind
|
||||
// (mouse movement → Vector2).
|
||||
//
|
||||
// 4. Runtime rebinding: press R to enter rebind mode; the next key /
|
||||
// mouse button / gamepad button you press becomes the new Jump
|
||||
// binding. The captured input does NOT fire Jump that frame.
|
||||
//
|
||||
// 5. Serialization: bindings are round-tripped to text strings at
|
||||
// startup. Use these as the on-disk format for your settings file.
|
||||
//
|
||||
// 6. Hot-plug: connect / disconnect a gamepad while running. The
|
||||
// Map's polling-based evaluation seamlessly switches over —
|
||||
// bindings to a vanished gamepad become inert until reconnect.
|
||||
//
|
||||
// Controls:
|
||||
// Space / Gamepad A → Jump
|
||||
// WASD / Left stick → Move
|
||||
// Mouse-Left / Right Trigger → Fire
|
||||
// Mouse delta / Right stick → Look
|
||||
// Mouse wheel → Zoom (Axis1D)
|
||||
// R → Start rebinding "Jump" — press
|
||||
// any key/button next to assign it
|
||||
// Esc → Quit
|
||||
//
|
||||
// Output is intentionally chatty so you can SEE the system working.
|
||||
// In a real game, replace the print statements with your gameplay
|
||||
// code — that's it.
|
||||
// =====================================================================
|
||||
|
||||
import Crafter.Graphics;
|
||||
import Crafter.Event; // EventListener<>: Crafter.Graphics imports
|
||||
import Crafter.Math; // Vector<float,2>: it transitively, but
|
||||
// C++20 module-reachability rules require
|
||||
// us to name them explicitly to instantiate
|
||||
// the templates in this TU.
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
using namespace Crafter::Input;
|
||||
|
||||
int main() {
|
||||
Device::Initialize();
|
||||
Window window(640, 360, "Crafter Input — give the window focus");
|
||||
|
||||
// =================================================================
|
||||
// Build the action map.
|
||||
//
|
||||
// AddAction returns a reference; we keep it so we can subscribe.
|
||||
// The map owns the actions (vector<unique_ptr>); references stay
|
||||
// valid for the map's lifetime.
|
||||
// =================================================================
|
||||
Map map;
|
||||
Action& jump = map.AddAction("Jump", ActionType::Button);
|
||||
Action& move = map.AddAction("Move", ActionType::Vector2);
|
||||
Action& fire = map.AddAction("Fire", ActionType::Button);
|
||||
Action& look = map.AddAction("Look", ActionType::Vector2);
|
||||
Action& zoom = map.AddAction("Zoom", ActionType::Axis1D);
|
||||
Action& reset = map.AddAction("Reset", ActionType::Button);
|
||||
Action& quit = map.AddAction("Quit", ActionType::Button);
|
||||
|
||||
// =================================================================
|
||||
// Default bindings.
|
||||
//
|
||||
// Key(CrafterKeys::X) is `consteval` — these literals fold to
|
||||
// per-platform scancodes at compile time, no runtime translation.
|
||||
// Multiple bindings per action are OR'd: pressing EITHER triggers
|
||||
// the action.
|
||||
//
|
||||
// gamepadId == 0 is a sentinel here meaning "first connected
|
||||
// gamepad". For multi-player you'd save the stable id from a
|
||||
// specific Device and use that.
|
||||
// =================================================================
|
||||
jump.bindings = {
|
||||
KeyBind{ Key(CrafterKeys::Space) },
|
||||
GamepadButtonBind{ 0, Gamepad::Button::South },
|
||||
};
|
||||
move.bindings = {
|
||||
WASDBind{
|
||||
Key(CrafterKeys::W), Key(CrafterKeys::S),
|
||||
Key(CrafterKeys::A), Key(CrafterKeys::D),
|
||||
},
|
||||
GamepadStickBind{ 0, Gamepad::Stick::Left },
|
||||
};
|
||||
fire.bindings = {
|
||||
MouseButtonBind{ 0 }, // left click
|
||||
GamepadAxisBind{ 0, Gamepad::Axis::RightTrigger, false }, // analog R2
|
||||
};
|
||||
look.bindings = {
|
||||
MouseDeltaBind{ 0.01f }, // scale down
|
||||
GamepadStickBind{ 0, Gamepad::Stick::Right },
|
||||
};
|
||||
zoom.bindings = {
|
||||
MouseScrollBind{},
|
||||
};
|
||||
reset.bindings = { KeyBind{ Key(CrafterKeys::R) } };
|
||||
quit.bindings = { KeyBind{ Key(CrafterKeys::Escape) } };
|
||||
|
||||
// =================================================================
|
||||
// Serialization round-trip — call sites can save these strings to
|
||||
// disk and load them back into Action::bindings later.
|
||||
//
|
||||
// IMPORTANT: KeyCode values are platform-specific (Win32 scancodes
|
||||
// vs. Wayland kernel keycodes). A saved file from one platform
|
||||
// does NOT load on the other. Store your config under platform-
|
||||
// specific paths.
|
||||
// =================================================================
|
||||
std::println("─── Default bindings (serialized form) ───");
|
||||
for (auto& a : map.actions) {
|
||||
for (auto& b : a->bindings) {
|
||||
std::string s = BindingToString(b);
|
||||
// Parse it back to prove the format roundtrips.
|
||||
auto parsed = BindingFromString(s);
|
||||
std::println(" {:<6} {} (roundtrip OK: {})",
|
||||
a->name, s, parsed.has_value());
|
||||
}
|
||||
}
|
||||
std::println("");
|
||||
|
||||
// =================================================================
|
||||
// Subscribe to action events.
|
||||
//
|
||||
// EventListeners use the same RAII pattern as the rest of the
|
||||
// library — declare them in scope, they unsubscribe when they go
|
||||
// out of scope. Capturing references to the Actions is safe
|
||||
// because the Map (and therefore its actions) outlive these
|
||||
// listeners (both are in main()'s scope).
|
||||
// =================================================================
|
||||
EventListener<void> jumpPerf(&jump.onPerformed,
|
||||
[] { std::println("[Jump] performed"); });
|
||||
EventListener<void> jumpCanc(&jump.onCanceled,
|
||||
[] { std::println("[Jump] canceled"); });
|
||||
|
||||
EventListener<Vector<float,2>> moveChanged(&move.onVector2Changed,
|
||||
[](Vector<float,2> v) {
|
||||
std::println("[Move] ({:+.2f}, {:+.2f})", v.x, v.y);
|
||||
});
|
||||
|
||||
EventListener<void> firePerf(&fire.onPerformed,
|
||||
[] { std::println("[Fire] performed"); });
|
||||
EventListener<void> fireCanc(&fire.onCanceled,
|
||||
[] { std::println("[Fire] canceled"); });
|
||||
|
||||
EventListener<Vector<float,2>> lookChanged(&look.onVector2Changed,
|
||||
[](Vector<float,2> v) {
|
||||
// Mouse delta fires every frame the mouse moves; throttle
|
||||
// by checking magnitude so the console doesn't drown.
|
||||
if (std::abs(v.x) + std::abs(v.y) > 0.01f) {
|
||||
std::println("[Look] ({:+.3f}, {:+.3f})", v.x, v.y);
|
||||
}
|
||||
});
|
||||
|
||||
EventListener<float> zoomChanged(&zoom.onValueChanged,
|
||||
[](float v) { std::println("[Zoom] delta {:+.0f}", v); });
|
||||
|
||||
EventListener<void> quitPerf(&quit.onPerformed,
|
||||
[&] {
|
||||
std::println("[Quit] shutting down");
|
||||
window.open = false;
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// Rebinding flow.
|
||||
//
|
||||
// Pressing R fires Reset → we call StartRebind for the Jump
|
||||
// action. While rebinding, the next qualifying input does NOT
|
||||
// fire Jump (or any other action that uses the same input). It
|
||||
// routes to the onCaptured callback exactly once.
|
||||
//
|
||||
// CaptureMask::Any accepts keyboard | mouse | gamepad. Narrow it
|
||||
// (Keyboard, Gamepad, etc.) to restrict what counts as a valid
|
||||
// binding for a given action.
|
||||
// =================================================================
|
||||
EventListener<void> resetPerf(&reset.onPerformed, [&] {
|
||||
std::println("[Rebind] Press any key / button to bind \"Jump\" "
|
||||
"(Esc to cancel)…");
|
||||
map.StartRebind(jump, CaptureMask::Any, [&](Binding captured) {
|
||||
jump.bindings.clear();
|
||||
jump.bindings.push_back(captured);
|
||||
std::println("[Rebind] Jump → {}", BindingToString(captured));
|
||||
});
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// Gamepad lifecycle observers.
|
||||
//
|
||||
// Gamepad::Tick() is called automatically inside Window::StartSync
|
||||
// every frame, so these events fire on the main thread alongside
|
||||
// every other event. Hot-plug a controller while running and you
|
||||
// should see the connect line below.
|
||||
// =================================================================
|
||||
EventListener<Gamepad::Device*> gpAdded(&Gamepad::onConnected,
|
||||
[](Gamepad::Device* d) {
|
||||
std::println("[Gamepad] connected: id={} name=\"{}\"",
|
||||
d->id, d->name);
|
||||
});
|
||||
EventListener<Gamepad::Device*> gpRemoved(&Gamepad::onDisconnected,
|
||||
[](Gamepad::Device* d) {
|
||||
std::println("[Gamepad] disconnected: id={}", d->id);
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// Connect the map to the window. Map polls window state + gamepad
|
||||
// state every Tick; we trigger Tick from onBeforeUpdate so it
|
||||
// runs after the OS event pump (and Gamepad::Tick) for that frame.
|
||||
// =================================================================
|
||||
map.Attach(window);
|
||||
EventListener<void> tick(&window.onBeforeUpdate, [&] { map.Tick(); });
|
||||
|
||||
std::println("─── Ready. Focus the window and try the controls. ───");
|
||||
window.Render();
|
||||
window.StartSync();
|
||||
|
||||
std::println("Done.");
|
||||
return 0;
|
||||
}
|
||||
23
examples/InputSystem/project.cpp
Normal file
23
examples/InputSystem/project.cpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
Configuration* graphics = LocalProject({
|
||||
.projectFile = "../../project.cpp",
|
||||
.args = std::vector<std::string>(args.begin(), args.end()),
|
||||
});
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "InputSystem";
|
||||
cfg.outputName = "InputSystem";
|
||||
ApplyStandardArgs(cfg, args);
|
||||
cfg.dependencies = { graphics };
|
||||
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue