CAEmitterLayer how to emit for a short time repeatedly

12k views Asked by At

I'm playing around with CAEmitterLayer and I face some problems now :(

I need a short particle effect - like a hit or explosion - at one place for example (so I have small UIView at this place). How should I do that?

1, I had an idea - create the emitterLayer with it's particles and set the lifeTime to 0. And when I need it I set the lifeTime to 1 for example and after awhile I can set it back to 0. - BUT it's not doing anything :(

2, The second idea was to create [CAEmitterLayer layer] every time I need it and add it as a layers sublayer. But I'm thinking what happen when I repeat it for example ten times… I have 10 sublayers with one acive and 9 "dead"? How to stop emitting in general? I have performSelector after some time to set the lifetime to 0 and other selector with longer interval to removeFromSuperlayer… But it's not so pretty as I would like to have it :( is there another "proper" way?

I think with too many sublayers is related my other problem… I want to emit just one particle. And when I do it it works. But SOMETIMES it emit three particles, sometimes two… And it makes me mad about that. When I don't stop emitter it's giving every time the correct number of particles...

So the questions…

how to emit particles for a short time. how to work with them - like stop, remove from parent layer, … how to emit just exact number of particles

EDIT:

emitter = [CAEmitterLayer layer];
emitter.emitterPosition = CGPointMake(self.view.bounds.size.width/2, self.view.bounds.size.height/2);
emitter.emitterMode = kCAEmitterLayerPoints;
emitter.emitterShape    = kCAEmitterLayerPoint;
emitter.renderMode      = kCAEmitterLayerOldestFirst;
emitter.lifetime = 0;


particle = [CAEmitterCell emitterCell];
[particle setName:@"hit"];
particle.birthRate      = 1;
particle.emissionLongitude  = 3*M_PI_2;//270deg
particle.lifetime       = 0.75;
particle.lifetimeRange      = 0;
particle.velocity       = 110;
particle.velocityRange      = 20;
particle.emissionRange      = M_PI_2;//PI/2 = 90degrees
particle.yAcceleration      = 200;
particle.contents       = (id) [[UIImage imageNamed:@"50"] CGImage];
particle.scale          = 1.0;
particle.scaleSpeed     = -0.5;
particle.alphaSpeed     = -1.0;

emitter.emitterCells = [NSArray arrayWithObject:particle];
[(CAEmitterLayer *)self.view.layer addSublayer: emitter];

Then in method linked with button I do this:

emitter.lifetime = 1.0;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.9 * NSEC_PER_SEC), dispatch_get_current_queue(), ^{
    emitter.lifetime = 0;
});

EDITED and UPDATED after changing to @David Rönnqvist attitude

CAEmitterCell *dustCell = [CAEmitterCell emitterCell];
[dustCell setBirthRate:1];
[dustCell setLifetime:1.5];
[dustCell setName:@"dust"];
[dustCell setContents:(id) [[UIImage imageNamed:@"smoke"] CGImage]];
[dustCell setVelocity:50];
[dustCell setEmissionRange:M_PI];
// Various configurations for the appearance...
// This is the only cell with configured scale, 
// color, content, emissionLongitude, etc...

[emitter setEmitterCells:[NSArray arrayWithObject:dustCell]];
[(CAEmitterLayer *)self.view.layer addSublayer:emitter];

// After one burst, change the birth rate of the cloud to 0
// so that there is only one burst per side.
double delayInSeconds = 0.5; // One cloud will have been created by now, but not two
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {

    [emitter setLifetime:0.0];
    [emitter setValue:[NSNumber numberWithFloat:0.0] 
               forKeyPath:@"emitterCells.dust.birthRate"];
});
6

There are 6 answers

0
D33 On

OK, finally, after hours of testing and trying very different styles (initializing, removing, configuring emitters) I came up with the final result... And actually it makes me very upset...

---It is not possible!---

Even when I create emitter and its particles everytime I need it, if I set only one particle to emit, it gives me most of time one particle... BUT it is not 100% and sometimes it just emitts three particles, or two... It's random. And that is very bad. Because it is visual effect... :(

Either way if someone has a tip how to solve this, please let me know...

1
thomax On

Thanks to foundrys excellent answer over here I solved a problem very similar to this. It does not involve hiding any views. Briefly, it goes like this:

Set up your emitter as you would normally, name the emitter cells and give them a birthrate value of zero:

-(void) setUpEmission {
  # ...
  # snip lots of config
  # ...
  [emitterCell1 setBirthrate:0];
  [emitterCell1 setName:@"emitter1"];
  [emitterCell2 setBirthrate:0];
  [emitterCell2 setName:@"emitter2"];
  emitterLayer.emitterCells = @[emitterCell1, emitterCell2];
  [self.view.layer addSublayer:emitterLayer];
}

Then create a start method which automatically turns off the emission after a short while, and a stop method:

-(void) startEmission {
  [emitterLayer setValue:@600 forKeyPath:@"emitterCells.emitter1.birthRate"];
  [emitterLayer setValue:@250 forKeyPath:@"emitterCells.emitter2.birthRate"];
  [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(stopEmission) userInfo:nil repeats:NO];
}

-(void) stopEmission {
  [emitterLayer setValue:@0 forKeyPath:@"emitterCells.emitter1.birthRate"];
  [emitterLayer setValue:@0 forKeyPath:@"emitterCells.emitter2.birthRate"];
}

In this example I've set birthrates to 600 and 250. And the timer shuts off emission after 0.2 seconds, but use whatever you see fit.

