stopping and continuing a SKAction - ios

How we can stop an action on certain frame (texture)? I want to make an animation which stops after the touch is ended. I searched trough the docs, but with no luck. The only thing I have found is methods removeAllActions and removeActionForKey: but I don't want to completely remove an action, I just want to stop it at the right texture - the current texture when the animating signal is stopped.
//This is from the scene init method
NSMutableArray *frames = [NSMutableArray array];
SKTextureAtlas *atlas = [SKTextureAtlas atlasNamed:#"hero-movement"];
int numImages = atlas.textureNames.count;
for (int i=1; i <= numImages; i++) {
NSString *texture = [NSString stringWithFormat:#"animation_00%d", i];
SKTexture *t = [atlas textureNamed:texture];
[frames addObject:t];
}
self.animation = frames;
And this is the method which I use in touchesBegan to start animation:
-(void)move :(float)delay
{
//This is our general runAction method to make our bear walk.
//By using a withKey if this gets called while already running it will remove the first action before
//starting this again.
[self.hero runAction:[SKAction repeatActionForever:[SKAction animateWithTextures:self.animation
timePerFrame:delay
resize:NO
restore:YES]] withKey:#"move"];
return;
}

You can use the speed property to pause/resume an action:
SKAction *action = [_hero actionWithKey:#"move"];
action.speed = 0; // to pause
or
action.speed = 1; // to resume

Related

Gradually increase speed of SpriteKit objects

I have a simple game with stones "raining" from top to bottom of the screen.
I am working with SpriteKit and I would like the stones to increase their speed by time like it would be level 1 level 2 level 3 and so on. Maximum speed should be easy to implement then.
I have a variable "speed" for the speed of the stones.
I think I could just use a timer and after 5s speed = 2, after 10s speed = 5, and so on, but I'm not sure how I can implement that.
EDIT:
I'm moving my stones with this:
-(void) moveStones{
SKAction *actionMove = [SKAction moveByX:0 y:-5 duration:0.25];
for(SKSpriteNode *stone in self.stones)
{
[stone runAction:actionMove];
}
}
And in my viewDidLoad I have the timer:
moveStones = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(moveStones) userInfo:nil repeats:YES];
If the timer does not respect the view, is there a way to move them without that timer?
If you are moving stones by physics, then after the stone is spawned you should apply an stronger impulse/force (based on current level) If you are moving them with actions, then change the speed of action, or lower the moveDuration. If you are moving stones by changing its position in update: method, then change the value which indicates how much points a sprite should move in every update call.
To implement time based actions like spawning stones or to decide when to change the speed of spawned nodes, you should use SKAction, or update: method and its passed parameter called currentTime. Using NSTimer for any of this in SpriteKit can lead to problems, because NSTimer don't respect view's , scene's or node's paused state, so its better to avoid using it.
Here is an example on how you can do what you want with SKAction. In this example you can see how to:
track the number of seconds passed
maintain the info about current level
spawn node with random delay
stop spawning nodes by using action key
and probably some more
GameScene.h
#import "GameScene.h"
#interface GameScene()
#property (nonatomic, assign) NSInteger counter;
#property (nonatomic, assign) NSInteger level;
#property (nonatomic, strong) SKLabelNode *debug;
#end
#implementation GameScene
// Here we set initial values of counter and level. Debug label is created here as well.
-(void)didMoveToView:(SKView *)view {
self.counter = 0;
self.level = 1;
self.backgroundColor = [SKColor grayColor];
self.debug = [SKLabelNode labelNodeWithFontNamed:#"ArialMT"];
self.debug.fontColor = [SKColor purpleColor];
self.debug.fontSize = 30.0f;
self.debug.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
self.debug.text = [NSString stringWithFormat:#"Counter : [ %ld ], Level [ %ld ]", (long)self.counter,(long)self.level];
[self addChild:self.debug];
}
//Method to start a timer. SKAction is used here to track a time passed and to maintain current level
-(void)startTimer{
__weak typeof (self) weakSelf = self; //make a weak reference to scene to avoid retain cycle
SKAction *block = [SKAction runBlock:^{
weakSelf.counter++;
//Maintaining level
if(weakSelf.counter < 5){
//level 1
weakSelf.level = 1;
}else if(weakSelf.counter >=5 && weakSelf.counter <10){
//level 2
weakSelf.level = 2;
}else{
//level 3
weakSelf.level = 3;
}
weakSelf.debug.text =
[NSString stringWithFormat:#"Counter : [ %ld ], Level [ %ld ]", (long)weakSelf.counter,(long)weakSelf.level];
}];
[self runAction:[SKAction repeatActionForever:[SKAction sequence:#[[SKAction waitForDuration:1], block]]] withKey:#"counting"];
}
//Method for stopping timer and reseting everything to default state.
-(void)stopTimer{
if([self actionForKey:#"counting"]){
[self removeActionForKey:#"counting"];
}
self.counter = 0.0f;
self.level = 1;
self.debug.text =
[NSString stringWithFormat:#"Counter : [ %ld ], Level [ %ld ]", (long)self.counter,(long)self.level];
}
//Get current speed based on time passed (based on counter variable)
-(CGFloat)getCurrentSpeed{
if(self.counter < 5){
//level 1
return 1.0f;
}else if(self.counter >=5 && self.counter <10){
//level 2
return 2.0f;
}else{
//level 3
return 3.0f;
}
}
//Method which stop generating stones, called in touchesBegan
-(void)stopGeneratingStones{
if([self actionForKey:#"spawning"]){
[self removeActionForKey:#"spawning"];
}
}
//You can use this useful method to generate random float between two numbers
- (CGFloat)randomFloatBetween:(CGFloat)smallNumber and:(CGFloat)bigNumber {
CGFloat diff = bigNumber - smallNumber;
return (((CGFloat) (arc4random() % ((unsigned)RAND_MAX + 1)) / RAND_MAX) * diff) + smallNumber;
}
//Method for generating stones, you run this method when you want to start spawning nodes (eg. didMoveToView or when some button is clicked)
-(void)generateStones{
SKAction *delay = [SKAction waitForDuration:2 withRange:0.5]; //randomizing delay time
__weak typeof (self) weakSelf = self; //make a weak reference to scene to avoid retain cycle
SKAction *block = [SKAction runBlock:^{
SKSpriteNode *stone = [weakSelf spawnStoneWithSpeed:[weakSelf getCurrentSpeed]];
stone.zPosition = 20;
[weakSelf addChild:stone];
}];
[self runAction:[SKAction repeatActionForever:[SKAction sequence:#[delay, block]]] withKey:#"spawning"];
}
//Returns stone with moving action added. Inside, you set standard things, like size, texture, physicsbody, name and position of a stone
-(SKSpriteNode*)spawnStoneWithSpeed:(CGFloat)stoneSpeed{
CGSize stoneSize = CGSizeMake(30,30); //you can randomize size here
CGPoint stonePosition = CGPointMake( [self randomFloatBetween:0.0f and:self.frame.size.width], CGRectGetMaxY(self.frame)); //you can randomize position here
SKSpriteNode *stone = [SKSpriteNode spriteNodeWithColor:[SKColor greenColor] size:stoneSize];
stone.name = #"stone"; //this helps if you want to enumerate all stones by name later on in your game
stone.position = stonePosition;
SKAction *move = [SKAction moveByX:0 y:-200 duration:3.25];
//one way to change speed
move.speed = stoneSpeed;
SKAction *moveAndRemove = [SKAction sequence:#[move, [SKAction removeFromParent]]];
[stone runAction:moveAndRemove withKey:#"moving"]; //access this key if you want to stop movement
return stone;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Called when a touch begins */
//just a simple way to start and stop a game
if(![self actionForKey:#"counting"]){
[self startTimer];
[self generateStones];
}else{
[self stopTimer];
[self stopGeneratingStones];
}
}
#end
And here is the result:
Now its up to you how you will set the speed. You can play with that.
If you want to skip some parts, or you don't know where to look, the heart of everything is in method called generateStones That is how you can spawn stones with a time delay by using SKActions. The another important method is spawnStoneWithSpeed: where you can see how to manipulate with speed of an action.
Another way to affect on node's moving speed, as I said, is to change duration parameter of method moveByX: duration:
Hope this helps!

creating physics body for a SKSpriteNode in order to detect contact

as the title suggest, i have issue with creating contact between my enemies sprite and the hero laser(bullet). i create my enemies trough the following method and am adding them to the view.
-(void)Enemies{
//not always come
int GoOrNot = [self getRandomNumberBetween:0 to:1];
if(GoOrNot == 1){
SKSpriteNode *enemy;
int randomEnemy = [self getRandomNumberBetween:0 to:1];
if(randomEnemy == 0)
enemy = [SKSpriteNode spriteNodeWithImageNamed:#"enemy_spaceship.png"];
else
enemy = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship15.png"];
enemy.scale = 0.4;
enemy.position = CGPointMake(450, 0);
enemy.hidden = NO;
CGPoint location = CGPointMake(450, 320);
SKAction *moveAction = [SKAction moveTo:location duration:2.5];
SKAction *doneAction = [SKAction runBlock:(dispatch_block_t)^() {
//NSLog(#"Animation Completed");
enemy.hidden = YES;
}];
SKAction *moveAsteroidActionWithDone = [SKAction sequence:#[moveAction,doneAction ]];
[enemy runAction:moveAsteroidActionWithDone withKey:#"asteroidMoving"];
[self addChild:enemy];
_enemyBullets = [[NSMutableArray alloc] initWithCapacity:kNumEnemyBullet];
for (int i = 0; i < kNumEnemyBullet; ++i) {
SKSpriteNode *enemyBullets = [SKSpriteNode spriteNodeWithImageNamed:#"rocket"];
enemyBullets.hidden = YES;
[enemyBullets setXScale:0.5];
[enemyBullets setYScale:0.5];
[_enemyBullets addObject:enemyBullets];
[enemy addChild:enemyBullets];
}
}
}
then i add the bullets through a mutable array and then adding my bullets to the enemies sprites as their child. that part works. i can create contact between hero and the enemy bullet and it gets detected. what i have issue with is that my hero's laser does not create contact with the enemy thus i can't make the enemy take hit from the laser. i tries adding the physics body to the method i am using but throws all the other sprites into hell and they don't respond properly anymore.
the following code is my collision code which i am using in update method:
for (SKSpriteNode *enemyBullets in _enemyBullets) {
if (enemyBullets.hidden) {
continue;
}
if ([_ship intersectsNode:enemyBullets]) {
enemyBullets.hidden = YES;
SKAction *blink = [SKAction sequence:#[[SKAction fadeOutWithDuration:0.1],
[SKAction fadeInWithDuration:0.1]]];
SKAction *blinkForTime = [SKAction repeatAction:blink count:4];
SKAction *shipExplosionSound = [SKAction playSoundFileNamed:#"explosion_large.caf" waitForCompletion:NO];
[_ship runAction:[SKAction sequence:#[shipExplosionSound,blinkForTime]]];
_lives--;
_livesLabel.text = [NSString stringWithFormat:#"%d Lives", _lives];
NSLog(#"your ship has been hit!");
}
}
is there any way that i can use to create a physics body for my enemies so i can create the contact between the hero laser and the enemies sprite with the same structure i have currently ? any help is amazingly appreciated.
You most certainly can create a physics body for your enemies. Right after this code enemy = [SKSpriteNode spriteNodeWithImageNamed:#"enemy_spaceship.png"]; you can add a physics body like this:
enemy.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.size];
enemy.physicsBody.categoryBitMask = CategoryEnemy; // or whatever you need
enemy.physicsBody.collisionBitMask = CategoryRock; // or whatever you need
enemy.physicsBody.contactTestBitMask = CategoryBullet // or whatever you need
enemy.physicsBody.dynamic = NO; // or YES
enemy.physicsBody.allowsRotation = NO; // or YES
enemy.physicsBody.restitution = 0; // or another value between 0 to 1
enemy.physicsBody.friction = 0.0; // or another value between 0 to 1
There are many ways to create a physics body. You can do a rectangle shape, circle shape, outline a texture or draw your own.
You should read the Apple docs on this subject to get a better understanding of what you can do and what properties are available.

How to run actions on previously generated SpriteNodes in SpriteKit

I am making an app, where objects fall from the top of the screen. These objects are spriteNodes and are generated with the method - (void)generateNodes. When you tap on the screen a touchPointer node is created and detects if you touch the spriteNode. This is done with a didBeginContact method. I have the detection of the touch, however, when I try to add a death animation to the objects, there is a problem. If I tap an object and it begins its death animation, then I tap another object that has been newly generated. The death animation from the previous continues with the newly generated object. So, instead of starting the newly generated object's own death animation – basically fading out, it will continue the death animation from the previous object as that object finishes.
Example:
firstObject alpha when tapped 0.5 -> 0
== second object is tapped whilst firstObject is still running the death animation ==
secondObject alpha when tapped 0.2 -> 0
This is because when I tap the secondObject it continues the animation of the first object. I want to it to be where the secondObject starts its own animation, where it should start from an alpha of 0.5
Code of didBeginContact
if([contact.bodyA.node.name isEqualToString:#"object"] && [contact.bodyB.node.name isEqualToString:#"touchPointer"]){
//Object Death Animation Actions
int randObjectMovement = (arc4random()%2) + 1;
touchedObject = [objects objectAtIndex:0];
touchedObject.alpha = 0.5;
if(randObjectMovement == 1){
touchedObject.physicsBody.velocity = CGVectorMake(0, 0);
[touchedObject.physicsBody applyImpulse:CGVectorMake(-10, 18)];
NSLog(#"Impluse completed");
}else{
touchedObject.physicsBody.velocity = CGVectorMake(0, 0);
[touchedObject.physicsBody applyImpulse:CGVectorMake(10, 18)];
NSLog(#"Impluse completed");
}
objectFadeOut = [SKAction fadeAlphaTo:0 duration:0.75];
objectRemove = [SKAction removeFromParent];
objectDeathRotation = [SKAction rotateByAngle:M_PI*2 duration:3];
[touchedObject runAction:objectDeathRotation completion:^{
NSLog(#"objectDeathRotation completed");
}];
[touchedObject runAction:objectFadeOut completion:^{
NSLog(#"objectFadeOut and Removal completed");
[touchedObject runAction:objectRemove];
}];
}
Code of generateObjects
- (void)generateObject{
//Object Texture delcaration
if ([self generateObjectType] <= 100 && [self generateObjectType] > 20){
objectTexture = [SKTexture textureWithImageNamed:#"Object_N"];
}else if([self generateObjectType] <= 20 && [self generateObjectType] > 5){
objectTexture = [SKTexture textureWithImageNamed:#"Object_B"];
}else{
objectTexture = [SKTexture textureWithImageNamed:#"Object_G"];
}
objectTexture.filteringMode = SKTextureFilteringNearest;
//Object Actions
objectRotation = [SKAction rotateByAngle:M_PI*2 duration:1];
//Object Sprite declaration
objectSprite = [SKSpriteNode spriteNodeWithTexture:objectTexture];
objectSprite.position = [self generateObjectPosition];
objectSprite.zPosition = 3;
objectSprite.size = CGSizeMake(30, 30);
[objectSprite runAction:objectRotation];
objectSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSprite.size];
objectSprite.name = #"object";
objectSprite.physicsBody.categoryBitMask = objectCategory;
objectSprite.physicsBody.collisionBitMask = groundCategory;
objectSprite.physicsBody.contactTestBitMask = groundCategory | touchCategory;
[objects insertObject:objectSprite atIndex:0];
[self addChild:objectSprite];
}
By the way, objects is an NSMutableArray.
Please help!
Why not just use the node from the contact:
SKSpriteNode *touchedObject = (SKSpriteNode*)contact.bodyA.node;
This gets the node involved in the collision and you can carry out the actions on it, no need for the array.
Update
Use local instances of the actions:
SKAction *objectFadeOutLocal = [SKAction fadeAlphaTo:0 duration:0.75];
SKAction *objectRemoveLocal = [SKAction removeFromParent];
SKAction *objectDeathRotationLocal = [SKAction rotateByAngle:M_PI*2 duration:3];

SpriteKit Animation Issue

I am having an issue with the sprite animation in my game.
In the current code posted below, the sprite will show on the screen but does not do the walking animation; it is only a static frame of one of the images in the walk cycle.
The init function:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize: size]) {
self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
// Create the game nodes
// Background
_backgroundNode = [self createBackgroundNode];
[self addChild: _backgroundNode];
// Foreground
_foregroundNode = [SKNode node];
[self addChild: _foregroundNode];
// Add the Player
_thePlayer = [self createPlayer];
[_foregroundNode addChild:_thePlayer];
[self walkingQueen];
}
return self;
}
Function creating the player / filling in the animation array:
-(SKSpriteNode *)createPlayer
{
SKSpriteNode *playerNode = [SKSpriteNode node];
//SKSpriteNode *_player = [SKSpriteNode node];
NSMutableArray *walkFrames = [NSMutableArray array];
SKTextureAtlas *queenAnimatedAtlas = [SKTextureAtlas atlasNamed: #"QueenSprites"];
SKTexture *walk1 = [queenAnimatedAtlas textureNamed:#"queen7"];
SKTexture *walk2 = [queenAnimatedAtlas textureNamed:#"queen8"];
SKTexture *walk3 = [queenAnimatedAtlas textureNamed:#"queen9"];
walkFrames[0] = walk1;
walkFrames[1] = walk2;
walkFrames[2] = walk3;
_queenWalkingFrames = walkFrames;
/*
int numImages = 9;
for (int i = 7; i <= numImages; i++) {
NSString *textureName = [NSString stringWithFormat:#"queen%d", i];
SKTexture *temp = [queenAnimatedAtlas textureNamed: textureName];
[walkFrames addObject: temp];
}
*/
//_queenWalkingFrames = walkFrames;
SKTexture *temp = _queenWalkingFrames[0];
SKSpriteNode *_player = [SKSpriteNode spriteNodeWithTexture:temp];
_player.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
//[self addChild:_player];
//[self walkingQueen];
[playerNode addChild:_player];
[self walkingQueen];
return playerNode;
}
And the function for starting the walk animation:
-(void)walkingQueen
{
for (int i = 0; i < _queenWalkingFrames.count; i++) {
if (_queenWalkingFrames[i] == NULL) {
NSLog(#"ERROR!");
}
}
[_thePlayer runAction:[SKAction repeatActionForever:
[SKAction animateWithTextures:_queenWalkingFrames
timePerFrame:0.1f
resize:NO
restore:YES]] withKey:#"walkingInPlaceQueen"];
//return;
}
I played around with the code a bit and the animation DOES work IF I ignore the concept of layers (background / foreground layers) and stick the creation of the player into the init function (basically taking what's inside the createPlayer() and sticking it inside initWithSize()).
I do need to have the layers for my game and everything seems like it should work; had no idea that moving a few lines of code into its own function block would create such an issue. The check inside the walkingQueen() function did not return any errors so I assume my array has been filled with the sprite images.
_thePlayer = [self createPlayer];
This means _thePlayer is nil until after the createPlayer method returns. Thus in the walkingQueen method _thePlayer is nil and the actions don't run.
Replace this:
SKSpriteNode *playerNode = [SKSpriteNode node];
with
_thePlayer = [SKSpriteNode node];
and use _thePlayer instead of playerNode. You can also skip returning and assigning it, it's an ivar after all.
Re-wrote the createPlayer() function. Correct implementation is much simpler and as follows:
-(SKSpriteNode *)createPlayer
{
NSMutableArray *walkFrames = [NSMutableArray array];
SKTextureAtlas *queenWalkingAtlas = [SKTextureAtlas atlasNamed:#"QueenSprites"];
SKTexture *walk1 = [queenWalkingAtlas textureNamed:#"queen7"];
SKTexture *walk2 = [queenWalkingAtlas textureNamed:#"queen8"];
SKTexture *walk3 = [queenWalkingAtlas textureNamed:#"queen9"];
walkFrames[0] = walk1;
walkFrames[1] = walk2;
walkFrames[2] = walk3;
_queenWalkingFrames = walkFrames;
SKTexture *temp = _queenWalkingFrames[0];
_thePlayer = [SKSpriteNode spriteNodeWithTexture:temp];
_thePlayer.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
return _thePlayer;
}
Basically doing as the post above suggested, get rid of the ivars, use the function ONLY to fill the array with the appropriate images. Then, in the init function, call createPlayer(), add it to the foreground layer, and then call walkingQueen().

how to move sprite with delay in cocos2d?

i have number of sprite in array. now i want to moving that sprite with delay time 0.5.i am use below code at that time all sprite are fall at same time but i want to falling the sprite one by one.i am also use CCDelay method but also not get required result.
for (int j = 1; j < [ary count]; j++)
{
torpedoOne.position = ccp(160,580);
id actionMove = [CCMoveTo actionWithDuration:2.0
position:ccp(30 + (j*25),300)];
id deleay = [CCDelayTime actionWithDuration:1.0];
[torpedoOne runAction:[CCSequence actions:actionMove,deleay,nil]];
[self addChild:torpedoOne];
}
first of all for loop is complete after the action is run so that all sprite has same acion with same time.
how can i run action when each time go in for loop?
i am aslo try COCOS2D: how to animate falling bricks into a grid
you logic is weird. Try
for (int j = 0;j<[ary count]; j++{ // gets all objects in ary : 0 to count-1
torpedoOne = [ary objectAtIndex:j]; // I am assuming this is what you wanted
torpedoOne.position = ccp(160,580);
id actionMove = [CCMoveTo actionWithDuration:2.0
position:ccp(30 + (j*25),300)];
float delayTime = j*0.5f;
torpedoOne.visible = NO;
id show = [CCShow action]; // if you want them invisible prior to start move
id delay = [CCDelayTime actionWithDuration:delayTime];
[torpedoOne runAction:[CCSequence actions:delay,show,actionMove,nil]];
}
also, you should set torpedoOne inside the loop.
After a long time,i got one by one with sprite with animation by particular delay in cocos2d .
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
^{
for (int j = 1; j < [ary count]; j++)
{
dispatch_async(dispatch_get_main_queue(),
^{
torpedoOne.position = ccp(160,580);
id actionMove = [CCMoveTo actionWithDuration:2.0
position:ccp(30 + (j*25),300)];
id deleay = [CCDelayTime actionWithDuration:1.0];
[torpedoOne runAction:[CCSequence actions:actionMove,deleay,nil]];
[self addChild:torpedoOne];
});
[NSThread sleepForTimeInterval:delay];
}
});

Resources