Sprite kit - Dealing with overlapping animations in fast sequence - ios

I'm building an app in which when a sprite is pressed it performs a very simple animation in which the initial image is replaced with a new image and then after a short pause the initial image is applied again. There are 2 of such sprites.
It works all fine unless the buttons are pressed in an extremely rapid sequence.
Let's say for example that the user presses button 1 and then very quickly afterwards button 2 (before this had the time to revert back to the initial image): in this case button 1 remains stuck with the new image!
Even if I disable the interaction with the sprite (by for example renaming it while the animation is in progress or inserting different flags), still a touchesBegan event is registered and I believe this is enough to get sprite 1 stuck with the new image and not reverting back to the initial one!
I've ran out of ideas... Any suggestions please? Cheers.
- (void)SwitchButtonImage
{
touchedNode.texture = [SKTexture textureWithImageNamed:ImageNew];
//After a pause change sprite image to the initial image
SKAction *wait = [SKAction waitForDuration: 0.3];
[touchedNode runAction: wait completion:^
{
touchedNode.texture = [SKTexture textureWithImageNamed:ImageInitial];
}];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
[self selectNodeForTouch:positionInScene];
}
- (void)selectNodeForTouch:(CGPoint)touchLocation
{
touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
_selectedNode = touchedNode;
if([[touchedNode name] isEqualToString:kButtonNodeName_01])
{[self SwitchButtonImage];}
else if([[touchedNode name] isEqualToString:kButtonNodeName_02])
{[self SwitchButtonImage];}
}

