SKScene runAction completion block not called - ios

I am working on a sprite kit game in which I have an action called playCountdown:
SKAction *playCountdown = [SKAction performSelector:#selector(startCountdown) onTarget:self];
playCountdown works fine but when it completes, the completion block never gets called.
[self runAction:playCountdown completion:^{
NSLog(#"anything goes");
}];
Here is startCountdown for completeness:
- (void)startCountdown{
SKLabelNode *countdownLabel = [SKLabelNode labelNodeWithFontNamed:#"Arial"];
CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
__block short countdownValue = 3;
countdownLabel.position = CGPointMake(screenWidth/2, screenHeight/2 - countdownLabel.fontSize/2);
countdownLabel.zPosition = 100.0;
countdownLabel.text = [NSString stringWithFormat:#"%d", countdownValue];
countdownLabel.fontColor = [UIColor blackColor];
countdownLabel.verticalAlignmentMode = SKLabelVerticalAlignmentModeBaseline;
[self addChild:countdownLabel];
SKAction *zoomIn = [SKAction scaleTo:1.5f duration:0.9];
SKAction *fadeOut = [SKAction fadeAlphaTo:0.0f duration:0.1];
SKAction *updateCount = [SKAction customActionWithDuration:0.0 actionBlock:^(SKNode *node, CGFloat elapsedTime) {
SKLabelNode *textNode = (SKLabelNode *)node;
countdownValue --;
if (countdownValue == 0) {
textNode.text = #"PLAY!";
}
else{
textNode.text = [NSString stringWithFormat:#"%d", countdownValue];
}
textNode.alpha = 1.0f;
textNode.xScale = 1.0f;
textNode.yScale = 1.0f;
}];
SKAction *sequence = [SKAction sequence:#[zoomIn, fadeOut, updateCount]];
SKAction *repeat = [SKAction repeatAction:sequence count:4];
[countdownLabel runAction:repeat completion:^{
[countdownLabel removeFromParent];
}];
NSLog(#"completed!");
}
I have verified that startCountdown actually reaches its end. However the completion block never gets called. Any ideas?
Thanks

I would recommend you verify that your code is indeed being called by creating a very simple NSLog only block.
If you are still having problems, consider using a SKAction sequence instead. This will accomplish the same thing as you are trying to do with a completion block.

There must be an issue with a SKAction created using a selector... The code:
SKAction *playCountdown = [SKAction performSelector:#selector(startCountdown) onTarget:self];
would execute calling:
[self runAction:playCountdown completion:^{ ... }];
although the completion block would never get called.
I have instead created the playCountdown action using a block:
SKAction *playCountdown = [SKAction runBlock:^{
[self startCountdown];
}];
and the completion block now gets called. The problem now is that the end of the playCountdown action is not "understood" by the system and the 2 actions do not run serially but almost concurrently.

Related

Move several SKSpriteNode in array with delay?

I have a problem with moving several nodes in array. There is a code:
SKSpriteNode* paddle = (SKSpriteNode*)[self childNodeWithName: paddleXCategoryName];
SKAction *move = [SKAction moveTo:CGPointMake(paddle.position.x, CGRectGetMaxY(self.scene.frame)) duration:1.5];
NSMutableArray *shots = [[NSMutableArray alloc] init];
for (int i = 1; i<=10; i++) {
SKSpriteNode *shot = [[SKSpriteNode alloc] initWithImageNamed:#"shoot.png"];
shot.size = CGSizeMake(2, 5);
shot.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(2, 6)];
shot.physicsBody.categoryBitMask = shotCategory;
shot.physicsBody.contactTestBitMask = blockCategory;
shot.physicsBody.collisionBitMask = blockCategory;
[shots addObject:shot];
}
int i = 0;
for (SKSpriteNode *shot in shots) {
SKAction *wait = [SKAction waitForDuration:0.2+0.3*i];
shot.position = paddle.position;
[self addChild:shot];
[shot runAction:[SKAction sequence:#[wait, move]] completion:^{
[shot removeFromParent];
}];
}
I have only one node moving. What am I doing wrong?
Actually all of your nodes are moving together without delay to the same point. This is because you are not incrementing your i variable inside the loop. Try the code below. For the SKAction to work properly also set affectedByGravity to false.
int i = 0;
for (SKSpriteNode *shot in shots) {
SKAction *wait = [SKAction waitForDuration:0.2+0.3*i];
shot.position = paddle.position;
[self addChild:shot];
shot.physicsBody.affectedByGravity = false; // Added line.
[shot runAction:[SKAction sequence:#[wait, move]] completion:^{
[shot removeFromParent];
}];
i++; // Added line.
}
They all run the same move action. Each sprite needs to run its own copy of the action. Try changing the runAction statement by replacing move with [move copy]:
[shot runAction:[SKAction sequence:#[wait, [move copy]]] completion:^{
[shot removeFromParent];
}];

SpriteKit - Adding a timer

I'd like to add a timer to a simple SpriteKit game. Using the code suggested in an existing answer ("Spritekit - Creating a timer") causes 5 errors (s. comments below). Maybe there are other ideas, too!
Thanks a lot!
This is my .m file:
#implementation GameScene {
BOOL updateLabel;
SKLabelNode *timerLabel;
}
int timer;
- (id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
self.backgroundColor = [SKColor whiteColor];
timer = 0;
updateLabel = false;
timerLabel = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
timerLabel.text = #"0";
timerLabel.fontSize = 48;
timerLabel.fontColor = [SKColor greenColor];
timerLabel.position = CGPointMake(700,50);
[self addChild: timerLabel];
}
return self;
}
-(void) didMoveToView:(SKView *)view {
}
The following code gives me the trouble:
id wait = [SKAction waitForDuration:1.0];//Redefinition of 'wait' as different kind of symbol
id run = [SKAction runBlock:^{//Initializer element is not a compile-time constant
timer ++;
updateLabel = true;
}];
[node runAction:[SKAction repeatActionForever:[SKAction sequence:#[wait, run]]];//1. Expected identifier or '('; 2. Use of undeclared identifier 'node'; 3. Collection element of type 'pid_t (int *)' is not an Objective-C object
-(void)update:(CFTimeInterval)currentTime {
if(updateLabel == true){
timerLabel.text = [NSString stringWithFormat:#"%i",timer];
updateLabel = false;
}
}
#end
int timer = 0;
SKLabelNode timerLabel;
- (id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
SKAction *wait = [SKAction waitForDuration:1];
SKAction *performSelector = [SKAction performSelector:#selector(timerLabel) onTarget:self];
SKAction *removeChild = [SKAction runBlock:^{
[timerLabel removeFromParent];
}];
SKAction *increaseTimer = [SKAction runBlock:^{
timer++;
}];
SKAction *sequence = [SKAction sequence:#[removeChild, performSelector, wait, increaseTimer]];
SKAction *repeat = [SKAction repeatActionForever:sequence];
[self runAction:repeat];
}
return self;
}
-(void)timerLabel
{
timerLabel = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
timerLabel.text = [NSString stringWithFormat:#"%i", timer];
timerLabel.fontSize = 48;
timerLabel.fontColor = [SKColor greenColor];
timerLabel.position = CGPointMake(700,50);
[self addChild: timerLabel];
}
Try to call with perform selector. I hope this will work.I didn't test.

Objective-c, SpriteKit particles not removed after action finished

I'm making a game in Xcode, with SpriteKit and I've encountered a problem while working with particles.
Method of Initializing SKEmitterNode:
-(SKEmitterNode *)newExplosionEmitter {
NSString *explosionPath = [[NSBundle mainBundle] pathForResource:#"explosionH" ofType:#"sks"];
SKEmitterNode *explosion = [NSKeyedUnarchiver unarchiveObjectWithFile:explosionPath];
return explosion;
}
This method of my mine is the AI of CPU1, which is called in the update method.
-(void)CPU1AI {
SKSpriteNode *CPU1 = (SKSpriteNode *)[self childNodeWithName:CPU1CategoryName];
int aiX = CPU1.position.x;
int aiY = CPU1.position.y;
int ballX = self.ball.position.x;
int ballY = self.ball.position.y;
if ((aiY-ballY)<250 && !CPU1isDead) {
//CPU1 AI
}
if (CPU1isDead) {
float posY = CPU1.position.y;
float centerX = self.frame.size.width/2;
CGPoint goToPos = CGPointMake(centerX, posY);
SKAction *moveToPoint = [SKAction moveTo:goToPos duration:3];
SKAction *removeNode = [SKAction removeFromParent];
SKAction *CPUdead = [SKAction runBlock:^{CPU1isDead = NO;}];
SKAction *sequence = [SKAction sequence:#[moveToPoint, removeNode, CPUdead]];
explosion.position = CPU1.position;
explosion.zPosition = 10;
[CPU1 runAction:sequence];
if (![explosion hasActions]) {
explosion = [self newExplosionEmitter];
[self addChild:explosion];
[explosion runAction:[SKAction sequence:#[[SKAction fadeAlphaTo:0.5 duration:3.5],[SKAction removeFromParent]]]];
}
}
[explosion removeAllChildren];
}
After the SK runActions ended, I put "[explosion removeAllChildren]" just to make sure my particles will be removed, but with or without it, one is still left in the memory ( I guess ) and is still buffering.
Is it because I declared it as a static SKEmitterNode in my scene ?
Thanks in advance !
Add these methods to your SKScene subclass (code from Apple's website that I modified slightly)...
- (SKEmitterNode*) newExplosionNode: (CFTimeInterval) explosionDuration
{
SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:#"explosionH" ofType:#"sks"]];
// Explosions always place their particles into the scene.
emitter.targetNode = self;
// Stop spawning particles after enough have been spawned.
emitter.numParticlesToEmit = explosionDuration * emitter.particleBirthRate;
// Calculate a time value that allows all the spawned particles to die. After this, the emitter node can be removed.
CFTimeInterval totalTime = explosionDuration + emitter.particleLifetime+emitter.particleLifetimeRange/2;
[emitter runAction:[SKAction sequence:#[[SKAction fadeAlphaTo:0.5 duration:totalTime],
[SKAction removeFromParent]]]];
return emitter;
}
- (void)createExplosionAtPosition:(CGPoint)position
{
SKEmitterNode *explosion = [self newExplosionNode:3.5];
explosion.position = position;
explosion.zPosition = 10;
[self addChild:explosion];
}
Remove the following from your code...
explosion.position = CPU1.position;
explosion.zPosition = 10;
[CPU1 runAction:sequence];
if (![explosion hasActions]) {
explosion = [self newExplosionEmitter];
[self addChild:explosion];
[explosion runAction:[SKAction sequence:#[[SKAction fadeAlphaTo:0.5 duration:3.5],[SKAction removeFromParent]]]];
}
and replace it with this
[self createExplosionAtPosition:CPU1.position];
I suspect you want to set CPU1isDead = NO in the if statement, so the code isn't executed multiple times. You should also delete [explosion removeAllChildren].
Just to help somebody else looking for the same answer I tried this
the one above didn't work for me either - But put me on the right track.
-(void)addTimeBonusParticles
{
SKAction *remove = [SKAction performSelector:#selector(StopParticlesFlame) onTarget:self];
SKAction *wait = [SKAction waitForDuration: .5];
SKAction* blockAction = [SKAction runBlock:^
{
NSString *file = [[NSBundle mainBundle] pathForResource:#"TimeBonusMagic" ofType:#"sks"];
stars = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
[self addChild:stars];
stars.zPosition = 5;
stars.position = CGPointMake(countDown.position.x, countDown.position.y);
}];
[self runAction:[SKAction sequence:#[blockAction,wait,remove]]];
}

Can there be an if statement in a sequence?

Just trying to enhance a part of this code, and I was wondering if I could include an if statement in a sequence? I want to slow down the spawning of debris when the debris count is greater than 10.
This is the current code:
-(void)spawnDebris {
//debris
SKSpriteNode * debris = [SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImageNamed:#"debris1.png"] size:CGSizeMake(40, 40)];
debris.zPosition = 1.0;
debris.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:15];
debris.physicsBody.allowsRotation = NO;
debris.physicsBody.categoryBitMask = CollisionDebris;
RandomPosition = arc4random() %248;
RandomPosition = RandomPosition + 34;
debris.position = CGPointMake (RandomPosition, self.size.height + 40);
[_debris addObject:debris];
[self addChild:debris];
//next Spawn:
[self runAction:[SKAction sequence:#[
[SKAction waitForDuration:2],
[SKAction performSelector:#selector(spawnDebris) onTarget:self],
]]];
if (_dead == YES) {
[self removeAllActions];
}
}
Where _debris is an NSMutableArray. So essentially the SKSpriteNodes are being added every 2 seconds, but to change it up, I wanted to have their waitForDurations to change after the _debris count is over a certain number.
This is what I thought to do:
//next Spawn:
[self runAction:[SKAction sequence:#[
[SKAction waitForDuration:2],
[SKAction runBlock:^{
if (_debris.count > 10) {
[SKAction waitForDuration:7];
}
}],
[SKAction performSelector:#selector(spawnDebris) onTarget:self],
]]];
But this didn't work. Is it even possible to use an if statement in a sequence? How can I get this to work?
The code won't work because the [SKAction waitForDuration:7] is not executed.
Much easier would be:
float time = 2.0;
if (_debris.count > 10) { time = 9.0; }
[self runAction:[SKAction sequence:#[
[SKAction waitForDuration:time],
[SKAction performSelector:#selector(spawnDebris) onTarget:self],
]]];
The block declares a waitForDuration action but never runs it on anything.
Try
[self runAction:[SKAction sequence:#[
[SKAction waitForDuration:2],
[SKAction runBlock:^{
if (_debris.count > 10) {
[self runAction:[SKAction waitForDuration:7]];
}
}],
[SKAction performSelector:#selector(spawnDebris) onTarget:self],
]]];
Why don't you just use a random number generator to make it more interesting.
Instead of the if statement and all that, simply replace 2 with arc4random()% x (< choose a number) then it will spawn randomly every x seconds.

Sprite Kit pause game when lose a life

I can not figure this out. I have a game where a player has to catch berries as they fall from the sky. When the player misses one, I want the screen to flash red and the berries to stop falling and for the game to pause for 3 seconds then resume.
#interface SpriteMyScene : SKScene{
SKAction *sceneUnPaused;
SKAction *scenePaused;
}
2 methods to pause the game.
-(SKAction*)unpaused {
sceneUnPaused = [SKAction runBlock:^{
self.scene.view.paused = NO;
}];
return sceneUnPaused;}
-(SKAction*)paused{
scenePaused = [SKAction runBlock:^{
self.scene.view.paused = YES;
}];
return scenePaused;
}
The method that creates the berries and their actions.
- (void)addBerry {
...
...
// Create the actions
//For each individual berry
SKAction * actionMove = ...
SKAction * gameWon = ...
SKAction * actionMoveDone = ...
SKAction *wait3Seconds = [SKAction waitForDuration:3];
SKAction * loseAction = [SKAction runBlock:^{
[self subtractLives];
NSLog(#"Lost a life");
[self vibrate];
[self runAction:scenePaused];
[self runAction:wait3Seconds];
[berry removeFromParent];
[self runAction:sceneUnPaused];
if (_playerLives == 0){
[[SoundManager sharedManager] stopMusic];
SKTransition *reveal = [SKTransition fadeWithDuration:1.0];
SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size andScore:_berriesKilled];
[self.view presentScene:gameOverScene transition: reveal];}}];
SKAction *berrie = [SKAction runBlock:^{
[berry runAction:actionMove];}];
[berry runAction:[SKAction sequence:#[berrie, gameWon, actionMove, loseAction, actionMoveDone]]];
}
I don't know if this is necessary or not for you guys to help me figure out why it won't work, but here is the update method that decides on when I will spawn the berries.
- (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)timeSinceLast {
self.lastSpawnTimeInterval += timeSinceLast;
if (self.lastSpawnTimeInterval > [self determineAmountOfBerries]) {
self.lastSpawnTimeInterval = 0;
[self addBerry];
}
}
When the player misses a berry, just do this:
[self.scene.view setPaused:YES];
Start an NSTimer that does the following after 3 seconds:
[berry removeFromParent];
[self.scene.view setPaused:NO];

Resources