CAEmitterLayer draining battery - ios

My project deals with IOS 12+ iPhones and i am using fire emitter to follow the finger of the user across the screen (with a pan gesture recogniser). When using the following code, i have noticed that the screen becomes warmer the game is played and it tends to drain battery more rapidly than usual the more the app is used.
Is there any particular to have a fire emitter for following a finger on the screen and yet have a battery friendly solution?
//set ref to the layer
cursorEmiter = [CAEmitterLayer layer];
//configure the emitter layer
cursorEmiter.emitterPosition = CGPointMake(50, 50);
cursorEmiter.emitterSize = CGSizeMake(5, 5);
CAEmitterCell* fire = [CAEmitterCell emitterCell];
fire.birthRate = 1000; // The number of emitted objects created every second. Animatable
fire.lifetime = 0.6; // The lifetime of the cell, in seconds. Animatable
fire.lifetimeRange = 0.5;
fire.color = [[UIColor colorWithRed:1.0 green:0.4 blue:0.2 alpha:0.3]
CGColor];
fire.contents = (id)[[UIImage imageNamed:#"PopCornSmall.png"] CGImage];
fire.velocity = 10; // The initial velocity of the cell. Animatable
fire.velocityRange = 20;
fire.scaleSpeed = 0.3;
fire.spin = 0.5;
cursorEmiter.renderMode = kCAEmitterLayerAdditive;
[fire setName:#"fire"];
//add the cell to the layer and we're done
cursorEmiter.emitterCells = [NSArray arrayWithObject:fire];
emiterNotAdded = false;
[(CAEmitterLayer *)self.layer addSublayer: cursorEmiter];

Related

How to remove strange delay between two sequential CAKeyframeAnimation animations?

iOs-coders!
This code to animate the little red square to drawing big sign "8" on the UIView. First an upper ring (animSequence1 = 5 sec), then right away a lower ring (animSequence2 = another 5 sec).
No delay needed!
But I stucked on strange delay (about 1 sec) between two sequential animations. Whence did this delay come from?!?
Code:
- (void)drawRect:(CGRect)drawRect {
CGContextRef context = UIGraphicsGetCurrentContext();
drawRect = CGRectMake(0, 0, 320, 320);
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor;);
CGContextFillRect(context, drawRect);
// Little red square
CALayer *layerTest;
layerTest = [CALayer layer];
layerTest.bounds = CGRectMake(0, 0, 20, 20);
layerTest.anchorPoint = CGPointMake(0, 0);
layerTest.backgroundColor = [UIColor redColor].CGColor;
[[self layer] addSublayer:layerTest];
// Upper ring
CAKeyframeAnimation *animSequence1;
animSequence1 = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animSequence1.fillMode = kCAFillModeForwards;
animSequence1.removedOnCompletion = NO;
animSequence1.autoreverses = NO;
animSequence1.repeatCount = 0;
animSequence1.duration = 5.0;
animSequence1.beginTime = 0.0;
animSequence1.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 120) radius:80 startAngle:DEG_TO_RAD(90) endAngle:DEG_TO_RAD(450) clockwise:YES].CGPath;
animSequence1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Lower ring
CAKeyframeAnimation *animSequence2;
animSequence2 = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animSequence2.fillMode = kCAFillModeForwards;
animSequence2.removedOnCompletion = NO;
animSequence2.autoreverses = NO;
animSequence2.repeatCount = 0;
animSequence2.duration = 5.0;
animSequence2.beginTime = 5.0;
animSequence2.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 280) radius:80 startAngle:DEG_TO_RAD(-90) endAngle:DEG_TO_RAD(-450) clockwise:NO].CGPath;
animSequence2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// A sequence of animations
CAAnimationGroup *animGroup;
layerTest.position = CGPointMake(200, 200);
animGroup = [CAAnimationGroup animation];
animGroup.duration = 10.0;
animGroup.animations = [NSArray arrayWithObjects:animSequence1, animSequence2, nil];
[layerTest addAnimation:animGroup forKey:nil];
}
Also I tried make it without CAAnimationGroup.
Set up [animSequence1 setDelegate:self]; for first CAKeyframeAnimation (animSequence1), and then start second CAKeyframeAnimation (animSequence2) by (void)animationDidStop.
It worked same, but this strange delay (about 1 sec) between two animations no dissapear!
QUESTION: How to remove strange delay between two sequential CAKeyframeAnimation animations? No delay needed!
Don’t create layers or animations in -drawRect:—you don’t have a whole lot of control over when it gets called, so you’re going to end up with multiple layerTests added to your view, all trying to animate separately. Create the layer / animation in an initializer, or in response to some user interaction.
I would also advise getting rid of your fillMode and removedOnCompletion settings on animSequence1; once animSequence2 starts, you’re effectively asking CA to run two non-additive animations once on the same property. Not sure whether the behavior for that is well-defined, but it’s another place the weirdness could be coming from.

