diff --git a/rfcs/25-ui-systems-event-handlers.md b/rfcs/25-ui-systems-event-handlers.md new file mode 100644 index 00000000..49a1b70b --- /dev/null +++ b/rfcs/25-ui-systems-event-handlers.md @@ -0,0 +1,394 @@ +# Feature Name: `ui-systems-event-handlers` + +## Summary + +App logic is dramatically clearer when it flows from input to actions to reactions. +This is handled quite simply (from an end user perspective) with a unified `Action` event type on UI elements and automatic input dispatching. +To execute logic triggered by interacting with the UI, use ordinary systems in concert with a general purpose event handling paradigm to store custom behavior on individual UI entities as components that tie into well-defined events in a principled way. + +## Motivation + +Bevy's ECS is a powerful and expressive tool for arbitrary computation and scheduling. +However, it tends to struggle badly when one-off behaviors are needed, resulting in both excessive boilerplate and a huge proliferation of systems that must run each and every frame. + +This is particularly relevant when it comes to designing the logic behind user interfaces, which are littered with special-cased, complex functions with very low performance demands. + +## User-facing explanation + +When building user interfaces in Bevy, data flows through three conceptual stages: + +1. **Input.** Raw keyboard, mouse, joystick events and so on are received. These are handled by various **input dispatching** systems, and converted into actions, with tangible game-specific meaning. +2. **Action.** Entities in our world (whether they're game objects or interactive **UI elements**) receive actions and change data. +3. **Reaction.** Other systems watch for changes or events produced by the UI elements that changed, and react to finish the tasks they started. + +In this chapter, we're going to discuss patterns you can use in Bevy to design user interfaces that map cleanly to this data flow, allowing them to be modified, built upon and debugged without the spaghetti. + +Once we've added an interactive UI element to our `World` (think a button, form or mini-map), we need to get actions to it in some form. +The simplest way to do this would be to listen to the input events yourself, and then act if an appropriate event is heard. + +```rust +// This system is added to CoreStage::Ui to ensure that it runs at the appropriate time +fn my_button(mut query: Query<(&Interaction, &mut Counter), + (With, Changed)>){ + // Extract the components on the button in question + // and see if it was clicked in the last frame + let (interaction, mut counter) = query.single_mut().unwrap(); + if *interaction == Interaction::Clicked { + *counter += 1; + } +} +``` + +However, this conflation of inputs and actions starts to get tricky if we want to add a keybinding that performs the same behavior. +Do we duplicate the logic? Mock the mouse input event to the correct button? +Instead, the better approach is to separate inputs from actions, and use the built-in **event-queue** that comes with our `ButtonBundle`. + +```rust +// Each of our buttons have an `Events` component, +// which stores a data-less event recording that it has been clicked. +// Adding your own `Events` components with special data to UI elements is easy; +// simply add it to your custom bundle on spawn. + +// Action events are automatically added to entities +// with an Interaction component when they are clicked on +// Here, we're adding a second route to the same end, triggering when "K" is pressed +fn my_button_hotkey(mut query: Query<&mut EventWriter, With>, keyboard_input: Res){ + if keyboard_input.just_pressed(KeyCode::K){ + let button_action_writer = query.single_mut().unwrap(); + // Sends a single, dataless event to our MyButton entity + button_action_writer.send(Action); + } +} + +// We can use the EventReader sugar to ergonomically read the events stored on our buttons +fn my_button(mut query: Query<(&mut EventReader, &mut Counter), With>){ + // Extract the components on the button in question + // and see if it was clicked in the last frame + let (actions, mut counter) = query.single_mut().unwrap(); + for _ in actions { + *counter += 1; + } +} +``` + +As you can see, decoupling inputs and actions in this way makes our code more robust (since it can handle multiple inputs in a single frame), and dramatically more extensible, without adding any extra boilerplate. +If we wanted to, we could add another system that read the same `Events` component on our `MyButton` entity, reading these events completely independently to perform new behavior each time either the button was clicked or "K" was pressed. + +Finally, we can use this decoupling to ensure that only valid inputs get turned into actions, by making sure that our systems runs after the `bevy::input::SystemLabels::InputDispatch` system label during the `CoreStage::PreUpdate` stage. +Input is converted to actions during systems with those labels, so we can intercept it before it is seen by any systems in our `Update` or `Input` stages. + +```rust +use bevy::prelude::*; +use bevy::input::SystemLabels; + +fn main(){ + App::build() + .add_system_to_stage(CoreStage::PreUpdate, + verify_cooldowns.system().after(SystemLabels::InputDispatch)) + .run(); +} + +/// Ignores all inputs to Cooldown-containing entities that are not ready +fn verify_cooldowns(mut query: Query<&Cooldown, &mut Events>){ + for cooldown, mut actions in query.iter_mut(){ + if !cooldown.finished{ + actions.clear(); + } + } +} +``` + +### Generalizing behavior + +Of course, we don't *really* want to make a separate system and marker component for every single button that we create. +Not only does this result in heavy code duplication, it also imposes a small (but non-zero) overhead for each system in our schedule every tick. +Furthermore, if the number of buttons isn't known at compile time, we *can't* just make more systems. + +Commonly though, we will have many related UI elements: unit building an RTS, ability buttons in a MOBA, options in a drop-down menu. +These will have similar, but not identical behavior, allowing us to move data from the *components* of the entity into an *event*, commonly of the exact same type. +In this way, we can use a single system to differentiate behavior. + +```rust +// Operates over any ability buttons we may have in a single system +fn ability_buttons(mut query: Query<(&mut EventReader, &mut Timer, &Ability)>, time: Res