Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add starts_immediately related methods #174

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,17 @@ cargo run --example spawn_on_command --features="bevy/bevy_winit bevy/bevy_pbr 3

![spawn_on_command](https://raw.githubusercontent.com/djeedai/bevy_hanabi/ffbf91be7f0780f8830869d14a64a79ca79baebb/examples/spawn_on_command.gif)

### Burst Over Time on Command

This example demonstrates how to emit a burst of particles over time when an event occurs. This gives total control of the spawning to the user code.

```shell
cargo run --example burst_over_time_on_command --features="bevy/bevy_winit bevy/bevy_pbr 3d"
```

![burst_over_time_on_command](https://raw.githubusercontent.com/zArubaru/bevy_hanabi/0cd82882cb6fdfe997bb9ba3aad54770deadb2dc/examples/burst_over_time_on_command.gif)


### Circle

This example demonstrates the `circle` spawner type, which emits particles along a circle perimeter or a disk surface. This allows for example simulating a dust ring around an object colliding with the ground.
Expand Down
Binary file added examples/burst_over_time_on_command.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
187 changes: 187 additions & 0 deletions examples/burst_over_time_on_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//! A circle bounces around in a box and spawns a trail of
//! particles when it hits the wall.
use bevy::{
log::LogPlugin,
math::Vec3Swizzles,
prelude::*,
render::{
camera::{Projection, ScalingMode},
render_resource::WgpuFeatures,
settings::WgpuSettings,
RenderPlugin,
},
};
use bevy_inspector_egui::quick::WorldInspectorPlugin;

use bevy_hanabi::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut wgpu_settings = WgpuSettings::default();
wgpu_settings
.features
.set(WgpuFeatures::VERTEX_WRITABLE_STORAGE, true);

App::default()
.insert_resource(ClearColor(Color::DARK_GRAY))
.add_plugins(
DefaultPlugins
.set(LogPlugin {
level: bevy::log::Level::WARN,
filter: "bevy_hanabi=warn,spawn=trace".to_string(),
})
.set(RenderPlugin { wgpu_settings }),
)
.add_plugin(HanabiPlugin)
.add_system(bevy::window::close_on_esc)
.add_plugin(WorldInspectorPlugin::default())
.add_startup_system(setup)
.add_system(update)
.run();

Ok(())
}

#[derive(Component)]
struct Ball {
velocity: Vec2,
}

const BOX_SIZE: f32 = 2.0;
const BALL_RADIUS: f32 = 0.05;

fn setup(
mut commands: Commands,
mut effects: ResMut<Assets<EffectAsset>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let mut camera = Camera3dBundle::default();
let mut projection = OrthographicProjection::default();
projection.scaling_mode = ScalingMode::FixedVertical(2.);
projection.scale = 1.2;
camera.transform.translation.z = projection.far / 2.0;
camera.projection = Projection::Orthographic(projection);
commands.spawn(camera);

commands
.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Quad {
size: Vec2::splat(BOX_SIZE),
..Default::default()
})),
material: materials.add(StandardMaterial {
base_color: Color::BLACK,
unlit: true,
..Default::default()
}),
..Default::default()
})
.insert(Name::new("box"));

let ball = commands
.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
sectors: 32,
stacks: 2,
radius: BALL_RADIUS,
})),
material: materials.add(StandardMaterial {
base_color: Color::WHITE,
unlit: true,
..Default::default()
}),
..Default::default()
})
.insert(Ball {
velocity: Vec2::new(1.0, 2f32.sqrt()),
})
.insert(Name::new("ball"))
.id();

let spawner = Spawner::new(32.0.into(), 0.5.into(), std::f32::INFINITY.into())
.with_starts_immediately(false);
let effect = effects.add(
EffectAsset {
name: "Impact".into(),
capacity: 32768,
spawner,
..Default::default()
}
.with_property("my_color", graph::Value::Uint(0xFFFFFFFF))
.init(InitPositionSphereModifier {
center: Vec3::ZERO,
radius: BALL_RADIUS,
dimension: ShapeDimension::Surface,
})
.init(InitVelocitySphereModifier {
center: Vec3::ZERO,
speed: 0.1.into(),
})
.init(InitLifetimeModifier {
lifetime: 2.5_f32.into(),
})
.init(InitAttributeModifier {
attribute: Attribute::COLOR,
value: "my_color".into(),
})
.render(SizeOverLifetimeModifier {
gradient: Gradient::constant(Vec2::splat(0.025)),
}),
);

let particle_effect = commands
.spawn(ParticleEffectBundle {
effect: ParticleEffect::new(effect),
..Default::default()
})
.insert(Name::new("effect"))
.id();

commands.entity(ball).push_children(&[particle_effect]);
}

