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

How to trigger an EffectSpawner multiple times per frame? #255

Open
paholg opened this issue Nov 20, 2023 · 5 comments
Open

How to trigger an EffectSpawner multiple times per frame? #255

paholg opened this issue Nov 20, 2023 · 5 comments
Labels
question Further information is requested

Comments

@paholg
Copy link

paholg commented Nov 20, 2023

First of all, thanks a lot for this library, I've been having a lot of fun with it!

I think the spawn_on_command example is a good showcase of what I'm after -- it has a ball bouncing, and we spawn particles every time it hits a wall. But what if we had a bunch of balls bouncing, and wanted to spawn particles every time any of them hit something?

This works fine, unless two of them happen to hit a wall in the same frame, then we end up only spawning particles for one of them. One idea is to have a sort of "spawner pool", where we keep multiple copies of an EffectSpawner around, and create a new one any time we want to spawn particles more times in a frame than we have before.

But this seems non-ideal, and I'd probably end up wrapping effect I'm using in it, so I thought I'd reach out and see if you have any better ideas.

@djeedai djeedai added the question Further information is requested label Nov 26, 2023
@djeedai
Copy link
Owner

djeedai commented Nov 26, 2023

This is an interesting one. I thought about it a bit. I think the proposed approach of having a pool of ParticleEffect (not spawners) is pretty reasonable. Keep the pool to the size of the maximum number of concurrent effects you want to run, and dynamically parent them to the right entity when needed. Or, alternatively, if the frequency of effects is high and the number of object is low, reparenting might be expensive, so having one particle effect instance per entity that is deactivated/reactivated might be a better approach.

The problem you will run into if you try to have two or more different emission sources in the same frame and you want to use a single particle effect is that the emitter's Transform determine the position of spawning, but each entity has a single Transform. So then you're into the business of, say, spawning 30 particles at location A and 30 others at location B from the same effect, which is doable probably but very complex (need to pass the transforms of A and B via properties, as well as any other spawning parameter since there's only one spawner/emitter per instance). I doubt I can even find a use case where that approach is optimal; most likely using multiple instances is better.

@paholg
Copy link
Author

paholg commented Nov 28, 2023

Here is my quick and dirty solution, that seems to work well enough:

use bevy::prelude::{Commands, Entity, Query, Transform};
use bevy_hanabi::{EffectSpawner, ParticleEffectBundle};

/// A wrapper around `ParticleEffectBundle` that allows spawning multiple copies
/// of the same effect in the same frame.
///
/// Some caveats:
/// 1. It is expected that the effect's spawner has `starts_immediately: true`.
///    This is left to the caller to verify.
/// 2. `ParticleEffectPool::reset` should be called once per frame, before any
///    calls to `ParticleEffectPool::trigger`. Failing to do so will result in a
///    memory leak.
pub struct ParticleEffectPool {
    bundle: ParticleEffectBundle,
    effects: Vec<Entity>,
    index: usize,
}

impl Clone for ParticleEffectPool {
    fn clone(&self) -> Self {
        Self::new(self.bundle.clone())
    }
}

impl From<ParticleEffectBundle> for ParticleEffectPool {
    fn from(value: ParticleEffectBundle) -> Self {
        Self::new(value)
    }
}

impl ParticleEffectPool {
    pub fn new(bundle: ParticleEffectBundle) -> Self {
        Self {
            bundle,
            effects: vec![],
            index: 0,
        }
    }

    pub fn reset(&mut self) {
        self.index = 0;
    }

    pub fn trigger(
        &mut self,
        commands: &mut Commands,
        transform: Transform,
        effects: &mut Query<(&mut Transform, &mut EffectSpawner)>,
    ) {
        if self.index < self.effects.len() {
            let entity = self.effects[self.index];
            self.index += 1;
            if let Ok((mut effect_transform, mut effect_spawner)) = effects.get_mut(entity) {
                *effect_transform = transform;
                effect_spawner.reset();
            } else {
                tracing::warn!("Missing effect");
            }
        } else {
            let mut bundle = self.bundle.clone();
            bundle.transform = transform;
            let entity = commands.spawn(bundle).id();
            self.effects.push(entity);
            self.index += 1;
        }
    }
}

Is some variant of this something that you would like to see in bevy_hanabi? If not, I'll happily close this issue.

@Reiuji-ch
Copy link

Over in the love2d world, we have this function called ParticleSystem:emit, which just takes in how many particles you want to spawn and immediately creates them, regardless of spawner/emitter settings.
Would it be possible to add something like that to bevy_hanabi?

I have a use-case where I want to emit bursts of particles, e.g. using a Spawner::once and calling reset(), but that obviously doesn't work if it needs to do multiple bursts per tick. I'd like to use just a single ParticleEffect, changing it's transform and calling emit whenever I need a burst, instead of managing a pool and having to keep track of which instances are currently in use.
I also think an emit function would be slightly more ergonomic for controlling the number of particles programmatically, for example if I want to spawn 1 particle per damage taken, I'd just do emit(damage_taken) instead of having to create/update a Spawner every time.

@djeedai
Copy link
Owner

djeedai commented Dec 22, 2023

@Reiuji-ch thanks for the details. It's interesting to compare what other libraries are doing. I'm assuming love2d is CPU-based though, no? Because that would be a lot easier to implement with a CPU based particle solution.

The problem with that approach in Hanabi / Bevy is that this doesn't map well at all with a buffered architecture (main and render apps) of Bevy, and the CPU/GPU duality. Here's what happens:

  • you move to A
  • you emit N particles
  • you move to B
  • you emit M particles
  • the main World update finishes, the render sub-app starts and gathers rendering data
    • the position is at B
    • the emit count is M+N

Game over. The emitter emits a single burst of N+M particles from point B alone. There's no way to even observe position A in the first place, because the Transform is only read when the render world extracts data at the end of the main frame.

For that approach to work, you also need an array of transforms per effect. And having variable-size arrays for GPU is horrible to manage in an efficient way. That the first issue (technical complexity).

The second issue is that I'd argue this goes against the very design of an emitter. If you really want to spawn 2 bursts of particles at 2 separate locations in the same frame, then you have 2 particles emitters here.

I'm not saying the use case is invalid. I'm totally open to suggestions on things we can improve to make things easier to handle. But handling multiple emissions per frame per effect sounds like a lot of added complexity of a use case which is not that much common.

@Reiuji-ch
Copy link

@djeedai I figured there was something like that which complicated it a lot. I agree that it adds unnecessary complexity.

I suppose a simple hack for my use-case would be something like this:

commands.spawn((ParticleEffectBundle {
    effect: ParticleEffect::new(effect_asset_handle_resource.0.clone())
        .with_spawner(Spawner::once(100.0.into(), true)),
     transform: transform.clone(),
     ..default()
}, GarbageCollectInSeconds(5.0)));

Just spawn an ephemeral effect at every location I want to emit a burst, then have a system despawn them once all the particles have timed out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants