Reputation: 239
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"];
});
Upvotes: 12
Views: 12213
Reputation: 5223
CAEmitter.birthRate
is animatable. Assuming you've added a few CAEmitterLayer
s 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];
}
}
}
Upvotes: 2
Reputation: 263
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
}
The delegate property could also be nil if you don't want to do anything on completion, as in animationDidStop:finished:
The constants kBirthRateDecayAnimationKey & kStandardAnimationDuration use my convention, not Apple's.
Upvotes: 0
Reputation: 9659
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.
Upvotes: 6
Reputation: 19879
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 :
confettiEmitter.birthRate = 0.0;
.Hope it can help.
Upvotes: 5
Reputation: 239
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...
Upvotes: 0
Reputation: 56645
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"];
});
Upvotes: 11