fn update(
mut balls: Query<(&mut Ball, &mut Transform, &Children)>,
mut compiled_effects: Query<&mut CompiledParticleEffect>,
mut effect_spawners: Query<&mut EffectSpawner>,
time: Res<Time>,
) {
const HALF_SIZE: f32 = BOX_SIZE / 2.0 - BALL_RADIUS;

let mut effect = compiled_effects.single_mut();

for (mut ball, mut transform, children) in balls.iter_mut() {
let mut pos = transform.translation.xy() + ball.velocity * time.delta_seconds();
let mut collision = false;

for (coord, vel_coord) in pos.as_mut().iter_mut().zip(ball.velocity.as_mut()) {
while *coord < -HALF_SIZE || *coord > HALF_SIZE {
if *coord < -HALF_SIZE {
*coord = 2.0 * -HALF_SIZE - *coord;
} else if *coord > HALF_SIZE {
*coord = 2.0 * HALF_SIZE - *coord;
}
*vel_coord *= -1.0;
collision = true;
}
}

transform.translation = pos.extend(transform.translation.z);

if collision {
// Pick a random particle color
let r = rand::random::<u8>();
let g = rand::random::<u8>();
let b = rand::random::<u8>();
let color = 0xFF000000u32 | (b as u32) << 16 | (g as u32) << 8 | (r as u32);
effect.set_property("my_color", color.into());

// Spawn the particles
children.iter().for_each(|child| {
if let Ok(mut spawner) = effect_spawners.get_mut(*child) {
spawner.reset();
}
});
}
}
}
45 changes: 39 additions & 6 deletions src/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,6 @@ impl Spawner {
/// // Spawn 32 particles in a burst once immediately on creation.
/// let spawner = Spawner::once(32.0.into(), true);
/// ```
///
/// [`reset()`]: crate::Spawner::reset
pub fn once(count: Value<f32>, spawn_immediately: bool) -> Self {
let mut spawner = Self::new(count, 0.0.into(), f32::INFINITY.into());
spawner.starts_immediately = spawn_immediately;
Expand Down Expand Up @@ -362,6 +360,31 @@ impl Spawner {
pub fn starts_active(&self) -> bool {
self.starts_active
}

/// Sets whether the spawner starts immediately when the effect is instantiated.
///
/// If `starts_immediately` is false, the effect will not start until
/// [`EffectSpawner::reset()`] is called.
pub fn with_starts_immediately(mut self, starts_immediately: bool) -> Self {
self.starts_immediately = starts_immediately;
self
}

/// Set whether the spawner starts immediately when the effect is instantiated.
///
/// If `starts_immediately` is false, the effect will not start until
/// [`EffectSpawner::reset()`] is called.
pub fn set_starts_immediately(&mut self, starts_immediately: bool) {
self.starts_immediately = starts_immediately;
}

/// Get whether the spawner starts immediately when the effect is instantiated.
///
/// If `starts_immediately` is false, the effect will not start until
/// [`EffectSpawner::reset()`] is called.
pub fn starts_immediately(&self) -> bool {
self.starts_immediately
}
}

/// Runtime component maintaining the state of the spawner for an effect.
Expand Down Expand Up @@ -407,10 +430,10 @@ impl EffectSpawner {
let spawner = *instance.spawner.as_ref().unwrap_or(&asset.spawner);
Self {
spawner,
time: if spawner.is_once() && !spawner.starts_immediately {
1. // anything > 0
} else {
time: if spawner.starts_immediately() {
0.
} else {
f32::INFINITY
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is nice (or not; see below) because it works with non-once spawners too. However this means there's now an implicit API contract which is that set_time(f32::INFINITY) is equivalent to / duplicates set_starts_immediately(false), which is both unexpected and undocumented. I liked the time-based approach better because this makes sense to users, and can be useful for precise control even beyond the current burst use case.

And on the fact it works for non-once spawners: thinking more about it I think that's a mistake, because this is redundant with the active state of the spawner itself. The once spawner is very special because once it emitted a burst it goes to "sleep" yet is still active, but the other spawners (anything with a finite period) should conceptually emit particles if active, and now with that infinite time trick they don't, and I think that makes things confusing for the user.

Actually, as I was writing, I'm now wondering if any of this is needed. If you want a burst-over-time effect you can simply call new() with non-zero count and time, but leave the period as infinite to get a non-repeating emission. There's no need to manipulate the time, is there?

},
curr_spawn_time: 0.,
limit: 0.,
Expand Down Expand Up @@ -442,6 +465,16 @@ impl EffectSpawner {
self.active
}

/// Set accumulated time since last spawn.
pub fn set_time(&mut self, time: f32) {
djeedai marked this conversation as resolved.
Show resolved Hide resolved
self.time = time;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add at least a debug check for robustness, because passing a negative time will likely break everything.

Suggested change
self.time = time;
debug_assert!(time >= 0.);
self.time = time;

}

/// Get accumulated time since last spawn.
pub fn get_time(&self) -> f32 {
self.time
}

/// Get the spawner configuration in use.
///
/// The effective [`Spawner`] used is either the override specified in the
Expand Down Expand Up @@ -482,7 +515,7 @@ impl EffectSpawner {
/// The integral number of particles to spawn this frame. Any fractional
/// remainder is saved for the next call.
pub fn tick(&mut self, mut dt: f32, rng: &mut Pcg32) -> u32 {
if !self.active {
if !self.active || self.time.is_infinite() {
self.spawn_count = 0;
return 0;
}
Expand Down