How to draw points obtained from an array in for loop with animation in iOS

I can draw points with code below, but I need to draw every point one by one with animation. When I try this, all points appear on the screen only after the for loop ends.
I did some research but I could not figure out why these points are not drawing one by one in the for loop. singerData is an array which has x values of points.
-(void)drawDotGraphForUser
{
UIView *viewAnimationUser = [[UIView alloc]init];
viewAnimationUser.frame = CGRectMake(0, 0, 320, 100);
for (int i = 0; i<singerPitchDataSize; i++)
{
CALayer *layer = [CALayer layer];
NSAssert1(layer != nil, #"failed to create CALayer #%i", i);
layer.backgroundColor = [UIColor colorWithRed:0 green:0 blue:1 alpha:1.0].CGColor;
layer.frame = CGRectMake(i+1, singerData[i], 1.0, 1.0);
NSLog(#"GraphUser= %d=%f",i,singerData[i]);
NSLog(#"MSprites=%#",mSprites[i]);
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 5;
pathAnimation.fromValue = [NSNumber numberWithInt:0];
pathAnimation.toValue = [NSNumber numberWithInt:singerPitchDataSize];
[layer addAnimation:pathAnimation forKey:#"strokeEnd"];
[viewAnimationUser.layer addSublayer:layer];
[self.view addSubview:viewAnimationUser];
}
}
These are all appearing to be happening at the same time because your animations are being sent to another thread automatically extremely quickly. If you want them to appear one by one you will need to send them to add them to the subview slower by adding a timer at the end of your function or you will need to listen for the animation to end to trigger the next one.

How to make something like this?

I'm not sure if I'm correct in wording it "modal sprite-kit scene" but what I'm trying to do is have a smaller sized scene appear over a scene when the game is complete. Attached is a screenshot to show what I mean:
The screenshot is from Flappy Bird where when the player dies, the small game-over scene pops up almost like a modal effect, and displays the user's final results. I was wondering how to go about creating this.
I tried calling this when the game was done:
[self.player runAction:death completion:^{
[self removeAllActions];
GameOverNode *gameOverNode = [[GameOverNode alloc] initWithScore:self.size];
gameOverNode.gameScene = self;
gameOverNode.position = CGPointMake(self.scene.size.width/2, -150);
[self addChild:gameOverNode];
[gameOverNode runAction:[SKAction moveToY:self.scene.size.height/2 duration:0.6]];
And this was the code for the gameOverNode in the gameOverScene.m's file:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
self.userInteractionEnabled = YES;
self.zPosition = 20;
SKSpriteNode *bg = [SKSpriteNode spriteNodeWithColor:[UIColor blackColor] size:CGSizeMake(280*DoubleIfIpad, 300*DoubleIfIpad)];
bg.alpha = 0.55;
But the node only shows up in the bottom left hand corner of the screen, as opposed to in the middle like I want it too.
What am I doing wrong? How can I make the small gameover node pop up and sit in the middle of the scene.
You simply can't. According to Apple Docs and multiple StackOverflow posts you cannot be running two SKScene's at the same time. To recreate the effect you want, you can create an SKShapeNode with border and line color similar to those you want, and place objects like score inside, or you can create an image for the screen that pops up and just add SKLabelNodes to the scene that show the high score and current score. I'm attaching a picture of how I created an end "view" for my app, Average Guy.
I made the popup screen using an SKShapeNode like this:
SKShapeNode *optionsMenu = [SKShapeNode node];
CGRect tempRect = CGRectMake(SELF_WIDTH/2, SELF_HEIGHT/2, SELF_WIDTH-20, (finalScoreLabel.position.y+(finalScoreLabel.frame.size.height/2)) - (mainMenuButton.position.y-(mainMenuButton.frame.size.height/2)) + 20);
CGPathRef tempPath = CGPathCreateWithRoundedRect(CGRectMake(tempRect.origin.x - tempRect.size.width/2, tempRect.origin.y - tempRect.size.height/2, tempRect.size.width, tempRect.size.height), 5, 5, Nil);
optionsMenu.path = tempPath;
optionsMenu.fillColor = [SKColor grayColor];
optionsMenu.strokeColor = [SKColor blueColor];
optionsMenu.zPosition = 30;
optionsMenu.glowWidth = 2.0f;
CGPathRelease(tempPath);
Of course, don't mind the hectic CGRect implementation, I wanted to make everything relative to fit all of the screen sizes so I didn't hardcode any of the positions. As for adding the score and the buttons, I did this:
SKLabelNode *restartButton = [SKLabelNode labelNodeWithFontNamed:#"00 Starmap Truetype"];
restartButton.text = #"Play Again";
restartButton.zPosition = 31;
restartButton.fontColor = [SKColor blueColor];
restartButton.fontSize = 30;
restartButton.position = CGPointMake(HALF_WIDTH, HALF_HEIGHT-50);
restartButton.name = #"restart_button";
SKLabelNode *restartButtonShadow = restartButton.copy;
restartButtonShadow.position = CGPointMake(restartButton.position.x+1, restartButton.position.y-1);
restartButtonShadow.color = [SKColor colorWithRed:0 green:0 blue:.5 alpha:.1];
SKLabelNode *mainMenuButton = [SKLabelNode labelNodeWithFontNamed:#"00 Starmap Truetype"];
mainMenuButton.text = #"Main Menu";
mainMenuButton.zPosition = 31;
mainMenuButton.fontColor = [SKColor blueColor];
mainMenuButton.fontSize = 30;
mainMenuButton.position = CGPointMake(HALF_WIDTH, HALF_HEIGHT - 85);
mainMenuButton.name = #"main_menu_button";

CAEmitterCell - slow stars or light effect

I'm playing around with CAEmitterCell, but most effects are very fast effects like the firework.
But I want to have a slow "stars" effect like you see here on www.findyourwaytooz.com
How can I do that with CAEmitterCell?
Thank you :)
I have a project that uses the following setup for the emitter, and it pretty accurately mimics what I think you mean:
//set ref to the layer
starsEmitter = (CAEmitterLayer*)self.layer; //2
//configure the emitter layer
starsEmitter.emitterPosition = CGPointMake(160, 240);
starsEmitter.emitterSize = CGSizeMake(self.superview.bounds.size.width,self.superview.bounds.size.height);
NSLog(#"width = %f, height = %f", starsEmitter.emitterSize.width, starsEmitter.emitterSize.height);
starsEmitter.renderMode = kCAEmitterLayerPoints;
starsEmitter.emitterShape = kCAEmitterLayerRectangle;
starsEmitter.emitterMode = kCAEmitterLayerUnordered;
CAEmitterCell* stars = [CAEmitterCell emitterCell];
stars.birthRate = 0;
stars.lifetime = 10;
stars.lifetimeRange = 0.5;
stars.color = [[UIColor colorWithRed:255 green:255 blue:255 alpha:0] CGColor];
stars.contents = (id)[[UIImage imageNamed:#"particle.png"] CGImage];
stars.velocityRange = 500;
stars.emissionRange = 360;
stars.scale = 0.2;
stars.scaleRange = 0.1;
stars.alphaRange = 0.3;
stars.alphaSpeed = 0.5;
[stars setName:#"stars"];
//add the cell to the layer and we're done
starsEmitter.emitterCells = [NSArray arrayWithObject:stars];
I uploaded the sample project to GitHub: SimpleCAEmitterLayer

How do you remove a CAEmitterLayer when it ends the lifetime of it's CAEmitter Cells -- instead of repeating until you remove it from super layer

I am using generic code (from the iOS Fireworks demo) in a slightly changed way. I have the following in a subclass of UIView. What I want is to make the firework appear at the point the user touches (not hard) and play out for the length of the CAEmitterLayer/CAEmitterCells 'lifetime'. Instead, this is immediately starting when i add it to addSublayer -- like I am sure it is meant to. However, I would like to use it in a slightly different way. Is there a way that I can change this so there is a CATransaction with a completion block (to removeFromSuperlayer) or something like that? Any ideas are welcomed.
#import "FireworksView.h"
#implementation FireworksView
- (void)launchFirework{
//Load the spark image for the particle
CAEmitterLayer *mortor = [CAEmitterLayer layer];
mortor.emitterPosition = CGPointMake(self.bounds.size.width/2, self.bounds.size.height*(.75));
mortor.renderMode = kCAEmitterLayerAdditive;
//Invisible particle representing the rocket before the explosion
CAEmitterCell *rocket = [CAEmitterCell emitterCell];
rocket.emissionLongitude = -M_PI / 2;
rocket.emissionLatitude = 0;
rocket.lifetime = 1.6;
rocket.birthRate = 1;
rocket.velocity = 400;
rocket.velocityRange = 100;
rocket.yAcceleration = 250;
rocket.emissionRange = M_PI / 4;
rocket.color = CGColorCreateCopy([UIColor colorWithRed:.5 green:.5 blue:.5 alpha:.5].CGColor);
rocket.redRange = 0.5;
rocket.greenRange = 0.5;
rocket.blueRange = 0.5;
//Name the cell so that it can be animated later using keypath
[rocket setName:#"rocket"];
//Flare particles emitted from the rocket as it flys
CAEmitterCell *flare = [CAEmitterCell emitterCell];
flare.contents = (id)[UIImage imageNamed:#"tspark.png"].CGImage;
flare.emissionLongitude = (4 * M_PI) / 2;
flare.scale = 0.4;
flare.velocity = 100;
flare.birthRate = 45;
flare.lifetime = 1.5;
flare.yAcceleration = 350;
flare.emissionRange = M_PI / 7;
flare.alphaSpeed = -0.7;
flare.scaleSpeed = -0.1;
flare.scaleRange = 0.1;
flare.beginTime = 0.01;
flare.duration = 0.7;
//The particles that make up the explosion
CAEmitterCell *firework = [CAEmitterCell emitterCell];
firework.contents = (id)[UIImage imageNamed:#"tspark.png"].CGImage;
firework.birthRate = 9999;
firework.scale = 0.6;
firework.velocity = 130;
firework.lifetime = 2;
firework.alphaSpeed = -0.2;
firework.yAcceleration = 80;
firework.beginTime = 1.5;
firework.duration = 0.1;
firework.emissionRange = 2 * M_PI;
firework.scaleSpeed = -0.1;
firework.spin = 2;
//Name the cell so that it can be animated later using keypath
[firework setName:#"firework"];
//preSpark is an invisible particle used to later emit the spark
CAEmitterCell *preSpark = [CAEmitterCell emitterCell];
preSpark.birthRate = 80;
preSpark.velocity = firework.velocity * 0.70;
preSpark.lifetime = 1.7;
preSpark.yAcceleration = firework.yAcceleration * 0.85;
preSpark.beginTime = firework.beginTime - 0.2;
preSpark.emissionRange = firework.emissionRange;
preSpark.greenSpeed = 100;
preSpark.blueSpeed = 100;
preSpark.redSpeed = 100;
//Name the cell so that it can be animated later using keypath
[preSpark setName:#"preSpark"];
//The 'sparkle' at the end of a firework
CAEmitterCell *spark = [CAEmitterCell emitterCell];
spark.contents = (id)[UIImage imageNamed:#"tspark.png"].CGImage;
spark.lifetime = 0.05;
spark.yAcceleration = 250;
spark.beginTime = 0.8;
spark.scale = 0.4;
spark.birthRate = 10;
preSpark.emitterCells = [NSArray arrayWithObjects:spark, nil];
rocket.emitterCells = [NSArray arrayWithObjects:flare, firework, preSpark, nil];
mortor.emitterCells = [NSArray arrayWithObjects:rocket, nil];
[self.layer addSublayer:mortor];
}
The answer to this is, using CAEmitter, there is NO WAY -- delegate, etc -- to stop the emitter when it ends the cycle. The only thing you can do is gracefully remove it from the layer when you think it should be removed.
Ok, so I was able to sort of achieve this by creating an animation with delegate that fired along with the emitter. in the animationDidStop i made a for loop that went through my view hierarchy and looked for emitters and removed them. it's buggy and I still want a real solution, but this works for now.
for (CALayer *layer in _plusButton.layer.sublayers) {
if (layer.class == [CAEmitterLayer class]) {
[layer removeFromSuperlayer];
}
}

Resources