pixijs / particle-emitter

A particle system for PixiJS
http://pixijs.io/particle-emitter/docs
MIT License
786 stars 124 forks source link

Emitter that spawns on-demand rather than by frequency + timer #131

Closed MxAshUp closed 4 years ago

MxAshUp commented 4 years ago

Background

For my project I'd like to emit a single particle or a burst of particles by calling a function (let's name spawn()) on Emitter. An example might be: spawn 1 particle each time click is fired.

Approaches

Create Emitter for Each Fire (Bad)

container.on('click', () => {
  new Emitter(
    container,
    [sprite], {
      //...
      "lifetime": {
        "min": 3,
        "max": 3
      },
      "frequency": 0.01,
      "particlesPerWave": 1,
      "emitterLifetime": 1,
    } 
  );
  // ...
});

This method is clunky, and requires a new emitter for every click. Not very efficient. It also does not guarantee your particle will be spawn as soon as the emitter is created. It will need to wait for the next update().

Changing Emitter.emit Value (Okay)

This approach doesn't require any new classes, only uses one emitter, but is still clunky. As far as I can tell it does not guarantee when your particle will spawn. It still needs to wait until the next ticker update.

const myEmitter =  new Emitter(
    container,
    [sprite], {
      //...
    } 
);

myEmmiter.emit = false;

container.on('click', () => {
  myEmitter.emit = true;
  // This next part s tricky, how do we ensure emit is set to false as soon as the next wave has spawn?
  setTimeout(() => {
    myEmitter.emit = false;
  }, 100);
});

Adding a Spawn() Method to an Emitter Subclass (Good)

Another approach is the create a new kind of emitter with a spawn() method.

class OnDemandEmitter extends Emitter {
  update(delta: number) {
    // Does everything in super.update except spawning
    // ...
  }
  spawn(timePassed: number = 0) {
    // This looks just like the spawn code in super.update()
    // timePassed is to indicate how many seconds have passed since the particle *should have* been spawned
    // ...
  }
}

const myEmitter =  new OnDemandEmitter(
    container,
    [sprite], {
      //...
    } 
);

container.on('click', () => {
  myEmitter.spawn();
});

This is the approach I'm using in my project. What's nice is I can use all the features in Emitter, but without the spawn timer.

Changes to pixi-particles

I'd like to open this up for discussion: 1) Is there already a best practice to spawn particles on-demand with native Emitters? 2) Does on-demand spawning functionality belong in pixi-particles? 3) If so, should it be implemented in Emitter, or as a subclass?

themoonrat commented 4 years ago

There is an 'emit' flag that you can set to true or false on an emitter. Is this not what you require? So the flag gets swapped on each click

MxAshUp commented 4 years ago

@themoonrat - oh that was the other approach I meant to mention. I've now included a code sample for this above.

I did try this method with some success, but it wasn't great. I could easily set emit to true when click is fired, but it was difficult setting emit back to false in the correct time window. The sample code above was my approach (using setTimeout), but based on my frequency parameters, it only sometimes resulted in particles being spawned, and sometimes too many being spawned.

It also suffers from another problem: spawn timing is still handled by the spawn timer. So it was still impossible to make the particle(s) spawn the exact moment click fired.

I this method could work if spawning could be guaranteed next update() AND emit could be set to false guaranteed after next update(). EDIT: I just realized this could be done by using a custom timer (ie autoupdate = false) for the emitter in a hacky way. I'll give it a go!

andrewstart commented 4 years ago

Hmm, this is somewhat tricky, because there are several potential approaches to wrap existing code, but each has their own problems. The method I would currently go with would be using playOnce() with an extremely short emitter life and an identical frequency, to only emit one (wave of) particle(s) on the next frame. It wouldn't work great with multiple clicks per frame, but so long as your users are being reasonable would work fine. If you need to handle multiple clicks per frame, you could, upon click, set emit to true, call update(<your frequency>), and then set emit back to false, and use a frequency of some stupid small number like 0.00001 so that these updates won't affect normal updates with deltas of ~0.016 seconds.

MxAshUp commented 4 years ago

@andrewstart thanks for suggestion. I did a few hours of reading code to finally wrap my head around _spawnTimer and the entire update function. The whole thing is designed really well - the interpolation between emitter positions is a nice detail, and it looks like _spawnTimer is carried to the next run, ensuring frequency is super accurate.

All that being said, I realized how Emitter is specifically designed for time-based spawning. Even looking at other engines, the approach is similar.

It got me thinking that maybe there's a better approach that's more cohesive with the existing Emitter: Perhaps an Emitter option for custom timing (as opposed to using the frequency option). I think this would make spawning on-demand simpler. Furthermore, a custom timing option could be used for other cool stuff like dynamically changing spawn frequency.

I think it's safe to close this issue. Thanks for helping me think through the options. If I come up with any meaningful code during my project that might be worth contributing, I'll open another ticket.