Skip to content

Commit

Permalink
Unify the clone modifier and spawners, and fix races.
Browse files Browse the repository at this point in the history
This large patch essentially makes particle trails and ribbons part of
the spawner, which is processed during the init phase, rather than
modifiers that execute during the update phase. Along the way, this made
it easier to fix race conditions in spawning of trails, because spawning
only happens in the init phase while despawning only happens in the
update phase. This addresses #376, as well as underflow bugs that could
occur in certain circumstances.

In detail, this commit makes the following changes:

* Every group now has an *initializer*. An initializer can either be a
  *spawner* or a *cloner*. This allows spawners to spawn into any group,
  not just the first one.

* The `EffectSpawner` component is now `EffectInitializers`, a component
  which contains the initializers for every group. Existing code that
  uses `EffectSpawner` can migrate by picking the first
  `EffectInitializer` from that component.

* The `CloneModifier` has been removed. Instead, use a `Cloner`, which
  manages the `age` and `lifetime` attributes automatically to avoid
  artifacts. The easiest way to create a cloner is to call `with_trails`
  or `with_ribbons` on your `EffectAsset`.

* The `RibbonModifier` has been removed. Instead, at most one of the
  groups may be delegated the ribbon group. The easiest way to delegate
  a ribbon group is to call `EffectAsset::with_ribbons`. (It may seem
  like a loss of functionality to only support one ribbon group, but it
  actually isn't, because there was only one `prev` and `next` attribute
  pair and so multiple ribbons never actually worked before.)

* The `capacity` parameter in `EffectAsset::new` is no longer a vector
  of capacities. Instead, you supply the capacity of each group as you
  create it. I figured this was cleaner.

* Init modifiers can now be specific to a particle group, and they
  execute for cloned particles as well. There's no need to use update
  modifiers to set values on cloned particles.

* Age and lifetime can no longer be set manually for cloned particles.
  (They can still be set as usual for spawned particles.) This enforces
  LIFO ordering for ribbons, which is necessary to avoid races.

* Underflow of particle counts that could occur in certain scenarios
  involving multiple groups is fixed. The racy hack that "put back"
  particles that would otherwise underflow the alive count is no longer
  needed.

* Particle linked lists no longer race with one another (#376).

Closes #376.
  • Loading branch information
pcwalton committed Oct 23, 2024
1 parent 75f07d7 commit 899f1ab
Show file tree
Hide file tree
Showing 40 changed files with 1,877 additions and 1,197 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ fn setup(mut effects: ResMut<Assets<EffectAsset>>) {
// Create the effect asset
let effect = EffectAsset::new(
// Maximum number of particles alive at a time
vec![32768],
32768,
// Spawn at a rate of 5 particles per second
Spawner::rate(5.0.into()),
// Move the expression module into the asset
Expand Down
2 changes: 1 addition & 1 deletion examples/2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ fn setup(
// By default the asset spawns the particles at Z=0.
let spawner = Spawner::rate(30.0.into());
let effect = effects.add(
EffectAsset::new(vec![4096], spawner, module)
EffectAsset::new(4096, spawner, module)
.with_name("2d")
.init(init_pos)
.init(init_vel)
Expand Down
4 changes: 2 additions & 2 deletions examples/activate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ fn setup(
let round = RoundModifier::constant(&mut module, 1.0);

let effect = effects.add(
EffectAsset::new(vec![32768], spawner, module)
EffectAsset::new(32768, spawner, module)
.with_name("activate")
.init(init_pos)
.init(init_vel)
Expand Down Expand Up @@ -159,7 +159,7 @@ fn setup(

fn update(
mut q_balls: Query<(&mut Ball, &mut Transform, &Children)>,
mut q_spawner: Query<&mut EffectSpawner>,
mut q_spawner: Query<&mut EffectInitializers>,
mut q_text: Query<&mut Text, With<StatusText>>,
time: Res<Time>,
) {
Expand Down
2 changes: 1 addition & 1 deletion examples/billboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ fn setup(
module.add_texture("color");

let effect = effects.add(
EffectAsset::new(vec![32768], Spawner::rate(64.0.into()), module)
EffectAsset::new(32768, Spawner::rate(64.0.into()), module)
.with_name("billboard")
.with_alpha_mode(bevy_hanabi::AlphaMode::Mask(alpha_cutoff))
.init(init_pos)
Expand Down
2 changes: 1 addition & 1 deletion examples/circle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ fn setup(
module.add_texture("color");

let effect = effects.add(
EffectAsset::new(vec![32768], Spawner::burst(32.0.into(), 8.0.into()), module)
EffectAsset::new(32768, Spawner::burst(32.0.into(), 8.0.into()), module)
.with_name("circle")
.init(init_pos)
.init(init_vel)
Expand Down
2 changes: 1 addition & 1 deletion examples/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
};

let effect = effects.add(
EffectAsset::new(vec![32768], Spawner::rate(500.0.into()), writer.finish())
EffectAsset::new(32768, Spawner::rate(500.0.into()), writer.finish())
.with_name("whirlwind")
.init(init_pos)
.init(init_age)
Expand Down
34 changes: 12 additions & 22 deletions examples/firework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
//! - An [`AccelModifier`] to pull particles down once they slow down, for
//! increased realism. This is a subtle effect, but of importance.
//!
//! The particles also have a trail, created with the [`CloneModifier`]. The
//! trail particles are stitched together to form an arc using the
//! [`RibbonModifier`].
//! The particles also have a trail. The trail particles are stitched together
//! to form an arc using [`EffectAsset::with_ribbons`].
use bevy::{
core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping},
Expand Down Expand Up @@ -74,10 +73,6 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
let lifetime = writer.lit(0.8).normal(writer.lit(1.2)).expr();
let init_lifetime = SetAttributeModifier::new(Attribute::LIFETIME, lifetime);

// Lifetime for trails
let init_lifetime_trails =
SetAttributeModifier::new(Attribute::LIFETIME, writer.lit(0.2).expr());

// Add constant downward acceleration to simulate gravity
let accel = writer.lit(Vec3::Y * -16.).expr();
let update_accel = AccelModifier::new(accel);
Expand Down Expand Up @@ -108,23 +103,21 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {

let effect = EffectAsset::new(
// 2k lead particles, with 32 trail particles each
vec![2048, 2048 * 32],
2048,
Spawner::burst(2048.0.into(), 2.0.into()),
writer.finish(),
)
// Tie together trail particles to make arcs. This way we don't need a lot of them, yet there's
// a continuity between them.
.with_ribbons(2048 * 32, 1.0 / 64.0, 0.2, 0)
.with_name("firework")
.init(init_pos)
.init(init_vel)
.init(init_age)
.init(init_lifetime)
.update_groups(CloneModifier::new(1.0 / 64.0, 1), lead)
.init_groups(init_pos, lead)
.init_groups(init_vel, lead)
.init_groups(init_age, lead)
.init_groups(init_lifetime, lead)
.init_groups(init_vel_trail, trail)
.update_groups(update_drag, lead)
.update_groups(update_accel, lead)
// Currently the init pass doesn't run on cloned particles, so we have to use an update modifier
// to init the lifetime of trails. This will overwrite the value each frame, so can only be used
// for constant values.
.update_groups(init_lifetime_trails, trail)
.update_groups(init_vel_trail, trail)
.render_groups(
ColorOverLifetimeModifier {
gradient: color_gradient1.clone(),
Expand All @@ -150,10 +143,7 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
screen_space_size: false,
},
trail,
)
// Tie together trail particles to make arcs. This way we don't need a lot of them, yet there's
// a continuity between them.
.render_groups(RibbonModifier, trail);
);

let effect1 = effects.add(effect);

Expand Down
8 changes: 4 additions & 4 deletions examples/force_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ fn setup(

// Force field effects
let effect = effects.add(
EffectAsset::new(vec![32768], spawner, writer.finish())
EffectAsset::new(32768, spawner, writer.finish())
.with_name("force_field")
.init(init_pos)
.init(init_vel)
Expand All @@ -329,15 +329,15 @@ fn setup(
}

fn spawn_on_click(
mut q_effect: Query<(&mut EffectSpawner, &mut Transform), Without<Projection>>,
mut q_effect: Query<(&mut EffectInitializers, &mut Transform), Without<Projection>>,
mouse_button_input: Res<ButtonInput<MouseButton>>,
camera_query: Query<(&Camera, &GlobalTransform), With<Projection>>,
window: Query<&Window, With<bevy::window::PrimaryWindow>>,
) {
// Note: On first frame where the effect spawns, EffectSpawner is spawned during
// CoreSet::PostUpdate, so will not be available yet. Ignore for a frame if
// so.
let Ok((mut spawner, mut effect_transform)) = q_effect.get_single_mut() else {
let Ok((mut initializers, mut effect_transform)) = q_effect.get_single_mut() else {
return;
};

Expand All @@ -354,7 +354,7 @@ fn spawn_on_click(
effect_transform.translation = spawning_pos;

// Spawn a single burst of particles
spawner.reset();
initializers.reset();
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn setup(
module.add_texture("color");

let effect = effects.add(
EffectAsset::new(vec![32768], Spawner::rate(1000.0.into()), module)
EffectAsset::new(32768, Spawner::rate(1000.0.into()), module)
.with_name("gradient")
.init(init_pos)
.init(init_vel)
Expand Down
22 changes: 9 additions & 13 deletions examples/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,15 @@ where

let init = make_modifier(&writer);

EffectAsset::new(
vec![32768],
Spawner::once(COUNT.into(), true),
writer.finish(),
)
.with_name(name)
.with_simulation_space(SimulationSpace::Local)
.init(init)
.render(OrientModifier::new(OrientMode::FaceCameraPosition))
.render(SetColorModifier {
color: COLOR.into(),
})
.render(SetSizeModifier { size: SIZE.into() })
EffectAsset::new(32768, Spawner::once(COUNT.into(), true), writer.finish())
.with_name(name)
.with_simulation_space(SimulationSpace::Local)
.init(init)
.render(OrientModifier::new(OrientMode::FaceCameraPosition))
.render(SetColorModifier {
color: COLOR.into(),
})
.render(SetSizeModifier { size: SIZE.into() })
}

fn spawn_effect(
Expand Down
4 changes: 2 additions & 2 deletions examples/instancing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ fn setup(
};

let effect = effects.add(
EffectAsset::new(vec![512], Spawner::rate(50.0.into()), writer.finish())
EffectAsset::new(512, Spawner::rate(50.0.into()), writer.finish())
.with_name("instancing")
.init(init_pos)
.init(init_vel)
Expand Down Expand Up @@ -288,7 +288,7 @@ fn setup(
module.add_texture("color");

let alt_effect = effects.add(
EffectAsset::new(vec![512], Spawner::rate(102.0.into()), module)
EffectAsset::new(512, Spawner::rate(102.0.into()), module)
.with_simulation_space(SimulationSpace::Local)
.with_name("alternate instancing")
.init(init_pos)
Expand Down
6 changes: 3 additions & 3 deletions examples/lifetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ fn setup(
};
let effect1 = effects.add(
EffectAsset::new(
vec![512],
512,
Spawner::burst(50.0.into(), period.into()),
writer1.finish(),
)
Expand Down Expand Up @@ -136,7 +136,7 @@ fn setup(
};
let effect2 = effects.add(
EffectAsset::new(
vec![512],
512,
Spawner::burst(50.0.into(), period.into()),
writer2.finish(),
)
Expand Down Expand Up @@ -191,7 +191,7 @@ fn setup(
};
let effect3 = effects.add(
EffectAsset::new(
vec![512],
512,
Spawner::burst(50.0.into(), period.into()),
writer3.finish(),
)
Expand Down
2 changes: 1 addition & 1 deletion examples/multicam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ fn make_effect(color: Color) -> EffectAsset {
speed: writer.lit(6.).expr(),
};

EffectAsset::new(vec![32768], Spawner::rate(5.0.into()), writer.finish())
EffectAsset::new(32768, Spawner::rate(5.0.into()), writer.finish())
.with_name("effect")
.init(init_pos)
.init(init_vel)
Expand Down
2 changes: 1 addition & 1 deletion examples/ordering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn make_firework() -> EffectAsset {
speed: (writer.rand(ScalarType::Float) * writer.lit(20.) + writer.lit(60.)).expr(),
};

EffectAsset::new(vec![2048], Spawner::rate(128.0.into()), writer.finish())
EffectAsset::new(2048, Spawner::rate(128.0.into()), writer.finish())
.with_name("firework")
.init(init_pos)
.init(init_vel)
Expand Down
2 changes: 1 addition & 1 deletion examples/portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
let tangent_accel = TangentAccelModifier::constant(&mut module, Vec3::ZERO, Vec3::Z, 30.);

let effect1 = effects.add(
EffectAsset::new(vec![16384, 16384], Spawner::rate(5000.0.into()), module)
EffectAsset::new(16384, Spawner::rate(5000.0.into()), module)
.with_name("portal")
.init(init_pos)
.init(init_age)
Expand Down
2 changes: 1 addition & 1 deletion examples/random.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ fn setup(

let effect = effects.add(
EffectAsset::new(
vec![32768],
32768,
Spawner::burst(CpuValue::Uniform((1., 100.)), CpuValue::Uniform((1., 4.))),
writer.finish(),
)
Expand Down
35 changes: 11 additions & 24 deletions examples/ribbon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
value: writer.lit(0.5).expr(),
};

let clone_modifier = CloneModifier::new(1.0 / TRAIL_SPAWN_RATE, 1);

let time = writer.time().mul(writer.lit(TIME_SCALE));

let move_modifier = SetAttributeModifier {
Expand All @@ -87,32 +85,21 @@ fn setup(mut commands: Commands, mut effects: ResMut<Assets<EffectAsset>>) {
.expr(),
};

let update_lifetime_attr = SetAttributeModifier {
attribute: Attribute::LIFETIME,
value: writer.lit(LIFETIME).expr(),
};

let render_color = ColorOverLifetimeModifier {
gradient: Gradient::linear(vec4(3.0, 0.0, 0.0, 1.0), vec4(3.0, 0.0, 0.0, 0.0)),
};

let effect = EffectAsset::new(
vec![256, 32768],
Spawner::once(1.0.into(), true),
writer.finish(),
)
.with_name("ribbon")
.with_simulation_space(SimulationSpace::Global)
.init(init_position_attr)
.init(init_velocity_attr)
.init(init_age_attr)
.init(init_lifetime_attr)
.init(init_size_attr)
.update_groups(move_modifier, ParticleGroupSet::single(0))
.update_groups(clone_modifier, ParticleGroupSet::single(0))
.update_groups(update_lifetime_attr, ParticleGroupSet::single(1))
.render(RibbonModifier)
.render_groups(render_color, ParticleGroupSet::single(1));
let effect = EffectAsset::new(256, Spawner::once(1.0.into(), true), writer.finish())
.with_ribbons(32768, 1.0 / TRAIL_SPAWN_RATE, LIFETIME, 0)
.with_name("ribbon")
.with_simulation_space(SimulationSpace::Global)
.init_groups(init_position_attr, ParticleGroupSet::single(0))
.init_groups(init_velocity_attr, ParticleGroupSet::single(0))
.init_groups(init_age_attr, ParticleGroupSet::single(0))
.init_groups(init_lifetime_attr, ParticleGroupSet::single(0))
.init_groups(init_size_attr, ParticleGroupSet::single(0))
.update_groups(move_modifier, ParticleGroupSet::single(0))
.render_groups(render_color, ParticleGroupSet::single(1));

let effect = effects.add(effect);

Expand Down
26 changes: 11 additions & 15 deletions examples/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ fn setup(
};

let effect1 = effects.add(
EffectAsset::new(vec![32768], Spawner::rate(500.0.into()), writer1.finish())
EffectAsset::new(32768, Spawner::rate(500.0.into()), writer1.finish())
.with_name("emit:rate")
.init(init_pos1)
// Make spawned particles move away from the emitter origin
Expand Down Expand Up @@ -163,19 +163,15 @@ fn setup(
speed: writer2.lit(2.).expr(),
};
let effect2 = effects.add(
EffectAsset::new(
vec![32768],
Spawner::once(1000.0.into(), true),
writer2.finish(),
)
.with_name("emit:once")
.init(init_pos2)
.init(init_vel2)
.init(init_age2)
.init(init_lifetime2)
.render(ColorOverLifetimeModifier {
gradient: gradient2,
}),
EffectAsset::new(32768, Spawner::once(1000.0.into(), true), writer2.finish())
.with_name("emit:once")
.init(init_pos2)
.init(init_vel2)
.init(init_age2)
.init(init_lifetime2)
.render(ColorOverLifetimeModifier {
gradient: gradient2,
}),
);

commands
Expand Down Expand Up @@ -236,7 +232,7 @@ fn setup(

let effect3 = effects.add(
EffectAsset::new(
vec![32768],
32768,
Spawner::burst(400.0.into(), 3.0.into()),
writer3.finish(),
)
Expand Down
Loading

0 comments on commit 899f1ab

Please sign in to comment.