🎁 Particles

Let's add some particles! They are a good effect to help make a game look nicer, and can be easy to add.

Bevy doesn't have first party support for particles, but there are at least two third party plugins that provide that:

bevy_hanabi uses the GPU through compute shaders, while bevy_enoki does it all on the CPU. In our case, as we want our game to work on Wasm in WebGL2 where compute shaders are not available, we'll use bevy_enoki.

bevy_enoki Setup

Let's add the plugin to our project:

cargo add bevy_enoki

And the plugin to our app:

extern crate bevy;
extern crate avian2d;
extern crate bevy_enhanced_input;
extern crate bevy_enoki;
use avian2d::{PhysicsPlugins, prelude::Gravity};
use bevy::prelude::*;
use bevy_enoki::EnokiPlugin;
use bevy_enhanced_input::EnhancedInputPlugin;
fn main() {
    App::new()
        // ...
       .add_plugins(DefaultPlugins)
        .add_plugins((PhysicsPlugins::default(), EnhancedInputPlugin, EnokiPlugin))
        // ...
;
}

Creating a Particle Effect for our Ship Jet

bevy_enoki particle effects are declared through an Particle2dEffect asset. The easiest way to do that is through a ron configuration file.

In the file assets/jet.particle.ron, add the following content:

(
    spawn_rate: 0.1,
    spawn_amount: 1,
    emission_shape: Point,
    lifetime: (1.0, 0.0),
    linear_speed: Some((100, 0.1)),
    direction: Some(((0, -1), 0.1)),
    scale: Some((3., 1.)),
    color: Some((red: 3.0, green: 3.0, blue: 0.0, alpha: 1.0)),
)

You don't need to define all the fields, only the one that you want to set with e different value than the default one.

If you look at the color we defined, it will be a yellow that will have a bloom effect.

We can now load that file in our GameAssets struct, in a field jet of type Handle<Particle2dEffect>.

We'll load it as a "sibling" to our jet Sprite. Like the jet which is set to hidden when no thrust is applied, the particle effect will be set as inactive:

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_enoki;
use bevy::prelude::*;
use bevy_enoki::prelude::*;
#[derive(Resource)]
struct GameAssets{ player_ship: Handle<Image>, jet_particles: Handle<Particle2dEffect> }
fn spawn_player(commands: &mut Commands, game_assets: &GameAssets) {
    // Actions setup

    commands
        .spawn((
            Sprite::from_image(game_assets.player_ship.clone()),
            // Rest of the components of the ship
            children![
                (
                    // Components for the jet sprite
                ),
                (
                    ParticleSpawner::default(),
                    ParticleSpawnerState {
                        active: false,
                        ..default()
                    },
                    ParticleEffectHandle(game_assets.jet_particles.clone()),
                    Transform::from_xyz(0.0, -40.0, 0.0),
                )
            ],
        ))
    // ...
;
}
}

And we'll need to enable the particle effect in the thrust system, and disable it in the thrust_stop system.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate avian2d;
extern crate bevy_enhanced_input;
extern crate bevy_enoki;
use avian2d::prelude::*;
use bevy::prelude::*;
use bevy_enoki::prelude::*;
use bevy_enhanced_input::prelude::*;
#[derive(Debug, InputAction)]
#[input_action(output = bool)]
struct Thrust;
fn thrust(
    trigger: Trigger<Fired<Thrust>>,
    mut player: Query<(&Transform, &mut LinearVelocity, &Children)>,
    mut visibility: Query<&mut Visibility>,
    mut particle_state: Query<&mut ParticleSpawnerState>,
) -> Result {
    let (transform, mut linear_velocity, children) = player.get_mut(trigger.target())?;
    linear_velocity.0 += transform.local_y().xy() * 2.0;
    linear_velocity.0 = linear_velocity.0.clamp_length_max(300.0);

    // Make jet sprite visible
    visibility
        .get_mut(children[0])?
        .set_if_neq(Visibility::Visible);

    // Make jet particles active
    particle_state
        .get_mut(children[1])?
        .map_unchanged(|s| &mut s.active)
        .set_if_neq(true);

    Ok(())
}
}

And similarly in the thrust_stop system:

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_enhanced_input;
extern crate bevy_enoki;
use bevy::prelude::*;
use bevy_enoki::prelude::*;
use bevy_enhanced_input::prelude::*;
#[derive(Debug, InputAction)]
#[input_action(output = bool)]
struct Thrust;
fn thrust_stop(
    trigger: Trigger<Completed<Thrust>>,
    player: Query<&Children>,
    mut visibility: Query<&mut Visibility>,
    mut particle_state: Query<&mut ParticleSpawnerState>,
) -> Result {
    let Ok(children) = player.get(trigger.target()) else {
        return Ok(());
    };

    // Make the jet sprite hidden
    visibility
        .get_mut(children[0])?
        .set_if_neq(Visibility::Hidden);

    // Make the jet particle inactive
    particle_state
        .get_mut(children[1])?
        .map_unchanged(|s| &mut s.active)
        .set_if_neq(false);

    Ok(())
}
}