12

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"];
});
D33
  • 239
  • 1
  • 3
  • 14
  • Have you looked at Apples sample code ([Fireworks](http://developer.apple.com/library/mac/#samplecode/Fireworks/Listings/ReadMe_txt.html)) for this? They basically do what you are trying to do (a short explosion of particles) – David Rönnqvist Jun 07 '12 at 10:01
  • Yes, I have. But it just looks like it does short emission. In general there's a layer (with emitter) added to the view inside the awakeFromNib and then it emitts "fireworks" round and round. But imagine the situation when I would like to start just one fireworks when you tap the button. How would you do that? That is exactly what I'm pointing out. – D33 Jun 07 '12 at 10:23

6 Answers6

11

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"];
});
David Rönnqvist
  • 56,267
  • 18
  • 167
  • 205
  • I'm a little bit desperate about it. I tried what you said. But with no success.But with a little step forward though. I'm creating the emitter in viewDidLoad like in the code I added into my original question. It works quite well BUT with delay :( When I hit the button, nothing happens. And after a second maybe it gives me that particle. What happens when I hit the button twice is for another story :( I already tried work with birthRate. But when I set it to 1 to start emission, it gave me more then 10 particles instead of one... – D33 Jun 07 '12 at 13:35
  • I don't know. If the dispatch_after is behaving strange you can have your emitter layer emit a cell with birthRate = 1 that in turn has the emits your real cells. This way you can emit one partie with no speed and a custom life time to emit your particles and then set its birthRate to 0 after 0.5 seconds. This will give you one burst of particles – David Rönnqvist Jun 07 '12 at 14:12
  • Hm maybe I didn't understand your answer well. But I don't have problem with dispatch (I guess). It's just starting to emit after some delay and I don't know why. I think it's something like when I create the emitterLayer, it's timer start to running.And when I changing the lifetime by the time,it's still working with existing running timer.So it's not emitting immediately but when the right time comes... And it gives me the delay. Hm. Any idea how to avoid this? – D33 Jun 07 '12 at 14:35
  • ok,I read that it start to emit when I set the array of particles.So i guess it also reset the "timer" too. But the behavior is still weird. In dispatch_after I set the emitterCells to nil and setting the array back when I want to start it. But sometimes the particle is visible only shortly and it's dismiss is very rough (not smoothly with alpha). BLEEH – D33 Jun 07 '12 at 14:49
  • I've updated my answer with some code I previously used to do the same thing that I believe you are trying to do. – David Rönnqvist Jun 07 '12 at 19:01
  • Thank you for your answer. But when I use [dustCloud setVelocity:110]; and stop the emitter and then when I start it back again, it gives me much more particles then one (but only for first emission). Others are ok... weird behavior. I'm testing it on the Simulator 5.1. On the real device it's not giving me anything sometimes (but not every "first" emission. Just sometimes)... – D33 Jun 09 '12 at 09:21
  • I played with that a little bit and tried the attitude with setting the array of particles of main emitter. When I set it, it's emitting and when I set it to nil, it stops. It works quite well (it looks like) but I'm not very satisfied with this kind of solution. Yours looks more proper (unfortunately it's not working as I would like to:( ) – D33 Jun 09 '12 at 11:11
  • What is the problem with it? Is the result something else then you were expecting or is it just not working at all? – David Rönnqvist Jun 09 '12 at 11:17
  • The problem is that when I start emitting by calling [dustEmitter setValue:[NSNumber numberWithFloat:1.0] forKeyPath:@"emitterCells.cloud.birthRate"];, it emitts about 30 particles instead of desired one. Then it dismiss (thats ok ofcourse) and then it emitts one particle every second (thats also ok). But the thing with that starting chaos effect is confusing and I don't get it why it is happening. – D33 Jun 09 '12 at 11:47
  • I didn't notice that :D... I have some other animations for the acceleration and such going on in my code so the effect wasn't as obvious. I changed the birthRateAnimation key to `@"emitterCells.cloud.emitterCells.dust.birthRate"` and that seemed to work for me. (I removed the other birthRate animation since leaving it in and changing both birthRates to 0 caused the same problem). Does this work for you as well? I've updated my answer (and my own code :P) – David Rönnqvist Jun 09 '12 at 12:48
  • Hm, I think I'm still missing something. I added new attitude (based on yours) under my original code. When I use 7000 as birthrate, it does it. I can't count the real number but it gives me particles. But when I set it to 1, it does nothing :( And also I have few question: - is really necessary to use emitter-emitterCells-emitterCells (I mean "dust" connected to "cloud"? What is it good for in general? - am I setting the properties correctly (like when I set the image for the particle, velocity, ... to the right emitterCell? – D33 Jun 09 '12 at 15:46
  • It may not be necessary. I originally used it because I thought that I could set the birthRate of the entire particle cloud 0 or 1 instead of setting exact number of particles. Now that I know that it doesn't work I tried it out with the "dust" particles added to the emitter without the "cloud". This doesn't seem to give the strange effect where to many particles emit when you turn it on again so this is probably the best solution. I guess we'r all new to the emitter layer. I would love to experiment more with it when I have the time to do so. – David Rönnqvist Jun 09 '12 at 16:23
  • I don't really know.Actually I don't know how did you do that it works.Did you tried emit only one particle (after some interactions like after button click and so on)? My first emit is ok but the others are multiplied... :( So I have to do it with adding and removing the array of particles... it's weird and I don't like this solution much but it seems it's the easiest solution for this. I've already tried many of attitudes based on yours or not but everytime it gives me similar (bad) results :( Just too many particles made by awakened emitter then I desired... – D33 Jun 09 '12 at 16:44
  • The last line of code above will try to set the birthRate float property with an NSNumber object. Will that do the right thing, or will it result in a random (essentially) value assigned to birthRate (that's what I'd expect but perhaps there is more magic in the key-value-observer runtime I don't know about :)). – theLastNightTrain Apr 08 '13 at 20:23
  • @theLastNightTrain you need to wrap primitives (you can't use a float) in KVO. [This page in the KVO programming guide](http://developer.apple.com/library/ios/documentation/cocoa/conceptual/KeyValueCoding/Articles/DataTypes.html) explains it in more detail. – David Rönnqvist Apr 09 '13 at 05:02
  • (embarrassed cough) ah yes, thanks. I don't use KVO often enough :), let's all try to forget I just ask that! :) – theLastNightTrain Apr 10 '13 at 08:24
6

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.

Community
  • 1
  • 1
thomax
  • 9,213
  • 3
  • 49
  • 68
  • thanks for the simple and elegant answer. I am still confused of how i can place emittercell1 and emittercell2 in two different locations on the emitterlayer. thanks – A. Adam Dec 29 '14 at 08:33
5

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.

shannoga
  • 19,649
  • 20
  • 104
  • 169
2

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];
    }
  }
}
strangetimes
  • 4,953
  • 1
  • 34
  • 62
0

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...

D33
  • 239
  • 1
  • 3
  • 14
0

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.

user396030
  • 263
  • 3
  • 13