diff --git a/doc/integrations/user_actions.md b/doc/integrations/user_actions.md new file mode 100644 index 000000000..2425e3f52 --- /dev/null +++ b/doc/integrations/user_actions.md @@ -0,0 +1,286 @@ +# User Actions integration + +@tableofcontents + +## Description + +The [user actions integration](@ref ecstasy::integration::user_action) allows to map events to an action. +Actions can be mapped to keyboard, mouse or gamepad, making the handle of different inputs easier since you don't have to deal with the different event types between keyboard/gamepad. +It also helps dealing with local multiplayer as an action can be linked to a specific user. +Finally it has included toml serialization functions to save/load the user keybinds. + +If you don't really understand what is an Action here is an example: +You play a 2D plateformer game where you can _move left_, _move right_ and _jump_, these are your 3 actions. +If you have local multiplayer you will need these actions for **each player**, that's why "user actions", action binding are attached to a specific player. + +@warning +This integration requires the event integration + +## Usage + +We'll see how to implement listeners for the example of a 2D platformer. +Move actions will be configured with arrows and ZQSD on keyboard, and left axis and left/right triggers on gamepad. + +### Register your actions and bindings + +An action is simply and identifier. +An action binding is a link between an action identifier, a user identifier and an event (multiple bindings can be linked to the same action/user id). + +The easier to define your action identifiers is simply to use an enum. + +```cpp +enum class Actions : size_t { MoveLeft, MoveRight, MoveHorizontal, Jump, Count }; +``` + +Now to create the bindings follow these steps: + +- Add a [Users](@ref ecstasy::integrations::user_action::Users) resource to the registry +- Add your [Action bindings](@ref ecstasy::integrations::user_action::ActionBinding) to your users +- Notify the user resources bindings have changed + +```cpp +using eua = ecstasy::integration::user_action; +using event = ecstasy::integration::event; + +void setupActions(ecstasy::Registry ®istry) +{ + // We need this resource to store the action bindings and handle the various users profiles + eua::Users &users = registry.addResource(); + + // Get a reference to the bindings vector of the first user (getUserProfile has a parameter to set the user ID, default to 0) + // I admit this is a little verbose + auto &binds = users.getUserProfile().getActionBindings().getBindings(); + // Run backwards ! + binds.emplace_back(Actions::MoveLeft, event::Keyboard::Key::Q); + binds.emplace_back(Actions::MoveLeft, event::Keyboard::Key::Left); + binds.emplace_back(Actions::MoveLeft, event::Gamepad::Axis::TriggerLeft); + // Run forward ! + binds.emplace_back(Actions::MoveRight, event::Keyboard::Key::D); + binds.emplace_back(Actions::MoveRight, event::Keyboard::Key::Right); + binds.emplace_back(Actions::MoveRight, event::Gamepad::Axis::TriggerRight); + // RUN, need a specific action because gamepad axis are from range -1 to 1 + binds.emplace_back(Actions::MoveHorizontal, event::Gamepad::Axis::LeftX); + // Jump ! + binds.emplace_back(Actions::Jump, event::Keyboard::Key::Space); + binds.emplace_back(Actions::Jump, event::Gamepad::Button::FaceDown); + + // This is important to notice the users the internal bindings vector has changed + users.updateBindings(); +} +``` + +### Listen to your actions + +There are 3 component types to have action listeners: + +- [ActionListener](@ref ecstasy::integration::user_action::ActionListener): Single action listener, easy to create but limited to a single function since entity can't have multiple instances of the same component. +- [ActionListeners](@ref ecstasy::integration::user_action::ActionListeners): Map between Action id <-> Action Listener, verbose to instantiate but allows to listen to every action type in different callbacks +- [ActionIdListener](@ref ecstasy::integration::user_action::ActionIdListener): Templated listener to take advantage of the ecs query system, requires another resource/system to works + +@note +They can be used together, either on different entities or on the same entity. + +@note +Listeners can be targeted toward an action type (identifier), if you want to match all actions you can use the special identifier [Actions::All](@ref ecstasy::integration::user_action::Action::All) + +#### ActionListener + +[ActionListener](@ref ecstasy::integration::user_action::ActionListener) are the easiest listeners, but you can attach only one of them to each entities. + +```cpp +using eua = ecstasy::integration::user_action; + +void setupListeners(ecstasy::Registry ®istry) +{ + // This entity will listen to every action change + registry.entityBuilder() + .with([](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Action "<([](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Jump action ("<({ + {static_cast(Actions::MoveLeft), [&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Move to the left at speed "<(Actions::MoveRight), [&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Move to the right at speed "<(Actions::Jump), [&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Jump! "<(); + // Add the actions polling system + // It takes as template parameters an integer sequence where each entry is supposed to match an action id, and therefore an ActionIdListener storage + registry.addSystem(Actions::Count)>>>(); + + + // This entity will listen to every action change, and have also special behaviors for the different actions + // The templated type expects eua::Action::Id (size_t) and enum cannot be converted implicitly so casts are required. + registry.entityBuilder() + .with(Actions::MoveLeft)>>([&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Move to the left at speed "<(Actions::MoveRight)>>([&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Move to the right at speed "<(Actions::Jump)>>([&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Jump! "<(Actions::MoveLeft)>>([&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + a.id = Actions::MoveHorizontal; + a.value = -a.value; // We are in the negative way + // Send it back to the appropriate listeners + r.getResource().callActionListeners(r, a) + }) + .with(Actions::MoveRight)>>([&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + a.id = Actions::MoveHorizontal; + // Send it back to the appropriate listeners + r.getResource().callActionListeners(r, a) + }) + .build(); + + // And this is our real entity watching the move action + registry.entityBuilder() + .with(Actions::MoveHorizontal)>>([&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Moving at speed "<(Actions::Jump)>>([&values](ecstasy::Registry &r, ecstasy::Entity e, eua::Action a) { + std::cout<<"Jump! "<().getUserProfile(userId).dump(); + + // Just replace this with a filestream if you want to save to a file + std::cout<< out <Q', 'Key->Left', 'GamePadAxis->TriggerLeft' ] +# Move Right +Action-1 = [ 'Key->D', 'Key->Right', 'GamePadAxis->TriggerRight' ] +# Move Horizontal +Action-2 = [ 'GamepadAxis->LeftX' ] +# Jump +Action-3 = [ 'Key->Space', 'GamepadButton->FaceDown' ] +``` + +#### Loading + +```cpp +using eua = ecstasy::integration::user_action; + +void loadProfile(ecstasy::Registry ®istry, std::string_view bindings) +{ + // Consider we called the setupActions from above + toml::table in = toml::parse(bindings); + + auto &profile = registry.getResource().getUserProfile(static_cast(in.get("id")->as_integer()->get())); + profile.load(in); +} + +```