iOS SpriteKit SKAction completion call not working/creating odd results - ios

I'm trying to have a SKNode move onto the screen on command. I've set up the following SKAction chain so that 1) The node moves up and offscreen, then 2) the node moves down into a starting place, then 3) starts moving around. I've used the following code to try and implement this:
SKAction *moveUp = [SKAction moveTo: shipIntroSpot1 duration:3.0];
SKAction *moveDown = [SKAction moveTo:shipSpot1 duration:ship1MovementSpeed];
[self enumerateChildNodesWithName:#"ship1" usingBlock:^(SKNode *node, BOOL *stop) {
NSLog(#"RUNNING MOVE UP");
[node runAction: moveUp];
[node runAction:moveUp completion:^{
NSLog(#"RUNNING MOVE DOWN");
[node setHidden: NO];
[node runAction: moveDown];
}];
[node runAction:moveDown completion:^{
NSLog(#"STARTING MOVEMENT");
}];
However, the SKActions don't appear to be firing in the correct order. I used the code so that once one step was completed, the next would start. However, the SKAction doesn't appear to move the node fast enough before the next SKAction starts working. I was under the impressions that using the completion call would mean that the next action wouldn't start until the previous one had finished? That does not appear to be the case here. In addition, if I leave a long-enough duration for step 1 (MOVING UP), it will skip over step 2 (MOVING DOWN) and start executing step 3 (STARTING MOVEMENT). I have absolutely no idea how this is possible. If anyone could point out my error in understanding how to chain different actions together properly, I would appreciate it.
(I did not do a SKAction sequence because I have to "unhide" the node halfway through. I don't think I can put that in a sequence, unless I'm wrong about that too.)

It does not seem like you understand how runAction is executed.
Using
[node runAction:moveUp completion:^{
NSLog(#"RUNNING MOVE DOWN");
[node setHidden: NO];
[node runAction: moveDown];
}];
will not just define a completion to be done at the end of the action, but it runs the action and then calls the completion, so the very first [node runAction:moveUp] needs to be removed.
Secondly, two runAction calls that are made in one function/block will be called simultaneously, and since you call [node runAction: moveUp], [node runAction:moveUp completion:] , and [node runAction:moveDown completion:] all in the same block, they will all be executed simultaneously.
This is the code you are looking for:
SKAction *moveUp = [SKAction moveTo: shipIntroSpot1 duration:3.0];
SKAction *moveDown = [SKAction moveTo:shipSpot1 duration:ship1MovementSpeed];
[self enumerateChildNodesWithName:#"ship1" usingBlock:^(SKNode *node, BOOL *stop) {
NSLog(#"RUNNING MOVE UP");
[node runAction:moveUp completion:^{
NSLog(#"RUNNING MOVE DOWN");
[node setHidden: NO];
[node runAction:moveDown completion:^{
NSLog(#"STARTING MOVEMENT");
}];
}];

The completion block runs after the action is finished. So, if you want to move up, then move down, then move around, you need to write the following code:
[node runAction:moveUp completion:^{
[node runAction:moveDown completion:^{
NSLog(#"STARTING MOVEMENT");
}];
}];
What you have right now won't work because you call three runAction's in a row. You first tell it to moveUp, but then immediately override that action with another moveUp, this time with a completion block where you run moveDown. Then, you immediately override that action with a moveDown, so essentially, all you're doing is telling the node to moveDown, everything else is superfluous.

I know it's been marked as answered but though I'd offer up an alternative.
An alternative option would be to simply sequence the Actions with just one completion handler
for example (could just be written as a one liner if you wanted):
NSArray *actions = #[moveUp, [SKAction unhide], moveDown];
SKAction *action = [SKAction sequence:actions];
then in your loop just write:
[node runAction:action withCompletion^{
NSLog(#"STARTING MOVEMENT");
}];
Hopefully someone will find this useful.

Related

I'm getting a sigabrt error when a certain collision is detected (iOS, SpriteKit)

I am currently building an iOS app in Objective-C. The idea of the app is that you have some sort of rocket ship navigating through an asteroid belt of sorts. It is played in portrait mode. There are two different kinds of asteroids. Normal ones that make you lose when you crash into them, and golden ones that you shoot at to get coins.
Now, the issue occurs when a collision is detected between the bullet and a gold asteroid. When a collision is detected between the player and a normal asteroid, all is well, and you lose. But when a collision is detected between a bullet and a gold asteroid, I receive a sigabrt error.
The code code that detects the collision is the following:
- (void)didBeginContact:(SKPhysicsContact *)contact
{
uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask);
if (collision == (playerCategory | asteroidCategory))
{
[self player:(SKSpriteNode *)self.player didCollideWithAsteroid:(SKSpriteNode *)self.asteroid];
}
uint32_t collision2 = (contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask);
if (collision2 == (bulletCategory | goldAsteroidCategory))
{
[self bullet:(SKSpriteNode *)self.bullet didCollideWithGoldAsteroid:(SKSpriteNode *)self.goldAsteroid];
}
}
The code that runs when "[self player:(SKSpriteNode *)self.player didCollideWithAsteroid:(SKSpriteNode *)self.asteroid];" is called is this:
- (void)player:(SKSpriteNode *)player didCollideWithAsteroid:(SKSpriteNode *)asteroid
{
[self runAction:[SKAction playSoundFileNamed:#"Explosion.mp3" waitForCompletion:NO]];
NSLog(#"Hit");
[self.player removeFromParent];
[self.asteroid removeFromParent];
SKAction *actionMoveDone = [SKAction removeFromParent];
SKAction * loseAction = [SKAction runBlock:^{
SKTransition *reveal = [SKTransition crossFadeWithDuration:0.5];
SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:NO];
[self.view presentScene:gameOverScene transition: reveal];
}];
[self.asteroid runAction:[SKAction sequence:#[loseAction, actionMoveDone]]];
}
This is the code that runs when "[self bullet:(SKSpriteNode *)self.bullet didCollideWithGoldAsteroid:(SKSpriteNode *)self.goldAsteroid];" is called:
- (void)bullet:(SKSpriteNode *)bullet didCollideWithGoldAsteroid:(SKSpriteNode *)goldAsteroid
{
[self runAction:[SKAction playSoundFileNamed:#"ding.m4a" waitForCompletion:NO]];
NSLog(#"Hit");
[self.bullet removeFromParent];
[self.goldAsteroid removeFromParent];
[self plusOneCoin];
}
I believe that should be enough information and code, but do not hesitate to ask me for more, I will post what is necessary.
When I looked in the log, I saw that there was an issue with the sound effect "ding.m4a". I replaced it with another coin sound, and it now works. I'm not exactly sure what was up with the sound, but that was the issue.

How to perform SKAction in a view controller file

Now I am working on a SKScene. I have two ViewController files and I created two scene files which is subview of a SKScene. I know I can perform many SKActions in the scene files. But I want to do some SKAction in the method appEntersBackground: in the file ViewController.m. But it seems the SKActions in this method don't work and I don't know why.
In the ViewController.m file:
-(void)appEntersBackground {
SKAction *action = [SKAction customActionWithDuration:0.1 actionBlock:^(SKNode *node, CGFloat elapsedTime) {
//perform some actions
}
[myNode runAction: action];
}
You cannot perform an SKAction from the viewController. You might however, trigger a method in the scene itself when the application enters the background and perform the necessary operations there.
Paste the appEntersBackground method in the SKScene itself. Then call the following notification in the didMoveToView method.
-(void)didMoveToView:(SKView *)view {
/* Setup your scene here */
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appEntersBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
//The rest of your setup goes here
}
-(void)appEntersBackground {
SKAction *action = [SKAction customActionWithDuration:0.1 actionBlock:^(SKNode *node, CGFloat elapsedTime) {
//perform some actions
}
[myNode runAction: action];
}
The method should get called when the application enters the background.

Can't unpause SKScene after adding a label

I can pause and unpause the SKScene with a pause button(SKSpriteNode) and a simple method.
-(void) pauseGame {
SKView *view = (SKView *) self.view;
if(!view.paused){
view.paused = YES;
}else{
view.paused = NO;
[pauseLabel removeFromParent];
}
}
And call it in the touchesBegan Method
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
// If game is paused allow the button to be touched
if (isGamePaused == NO) {
//if pause button touched, pause the game
if (node == pauseButton) {
[self pauseGame];
}
}
}
This way works fine. I can push the button to pause the game and push it again to unpause the game. However, I want to give the user a way to know the game is paused other then the screen be frozen, so I tried this:
I create this SKLabel.
-(void) addPauseLabel {
// Creating label
pauseLabel = [SKLabelNode labelNodeWithFontNamed:universalFont];
pauseLabel.text = #"Game is Paused";
pauseLabel.fontSize = 42;
pauseLabel.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2 + 60);
pauseLabel.zPosition = 6;
[self addChild:pauseLabel];
}
Then I created a sequence in which I add the label then pause the game
-(void) pauseSequence{
SKAction *pauseSequence = [SKAction sequence:#[
[SKAction performSelector:#selector(addPauseLabel) onTarget:self],
[SKAction performSelector:#selector(pauseGame) onTarget:self],
]];
[self runAction:pauseSequence];
}
And finally, in the touchesBegan method, I call [self pauseSquence]; at the end instead of [self pauseGame];
When I push the pause button the label is added and the game is pauseed, but pushing the button again to unpuase the game doesn't do anything. Why is that? Any help would be greatly appreciated. Thank you.
You can't run an action while the view is paused, so you should call [self pauseSequence] if the view is not paused and [self pauseGame] if the view is paused.
In the touchesBegan method
if (!self.view.paused) {
// Add label and pause
[self pauseSequence];
}
else {
// Resume game
[self pauseGame];
}
The most likely reason your unpause button doesn't work is that after you pause, no touches can be detected. You are essentially locking yourself out of the scene, meaning the scene can't do work to unpause itself.
The generally accepted way to pause a SpriteKit game is to not pause the scene, but to create multiple nodes. One node will contain your gameplay (the "Game Layer") and the other will contain pause elements (the "Pause Layer"). When your scene detects the pause button has been pressed, set everything in the Game Layer to paused, then bring up your Pause Layer. When the resume button is pressed, hide the Pause Layer and unpause the Game Layer.
You should know that the sequence serves no purpose here. It's best to just call:
[self addPauseLabel];
[self pauseGame];
Since that's the main change it's possible that the odd and unnecessary use of a sequence causes this behavior.

iOS how to achieve UIButton's like behavior for nodes in Sprite Kit?

I would like some of my Sprite Kit nodes to behave like UIButtons. I tried 2 approaches:
1) Use touchesBegan: - this works if a user is careful, but seems to fire multiple times, faster than I can disable interaction, resulting in a button being able to be activated multiple times:
spriteNode.userInteractionEnabled = YES;
//causes the following to fire when node receives touch
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.userInteractionEnabled = NO;
DLog(#"Do node action");
self.userInteractionEnabled = YES;
}
2)I switched to Kobold-Kit as a layer on top of iOS Sprite Kit. One of the things it allows me to do is add button - like behavior to any node. However, I'm running into an issue where a if I have 2 buttons stacked on top of each other, tapping the top button activates both. Boolean flags can prevent repeated interactions in this case. I tracked the issue of stacked buttons firing together to this call within KKScene:
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
for (id observer in _inputObservers)
{
if ([observer respondsToSelector:#selector(touchesBegan:withEvent:)])
{
[observer touchesBegan:touches withEvent:event];
}
}
}
The scene simply sends notifications to all nodes within the scene. This causes buttons behaviors that are stacked on top of each other to fire together.
Is there a way or example that shows how to properly arrange sprite nodes to allow behavior similar to UIButton, where I can have only the top button activate, and each activation disables the button for a short time afterwards?
#property (nonatomic, strong) UITapGestureRecognizer *tapGesture;
self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapGestureStateChanged:)];
self.tapGesture.delegate = self;
[self.view addGestureRecognizer:self.tapGesture];
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer == self.tapGesture) {
return CGRectContainsPoint(self.neededSprite.frame, [self convertPoint:[sender locationInView:self.view] toNode:self]);
}
return YES;
}
- (void)tapGestureStateChanged:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateRecognized) {
/* do stuff here */
}
}
The SpriteKit Programming Guide suggests using a SKNode name to gate the behavior that you want to achieve when touched. The example in the section called "Using Actions to Animate Scenes" overrides the touchesBegan:withEvent: method and only runs when the name is not nil. When you're done, just reset the name so that you can catch the next touch.
- (void)touchesBegan:(NSSet *) touches withEvent:(UIEvent *)event {
SKNode *helloNode = [self childNodeWithName:#"helloNode"];
if (helloNode != nil) {
helloNode.name = nil;
SKAction *moveUp = [SKAction moveByX: 0 y: 100.0 duration: 0.5];
SKAction *zoom = [SKAction scaleTo: 2.0 duration: 0.25];
SKAction *pause = [SKAction waitForDuration: 0.5];
SKAction *fadeAway = [SKAction fadeOutWithDuration: 0.25];
SKAction *remove = [SKAction removeFromParent];
SKAction *moveSequence = [SKAction sequence:#[moveUp, zoom, pause, fadeAway, remove]];
[helloNode runAction: moveSequence];
}
}
Another idea would be to perform your re-enabling of the user interaction to touchesEnded.

How can I add a delay before it continues

Is there a way in the normal obj-c or cocos2d to make a delay inside a if-else block?
Like
if ([self isValidTileCoord:cTileCoord] && ![self isWallAtTileCoord:cTileCoord])
{
[self addChild:circle0];
//wait two seconds
//perform another task
}
Just a simple lag to wait between two tasks, or stalling an action. Is there any simple way to do this?
There are many ways to do this. I would use GCD
if ([self isValidTileCoord:cTileCoord] && ![self isWallAtTileCoord:cTileCoord])
{
[self addChild:circle0];
//wait two seconds
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_current_queue(), ^{
//perform another task;
});
}
You can use the performSelector: withObject: afterDelay: method to delay tasks:
if ([self isValidTileCoord:cTileCoord] && ![self isWallAtTileCoord:cTileCoord])
{
[self addChild:circle0];
//wait two seconds
[self performSelector:#selector(continueTask) withObject:nil afterDelay:2.0];
}
And in the selector method:
-(void)continueTask
{
//perform another task
}
Inside the cocos2d you can also run an Action on the node like,
[self runAction:[CCSequence actions:[CCDelayTime actionWithDuration:2.0],[CCCallFunc actionWithTarget:self selector:#selector(anothertaks)],nil]];
Perform selector will also work but the problem is that all the schedulers of cocos2D get paused when the app go to background but on the other side perform selector will still count the time, so some time it create task,animation,etc syncing problems.
Perform selector:
[self performSelector:#selector(continueTask) withObject:nil afterDelay:2.0];

Resources