EDIT: I edited my answer to handle more than one button and to work with your original code.
- (void)SwitchButtonImage
{
touchedNode.texture = [SKTexture textureWithImageNamed:#"NewImage"];
//After a pause change sprite image to the initial image
SKAction *wait = [SKAction waitForDuration: 0.3];
SKTexture *texture = [SKTexture textureWithImageNamed:#"InitialImage"];
SKAction *resetImage = [SKAction setTexture:texture];
// Add action with a unique identifier
[touchedNode runAction:[SKAction sequence:#[wait, resetImage]] withKey:#"ResetImage"];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
[self selectNodeForTouch:positionInScene];
}
- (void)selectNodeForTouch:(CGPoint)touchLocation {
touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
_selectedNode = touchedNode;
if([[touchedNode name] isEqualToString:kButtonNodeName_01]) {
// Change image only if previous action is done
if (![touchedNode actionForKey:#"ResetImage"]) {
[self SwitchButtonImage];
}
}
else if([[touchedNode name] isEqualToString:kButtonNodeName_02]) {
// Change image only if previous action is done
if (![touchedNode actionForKey:#"ResetImage"]) {
[self SwitchButtonImage];
}
}
}
EDIT 2: You can add a key to an SKAction that has a completion block, but you can add a runBlock to the end of an SKAction sequence to do that same. Here's an example:
SKAction *action1 = [SKAction rotateByAngle:M_PI duration:1];
SKAction *action2 = [SKAction rotateByAngle:-M_PI duration:1];
SKAction *completed = [SKAction runBlock:^{
NSLog(#"I am done");
}];
SKAction *action = [SKAction sequence:#[action1, action2, completed]];
[sprite runAction:action withKey:#"actionKey"];

You can do this by using blocks and a BOOL ivar.
Create an ivar:
BOOL readyForNextAnimation;
Set up your blocks something like this:
SKAction *block0 = [SKAction runBlock:^{
// your animation code here
}];
SKAction *wait0 = [SKAction waitForDuration:0.3]; // your animation time length
SKAction *block1 = [SKAction runBlock:^{
readyForNextAnimation = true;
}];
[self runAction:[SKAction sequence:#[block0, wait0, block1]]];
The above code would then be used something like this:
// some trigger just occurred
if(readyForNextAnimation == true)
{
readyForNextAnimation = false;
// create blocks 0,1,2 and run them as above
}
If you need to do other things, just create more blocks and add them into the run sequence.

Related

iOS SpriteKit - Touch at a specific location, want node at specific zPosition

I have a project where when I tap I check to see if the tap is on a specific node. If it is then I create a new SpriteNode at that position that is only visible for .1s before being deleted. I want to be able to spam tapping on the screen but with that current check the node that I end up tapping on is the new SpriteNode that I've created.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
if (node == shutdahellup){
[self runAction:[SKAction playSoundFileNamed:#"shut-da-hell-uh.mp3" waitForCompletion:NO]];
}else if (node == hotdamn){
[self runAction:[SKAction playSoundFileNamed:#"hot-damn.mp3" waitForCompletion:NO]];
}else if (node == wafflewednesday){
[self runAction:[SKAction playSoundFileNamed:#"waffle-wednesday.mp3" waitForCompletion:NO]];
}else if (node == damnwaffle){
[self runAction:[SKAction playSoundFileNamed:#"get-a-damn-waffle.mp3" waitForCompletion:NO]];
}
[self createFace:touch];
}
}
-(void) createFace:(UITouch *)touch {
SKSpriteNode * face = [SKSpriteNode spriteNodeWithImageNamed:#"shane.png"];
CGPoint location = [touch locationInNode:self];
face.position = location;
face.zPosition = 100;
[self addChild:face];
SKAction * wait = [SKAction waitForDuration:0.1];
SKAction * remove = [SKAction removeFromParent];
SKAction * sequence = [SKAction sequence:#[wait, remove]];
[face runAction:sequence];
}
Any ideas? Thanks!

How can I make a functioning animated button?

I have a button set up in a scene like this:
self.playButton = [SKSpriteNode spriteWithImageNamed:#"playbutton.png"];
self.playButton.size = CGSizeMake(100,100);
self.playButton.position = CGPointMake(CGRectGetMidX(self.frame), 100);
[self addChild:self.playButton];
Then in touchesBegan:
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
if([self.playButton containsPoint:location]){
self.playButton.texture = [SKTexture textureWithImageNamed:#"playbuttonpressed.png"];
[self.playButton runAction: [self touchButtonAction]];
}
Then in touchesEnded:
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
if([self.playButton containsPoint:location]){
self.playButton.texture = [SKTexture textureWithImageNamed:#"playbutton.png"];
[self.view presentScene:self.nextScene transition:self.sceneTransition];
}
Then in touchButtonAction:
SKAction *toSmall = [SKAction scaleBy:0.8 duration:0.1];
return toSmall;
Here's the problem: When I tap the button, touchesEnded gets called before the action is finished, which causes for an incomplete animation which doesn't look good (it seems laggy). How can I make a button that finishes the animation before transitioning?
There are a couple of ways of accomplishing what you want. One of those is using blocks.
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
if([self.playButton containsPoint:location]){
SKAction *block0 = [SKAction runBlock:^{
self.playButton.texture = [SKTexture textureWithImageNamed:#"playbuttonpressed.png"];
[self.playButton runAction: [self touchButtonAction]];
}];
SKAction *wait0 = [SKAction waitForDuration:1.0]; // <- however long it takes for the animation to complete
SKAction *block1 = [SKAction runBlock:^{
self.playButton.texture = [SKTexture textureWithImageNamed:#"playbutton.png"];
[self.view presentScene:self.nextScene transaction:self.sceneTransaction];
}];
[self runAction:[SKAction sequence:#[block0, wait0, block1]]];
}
For more information on using blocks, you can read the Apple Blocks Programming Guide.

stop Impulse on sprite touch [duplicate]

This question already has an answer here:
stop impulse on SKSpriteKit click
(1 answer)
Closed 8 years ago.
i'm creating an game where there is a pause button. For checking wether its touched i'm checking the location of the touch and if its equal to the paused button name.
When the pausedButton is clicked its call a method which pause the scene.
The problem is that in the touchBegan method whenever you touch the screen it apply an impulse, so when i press the pauseButton and unpause it the applyforce will come after. This is not ideal for the game. I've tried with a bool like shouldImpulse, but havent got it to work. here is my touchedBegan method:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
if ([node.name isEqualToString:#"home"]) {
SKTransition *reveal = [SKTransition fadeWithDuration:2.0 ];
Menu *newScene = [[Menu alloc] initWithSize: CGSizeMake(self.size.width,self.size.height)];
// Optionally, insert code to configure the new scene.
[self.scene.view presentScene: newScene transition: reveal];
}
if ([node.name isEqualToString:#"pause"]) {
[self pausedMenu];
}
if ([node.name isEqualToString:#"start"]) {
[self startMenu];
}
showpipes = showpipes + 1;
if (showpipes == 1) {
self.physicsWorld.gravity = CGVectorMake( 0.0, -5.0 );
SKAction* spawn = [SKAction performSelector:#selector(spawnPipes) onTarget:self];
SKAction* delay = [SKAction waitForDuration:2.0];
SKAction* spawnThenDelay = [SKAction sequence:#[spawn, delay]];
SKAction* spawnThenDelayForever = [SKAction repeatActionForever:spawnThenDelay];
[self runAction:spawnThenDelayForever];
}
started = 1;
if (started == 1) {
mover.physicsBody.restitution = 0.0;
mover.physicsBody.velocity = CGVectorMake(0, 0);
[mover.physicsBody applyImpulse:CGVectorMake(0, 15)];
}
}
You can do that with a simple IF, ELSE IF, ELSE:
if([node.name isEqualToString:#"home"])
{
// do stuff...
} else if ([node.name isEqualToString:#"pause"])
{
// do stuff...
} else if ([node.name isEqualToString:#"start"])
{
// do stuff...
} else
{
// do whatever else here...
}

After rotating SKLabelNode it becomes difficult to get location in touchesBegan:

I have an SKLabel Node setup as a button in my app and it works fine. However, when I rotate the SKLabelNode with this code my touchesBegan method no longer executes correctly and it becomes very difficult to have the SKLabelNode touches register correctly:
[_menuButton runAction:[SKAction sequence:#[[SKAction rotateByAngle:M_PI duration:.2],
[SKAction moveByX:0 y:-15 duration:.2]]]];
Here is my touches began method:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
CGPoint positionInScene = [touch locationInNode:self];
[self selectNodeForTouch:positionInScene];
if ([node.name isEqualToString:menuButtonName]) {
//Touched Menu Button
[self animateMenuAndShow:YES];
}
}
EDIT: One additional piece of information is that I'm using SKTEffects which requires that instead of using self.scene as my base I'm using _worldLayer to add all elements in the scene to. Is it possible that this is throwing off my touch calculations? And if so how can I correct this?
self.scaleMode = SKSceneScaleModeResizeFill;
self.anchorPoint = CGPointMake(0.5, 0.5);
// The origin of the pivot node must be the center of the screen.
_worldPivot = [SKNode node];
[self addChild:_worldPivot];
// Create the world layer. This is the only node that is added directly
// to the pivot node. If you have a HUD layer you would add that directly
// to the scene and make it sit above the world layer.
_worldLayer = [SKNode node];
_worldLayer.position = self.frame.origin;
[_worldPivot addChild:_worldLayer];
Also after following sangony's advice here is my updated touchesBegan: method which works well at first, but runs into the same problem after rotating the label by M_PI
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:_worldLayer];
CGPoint locationInMenu = [touch locationInNode:_menuBackground];
if (CGRectContainsPoint(_menuButton.frame, location)) {
//Touched Menu Button
if (!self.menuVisible) {
[self animateMenuAndShow:YES];
}
else
{
[self animateMenuAndShow:NO];
}
}
}
EDIT2:
Here is my animateMenuAndShow: method
-(void)animateMenuAndShow:(BOOL)show
{
if (show == YES) {
// self.paused = YES;
self.menuVisible = !self.menuVisible;
[self createMenu];
SKAction *showMenuAction = [SKAction sequence:#[
[SKAction moveTo:CGPointMake(self.size.width / 2, self.size.height/2) duration:0.3],
[SKAction runBlock:^{
[self jelly:self.menuBackground];
}],
[SKAction waitForDuration:1.5]
]];
[self.menuBackground runAction: showMenuAction completion:^{
self.paused = YES;
}];
if (_flipped == YES) {
[self.menuBackground runAction:[SKAction sequence:#[
[SKAction moveByX:0 y:0 duration:.2],[SKAction rotateByAngle:M_PI duration:.2]]]];// [SKAction rotateByAngle:M_PI duration:.2]
}
}
else
{
self.paused = NO;
SKAction *removeMenuAction = [SKAction sequence:#[
[SKAction moveTo:CGPointMake(self.size.width / 2, -self.size.height + 300) duration:0.3],
]];
[self.menuBackground runAction:removeMenuAction completion:^{
[self.menuBackground removeFromParent];
}];
self.menuVisible = !self.menuVisible;
}
}
Try this touchesBegan instead:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint touchLocation = [touch locationInNode:self.scene];
if (CGRectContainsPoint(myLabel.frame, touchLocation))
{
[self animateMenuAndShow:YES];
}
}

SpriteKit crash on run action on other SKScene

I have a crash in interesting form. I setUp SKAction like
-(id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size])
{
self.backgroundColor = [SKColor colorWithWhite:255 alpha:1];
[self createNinja];
[self setUpJump];
}
return self;
}
- (void)setUpJump
{
SKTextureAtlas *jumpAtlas = [SKTextureAtlas atlasNamed:#"Ninja_jump"];
SKTexture *jump1 = [jumpAtlas textureNamed:#"Ninja_jump_1"];
SKTexture *jump2 = [jumpAtlas textureNamed:#"Ninja_jump_2"];
SKTexture *jump3 = [jumpAtlas textureNamed:#"Ninja_jump_3"];
SKTexture *jump4 = [jumpAtlas textureNamed:#"Ninja_jump_4"];
SKAction *jumpUpAnimation = [SKAction animateWithTextures:#[jump1, jump2, jump3, jump4]
timePerFrame:0.07];
SKAction *jumpDownAnimation = [SKAction animateWithTextures:#[jump3, jump2, jump1, [SKTexture textureWithImageNamed:#"Ninja"]]
timePerFrame:0.07];
SKAction *wait = [SKAction waitForDuration:0.3];
self.jumpAction = [SKAction sequence:#[jumpUpAnimation, wait, jumpDownAnimation]];
}
But when I don't run this action on first SKScene and go to other SKScene, when I set up the same action, I have a crash
But if I run this action on first SKScene, everything is OK on next SKScene.
Is there problem in SpriteKit in SKTexture?
Run my action like that
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches)
{
CGPoint location = [touch locationInNode:self];
SKSpriteNode *ninja = (SKSpriteNode *)[self childNodeWithName:#"ninja"];
if (location.x > 230 &&
(location.x < 419 && location.y > 500))
{
[ninja runAction:self.jumpAction];
}
}
}
Not 100% sure what you are saying but if I read you right, you are setting up an SKAction property jumpAction in the first scene and are referring to that action in the other scene's touchesBegan: method(?):
[ninja runAction:self.jumpAction];
If you are not setting up the self.jumpAction for your second scene, then you are essentially saying [ninja runAction:nil];
I personally find it a good idea to tie actions as close as possible to the node that is supposed to perform it (in this case the sprite). If you are reusing a lot of actions for different nodes creating an ActionFactory class could be another way to go about it...

Resources