#bevy-ui #bevy #ui #gamedev

better_button

Extend Bevy buttons with on-entered and on-exited events for press, hover and mouse over states

5 releases (stable)

1.2.0 Jul 20, 2024
1.1.1 Jul 9, 2024
1.1.0 Jul 5, 2024
1.0.0 Jun 29, 2024
0.8.0 Jun 26, 2024

#766 in Game dev

MIT license

18KB
266 lines

Introduction

Expands on the Interaction component provided in Bevy by tracking more states, and whether those states have just been entered or exited. These states are all bundled together in BButtonBundle as components, and can be used by querying for these components, or listening for the events they generate.

The library works by updating the additional button components based on the Interaction component placed along side them, which ensures parity with Bevy's own button behavior.

Simpy add the BButtonPlugin to your project and use BButtonBundle instead of Bevy's ButtonBundle to get started.

Tutorial

1. Setup

Create a new binary crate, add bevy as a dependency and copy the following code into your main.rs:

use bevy::prelude::*;
use bevy::color::palettes::css::*;

fn main() {
    App::new()
        .add_plugins(
            (
                DefaultPlugins,
            )
        )
        .add_systems(
            Startup,
            (
                spawn_camera,
            ),
        )
        .run();
}

fn spawn_camera(mut commands: Commands) {
    commands.spawn(
        Camera3dBundle {
            ..default()
        }
    );
}

2. Add A BButtonPlugin

Import the better_button prelude and add the BButtonPlugin to your app:

use bevy::prelude::*;
use bevy::color::palettes::css::*;
use better_button::prelude::*; // <------- Import the `better_button` prelude.

fn main() {
    App::new()
        .add_plugins(
            (
                DefaultPlugins,
                BButtonPlugin // <------- Add the `BButtonPlugin` to the app.
            )
        )
        .run();
}

This simply adds the necessary systems to update the button states, and also registers the button events for you.

3. Spawn The BButtonBundle

The BButtonBundle contains the Bevy ButtonBundle along with the button components provided by the better_button crate.

Create a new system to spawns your first BButtonBundle:

fn spawn_button(mut commands: Commands) {
    commands.spawn(BButtonBundle::new(
        ButtonBundle {
            style: Style
            {
                width: Val::Px(125.0),
                height: Val::Px(125.0),
                align_self: AlignSelf::Center,
                justify_self: JustifySelf::Center,
                ..default()
            },
            background_color: BackgroundColor(WHITE.into()),
            ..default()
        }
    ));
}

Add it to your Bevy app:

fn main() {
    App::new()
        .add_plugins(
            (
                DefaultPlugins,
            )
        )
        .add_systems(
            Startup,
            (
                spawn_camera,
                spawn_button // <------- Add the `spawn_button` system to the `Startup` schedule.
            ),
        )
        .run();
}

4. Respond To Button Presses In Update

Create a new system that changes the button's color to when pressed:

fn respond_to_button_state(
    mut query: Query<(&BPressState, &mut BackgroundColor)>
) {
    for (state, mut background_color) in &mut query {
        if state.just_entered {
            background_color.0 = YELLOW_GREEN.into();
        }
        if state.just_exited {
            background_color.0 = WHITE.into();
        }
    }
}

The system queries the BPressState component, which is a part of the BButtonBundle we used earlier.

Now add the system to your app, but make sure specify that it should run before or after the BButtonUpdateSet. This ensures that your system will run at least once between consecutive runs of the BButtonUpdateSet, since the order in which systems and system sets run in Bevy can change from frame-to-frame if left unordered:

fn main() {
    App::new()
        .add_plugins(
            (
                DefaultPlugins,
                BButtonPlugin
            )
        )
        .add_systems(
            Startup,
            (
                spawn_camera,
                spawn_button
            ),
        )
        .add_systems(
            Update, // <------- Make sure it's in the `Update` schedule. The reason will be explained later.
            respond_to_button_state.after(BButtonUpdateSet) // <------- Add the system, and set it to run after the `BButtonUpdateSet`.
        )
        .run();
}

4. Respond To Button Presses By Reading Events

Since the state components of the button we created are updated in the Update schedule when using the BButtonPlugin, we cannot reliably read button presses from systems in the FixedUpdate schedule with queries on the button components. This is because the just_entered and just_exited properties of the BPressState component are only set to true for one frame in the Update schedule. And since in most cases the Update schedule runs multiple times for each FixedUpdate, the just_entered and just_exited properties could be set to true and then back to false between two FixedUpdate frames.

This is where using events come in hand, since Bevy ensures that all systems in both Update and FixedUpdate receive any events generated exactly once before the events disappear.

Create a new system that changes the button's color when hovered, this time using events:

fn respond_to_button_events(
    mut query: Query<&mut BackgroundColor, With<BHoverState>>,
    mut event_reader: EventReader<BHoverEvent>,
) {
    for event in event_reader.read() {
        match event {
            BHoverEvent::JustEntered { entity } => {
                if let Ok(mut background_color) = query.get_mut(*entity) {
                    background_color.0 = YELLOW_GREEN.into();
                }
            }
            BHoverEvent::JustExited { entity } => {
                if let Ok(mut background_color) = query.get_mut(*entity) {
                    background_color.0 = WHITE.into();
                }
            }
        }
    }
}

Comment out the previous system and add the latest one to the FixedUpdate schedule in your Bevy app:

fn main() {
    App::new()
        .add_plugins(
            (
                DefaultPlugins,
                BButtonPlugin
            )
        )
        .add_systems(
            Startup,
            (
                spawn_camera,  
                spawn_button  
            ),
        )
        // .add_systems(
        //     Update,
        //     respond_to_button_state.after(BButtonUpdateSet)
        // )
        .add_systems(
            FixedUpdate,
            respond_to_button_events, // <------- Add the `respond_to_button_events` system to `FixedUpdate`.
        )
        .run();
}

As you might have noticed, we do not need to specify whether our new system runs before or after BButtonUpdateSet. As mentioned earlier, Bevy ensures that all systems receive the events they read exactly once. So even if we did add our new system to the Update schedule without using .after(BButtonUpdateSet), we can know for sure that it will not miss anything.

5. Conclusion

The decision whether to use the better_button crate with its events or by querying the components directly is up to you. There are pros and cons for both techniques.

In general, when working in the Update schedule, it tends to be easier to query the button components directly when working with non-gameplay related logic. Like updating your button's visuals. On the other hand, using events are necessary when you are working in FixedUpdate. For example, using a button press to make a character jump on mobile devices.

What Next?

Please check out the Wiki for more information about this library.

Dependencies

~24MB
~455K SLoC