-
Notifications
You must be signed in to change notification settings - Fork 78
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
zArubaru
wants to merge
7
commits into
djeedai:main
Choose a base branch
from
zArubaru:feature/spawner-time-manipulation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a070546
Add `with_time`, `set_time`, and `get_time` functions to `Spawner`
zArubaru 9373d32
Add example for bursting over time on command
zArubaru e1a3246
Add 'Burst Over Time on Command' to README.md
zArubaru 084575f
Add a more distinct example for Burst Over Time on Command
zArubaru 283b1ea
Update image for Burst Over Time on Command
zArubaru b0973ff
Remove extra doc line.
zArubaru a3ac6ea
Add `starts_immediately` related methods to `Spawner`.
zArubaru File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -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; | ||||||||
|
@@ -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. | ||||||||
|
@@ -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 | ||||||||
}, | ||||||||
curr_spawn_time: 0., | ||||||||
limit: 0., | ||||||||
|
@@ -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; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||
} | ||||||||
|
||||||||
/// 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 | ||||||||
|
@@ -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; | ||||||||
} | ||||||||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 thatset_time(f32::INFINITY)
is equivalent to / duplicatesset_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 theactive
state of the spawner itself. Theonce
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 finiteperiod
) 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-zerocount
andtime
, but leave theperiod
as infinite to get a non-repeating emission. There's no need to manipulate the time, is there?