241 lines
11 KiB
C++
241 lines
11 KiB
C++
|
|
// =====================================================================
|
||
|
|
// 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;
|
||
|
|
}
|