From aea9b247b5dbcf99382da28b9679f6254abd1227 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 27 May 2021 00:56:22 -0400 Subject: [PATCH 01/23] Initial explanation --- rfcs/one-shot-call-backs.md | 205 ++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 rfcs/one-shot-call-backs.md diff --git a/rfcs/one-shot-call-backs.md b/rfcs/one-shot-call-backs.md new file mode 100644 index 00000000..d71626cf --- /dev/null +++ b/rfcs/one-shot-call-backs.md @@ -0,0 +1,205 @@ +# Feature Name: `one-shot-callbacks` + +## Summary + +When combined with ordinary systems, one-shot systems stored as components on UI entities offer a powerful and expressive callback-like paradigm for one-off UI behavior. +In order to reduce the importance of system ordering and avoid data integrity bugs, these systems run within a dedicated looping substage. + +## 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. + +### 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