The optimal solution would be if Apple had implemented start/stop methods, but short of that I find this a satisfactory solution.

0
user396030 On

Have you looked into taking advantage of the animatable properties of CALayers?

  func setEmitterProperties() {

    backgroundColor = UIColor.clearColor().CGColor
    birthRate = kStandardBirthRate
    emitterShape = kCAEmitterLayerLine
    emitterCells = [typeOneCell, typeTwoCell, typeOneCell]
    preservesDepth = false

    let birthRateDecayAnimation = CABasicAnimation()
    birthRateDecayAnimation.removedOnCompletion = true
    birthRateDecayAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
    birthRateDecayAnimation.duration = CFTimeInterval(kStandardAnimationDuration)
    birthRateDecayAnimation.fromValue = NSNumber(float: birthRate)
    birthRateDecayAnimation.toValue = NSNumber(float: 0)
    birthRateDecayAnimation.keyPath = kBirthRateDecayAnimationKey
    birthRateDecayAnimation.delegate = self

  }
  1. The delegate property could also be nil if you don't want to do anything on completion, as in animationDidStop:finished:

  2. The constants kBirthRateDecayAnimationKey & kStandardAnimationDuration use my convention, not Apple's.

0
shannoga On

I was looking for a solution too and found this article.

look at this Gist for the confetti particle, and its Stop Emitting method.

What it does is :

  1. Add the particle view to your display view.
  2. Let the particle run as long as you want.
  3. stop the emission of new particles with confettiEmitter.birthRate = 0.0;.
  4. wait few seconds.
  5. remove the particle view.

Hope it can help.

16
David Rönnqvist On

You can do this by configuring everything once (don't add a new emitter cell every time) and setting the birthRate to 0 (no particles will be created). Then when you want to create your particles you can set the birthRate to the number of particles per second that you want to create. After a certain time you set the birthRate back to 0 so that the emission stops. You could use something like dispatch_after() to do this delay.


I did something similar a while back and solved it like this. The following will create one quick burst of particles. The next time you want the particles to emit, you change the birthRate of the "cloud" back to 1.

CAEmitterCell *dustCell = [[CAEmitterCell alloc] init];
[dustCell setBirthRate:7000];
[dustCell setLifetime:3.5];
// Various configurations for the appearance...
// This is the only cell with configured scale, 
// color, content, emissionLongitude, etc...

CAEmitterCell *dustCloud = [CAEmitterCell emitterCell];
[dustCloud setBirthRate:1.0]; // Create one cloud every second
[dustCloud setLifetime:0.06]; // Emit dustCells for 0.06 seconds
[dustCloud setEmitterCells:[NSArray arrayWithObject:dustCell]];
[dustCloud setName:@"cloud"]; // Use this name to change the birthRate later

[dustEmitter setEmitterPosition:myPositionForDustEmitter];
[rightDustEmitter setEmitterCells:[NSArray arrayWithObject:dustCloud]];

// After one burst, change the birth rate of the cloud to 0
// so that there is only one burst per side.
double delayInSeconds = 0.5; // One cloud will have been created by now, but not two
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {

    // For some reason, setting the birthRate of the "cloud" to 0
    // has a strange side effect that when you set it back to 1 all
    // the missed emissions seems to happen at once during the first
    // emission and then it goes back to only emitting once per
    // second. (Thanks D33 for pointing this out).
    // By instead changing the birthRate of the "dust" particle
    // to 0 and then back to (in my case) 7000 gives the visual
    // effect that I'm expecting. I'm not sure why it works
    // this way but at least this works for me...
    // NOTE: This is only relevant in case you want to re-use
    // the emitters for a second emission later on by setting
    // the birthRate up to a non-zero value.
    [dustEmitter setValue:[NSNumber numberWithFloat:0.0] 
               forKeyPath:@"emitterCells.cloud.emitterCells.dust.birthRate"];
});
0
strangetimes On

CAEmitter.birthRate is animatable. Assuming you've added a few CAEmitterLayers to the view, you can do this to animate the decay of the birthrate and then re-start after a few seconds:

- (void) startConfetti{
  for (CALayer *emitterLayer in self.layer.sublayers) {
    if ([emitterLayer isKindOfClass: [CAEmitterLayer class]]) {
      ((CAEmitterLayer *)emitterLayer).beginTime = CACurrentMediaTime();
      ((CAEmitterLayer *)emitterLayer).birthRate = 6;

      // Decay over time
      [((CAEmitterLayer *)emitterLayer) removeAllAnimations];

      [CATransaction begin];
      CABasicAnimation *birthRateAnim = [CABasicAnimation animationWithKeyPath:@"birthRate"];
      birthRateAnim.duration = 5.0f;
      birthRateAnim.fromValue = [NSNumber numberWithFloat:((CAEmitterLayer *)emitterLayer).birthRate];
      birthRateAnim.toValue = [NSNumber numberWithFloat:0.0f];
      birthRateAnim.repeatCount = 0;
      birthRateAnim.autoreverses = NO;
      birthRateAnim.fillMode = kCAFillModeForwards;
      [((CAEmitterLayer *)emitterLayer) addAnimation:birthRateAnim forKey:@"finishOff"];
      [CATransaction setCompletionBlock:^{
        ((CAEmitterLayer *)emitterLayer).birthRate = 0.f;
        [self performSelector:@selector(startConfetti) withObject:nil afterDelay:10];
      }];
      [CATransaction commit];
    }
  }
}