// ===================================================================== // 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: 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); 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 jumpPerf(&jump.onPerformed, [] { std::println("[Jump] performed"); }); EventListener jumpCanc(&jump.onCanceled, [] { std::println("[Jump] canceled"); }); EventListener> moveChanged(&move.onVector2Changed, [](Vector v) { std::println("[Move] ({:+.2f}, {:+.2f})", v.x, v.y); }); EventListener firePerf(&fire.onPerformed, [] { std::println("[Fire] performed"); }); EventListener fireCanc(&fire.onCanceled, [] { std::println("[Fire] canceled"); }); EventListener> lookChanged(&look.onVector2Changed, [](Vector 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 zoomChanged(&zoom.onValueChanged, [](float v) { std::println("[Zoom] delta {:+.0f}", v); }); EventListener 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 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 gpAdded(&Gamepad::onConnected, [](Gamepad::Device* d) { std::println("[Gamepad] connected: id={} name=\"{}\"", d->id, d->name); }); EventListener 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 tick(&window.onBeforeUpdate, [&] { map.Tick(); }); std::println("─── Ready. Focus the window and try the controls. ───"); window.Render(); window.StartSync(); std::println("Done."); return 0; }