I want to move two (or more) SKSpriteNodes in sync. Any difference will show. I tried to trigger the SKAction for each sprite in order and when the last one is finished it triggers a new move. But it turns out that the actions doesn't end in the same order they are started, which causes a slightly time difference that is noticeable.
Is there a way to run parallel SKActions on two or more sprites with a duration so that they end at exactly the same time or at least in the order they are started?
Here is an principle example of what is not working:
- (void)testMethod1{
SKSpriteNode *child_1=[arrayWithSprites objectAtIndex:1];
SKSpriteNode *child_2=[arrayWithSprites objectAtIndex:2];
//This doesn't work.
[child_1 runAction:[SKAction moveToX:20.0 duration:0.5]];
[child_2 runAction:[SKAction moveToX:20.0 duration:0.5]
completion:^{[self testMethod1];}];
//Actions might not be finished in the order they are started.
}
And here is a way I haven't tried yet but wonder if it might solve my problem:
- (void)testMethod2{
SKSpriteNode *child_1=[arrayWithSprites objectAtIndex:1];
SKSpriteNode *child_2=[arrayWithSprites objectAtIndex:2];
//Will this guarantee total syncronisation?
[self runAction:[SKAction group:[NSArray arrayWithObjects:
[SKAction runBlock:^{[child_1 runAction:[SKAction moveToX:20.0 duration:0.5]];}],
[SKAction runBlock:^{[child_2 runAction:[SKAction moveToX:20.0 duration:0.5]];}],
nil]]
completion:^{[self testMethod2];}];
}
I hope my English and thoughts are understandable.
//Micke....
Just add a container SKNode, and then they will both move at once:
SKNode *containerNode = [[SKNode alloc] init];
[containerNode addChild:node1];
[containerNode addChild:node2]; //add as many as necessary
[containerNode runAction:someAction]; //declare the action you want them both to perform
The solution by Tyler works perfectly.
But if you can't or don't want to do that, you should know that very likely actions are multithreaded and thus they can finish in any order, but they should still finish in the same frame.
To ensure they both ran to completion before running testMethod, you should perform the code in didEvaluateActions. Then you need to find a way to figure out whether both actions have finished. One way is to use a key.
[child_1 runAction:[SKAction moveToX:20.0 duration:0.5] withKey:#"action1"];
[child_2 runAction:[SKAction moveToX:20.0 duration:0.5] withKey:#"action2"];
Then check if both of these actions have run to completion by checking if they still exist:
-(void) didEvaluateActions
{
if ([child_1 actionForKey:#"action1"] == nil &&
[child_2 actionForKey:#"action2"] == nil)
{
// both actions have ended, start new ones here ...
}
}
Alternatively you can use a completion block that both actions run. Increase a NSUInteger counter variable and in the block increase the counter by 1, then test if the counter's value is equal to (or greater) than the number of concurrent actions. If it is, you know that both actions have run to completion:
__block NSUInteger counter = 0;
void (^synchBlock)(void) = ^{
counter++;
if (counter == 2)
{
[self testMethod1];
}
};
[child_1 runAction:[SKAction moveToX:20.0 duration:0.5] completion:synchBlock];
[child_2 runAction:[SKAction moveToX:20.0 duration:0.5] completion:synchBlock];
Is your framerate constant ? Your first example technically should work, however the completion order might be based on something else, like their draw order.
One concept you could employ is to temporarily encapsulate them in a container node, and then move just the container node via an action. When the action is complete remove them from the container and back to their original parent.
Are you finding that they move out of sync visually ? Or does your testMethod1 in some way require that both actions are complete ?
Although the container concept I have suggested will work for your specific example, it's not a valid option unless it's desired that the objects are moving as if they are connected. An example of where the container would not work, is if each object had to move to different locations in 1 second.
Related
I'm new to Swift and I'm trying to implement a simple game. In this game, once the view is loaded I want periodic animations to happen.
The problem is that I try to animate my buttons with, for instance, button.frame.origin.x += 50, but instead of moving it from the origin to 50px right, it appears at button.frame.origin.x - 50 and goes to its (initial) position.
Funnily enough, at one point I show an AlertDialog to the user, and after it is shown the animation starts to happen as I expected.
My problem is the same of this topic, but the accepted answer just didn't solve it for me.
Any thoughts?
Edit:
Digging into the code and testing a lot I found out the method where I show the AlertDialog also happens to invalidate my timer. I have two timers: one to update the UI time (a TextField), and the other to perform the animation. Both are scheduling a task. From what I read here, it is not possible to have two timers like that.
A timer object can be registered in only one run loop at a time,
although it can be added to multiple run loop modes within that run
loop.
Hence, the obvious solution would be to merge the two selectors (functions) into one, so the timer would work for both tasks. However, if I try to update the UI AND perform animations, the animations don't work as expected anymore. I'm just lost on this problem and can't make both things work.
These are my important pieces of code:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
...
resetOrCreateGame()
}
func resetOrCreateGame() {
// Do stuff to initialize values
timerToRotate = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: "rotateBlocks", userInfo: nil, repeats: true)
}
func rotateBlocks() {
// Calculate positions to rotate
UIView.animateWithDuration(0.5, delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {
// Try to update a timer in the UI
//self.timeLeft.text = String(self.seconds)
for i in 0 ... 8 {
println("\(i) before \(self.buttons[i].frame.origin.x) \(self.buttons[i].frame.origin.y)")
self.buttons[i].frame.origin.x = self.positions[self.indicesToRotate[i]].x
self.buttons[i].frame.origin.y = self.positions[self.indicesToRotate[i]].y
println("\(i) after \(self.buttons[i].frame.origin.x) \(self.buttons[i].frame.origin.y)")
}
}, completion: { _ in
println("completed")
})
If I leave the line self.timeLeft.text = String(self.seconds) commented, the animations work fine. No matter how I try to update the timeLeft, if I do so it screws my animations. I tried to update it in a separate thread, dispatch it to the main thread, or even update it inside the animations closure: it just doesn't work.
Try using SKActions to animate.
Set up an SKAction, then on the SpriteKit node you want to animate (replace "node" with the name of the node), and call it action.
Then, call the simple method:
[node runAction:action];
For example, if you want to set up an SKAction to move a node titled button 50 pixels to the right over a timespan of 3 seconds...
SKAction *action = [SKAction moveByX:50.0 y:0.0 duration:3.0];
[button runAction:action];
Heck, if I don't run an action more than once, I'd simply do this:
[button runAction:[SKAction moveByX:50.0 y:0.0 duration:3.0]];
And finally, you can use the runAction:completion: method to not only run your SKAction, but to call an Objective-C block after it's finished, like in this coding example:
SKAction *action = [SKAction moveByX:50.0 y:0.0 duration:3.0];
[button runAction:action completion:^{
// some code here that runs after an animation completes
}];
And, to non-complicate your code if you want to run a sequence of actions on a node (move a button 50 pixels to the right called button over 3 seconds, then fade it out over 1 second), you can program a sequence of actions into one action, like so:
SKAction *firstAction = [SKAction moveByX:50.0 y:0.0 duration:3.0];
SKAction *secondAction = [SKAction fadeAlphaTo:0.0 duration:1.0];
SKAction *theSequence = [SKAction sequence:[NSArray arrayWithObjects:firstAction, secondAction, nil];
[button runAction:theSequence]
You CAN run another action in the completion block, but the method of using a sequence cleans up code.
Finally, I found the solution!
The problem for some reason was Auto Layout. After disabling it everything worked as expected.
Hope it helps someone in the future!
Please bear with me because I'm very new to OOP/ObjC/Cocos2d.
I have a method that is triggered every second like so: [self schedule:#selector(eyelidsBlink:) interval:1.0];
The schedule method is this:
-(CCTimer *) schedule:(SEL)selector interval:(CCTime)interval
{
return [self schedule:selector interval:interval repeat:CCTimerRepeatForever delay:interval];
}
The method is below:
- (void)eyelidsBlink:(CCTime)dt{
CCActionRemove *actionRemoveEyelidsNormal = [CCActionRemove action];
[_whiteGuy_EyelidsNormal runAction:actionRemoveEyelidsNormal];
_whiteGuy_EyelidsBlink = [CCSprite spriteWithImageNamed:#"EyelidsBlink_iPhone4.png"];
_whiteGuy_EyelidsBlink.position = ccp(self.contentSize.width/2,self.contentSize.height/2);
[_whiteGuy_EyelidsBlink setScale:0.5];
[self addChild:_whiteGuy_EyelidsBlink];
CCActionRemove *remove_eyelidsBlink = [CCActionRemove action];
[_whiteGuy_EyelidsBlink runAction:remove_eyelidsBlink];
NSLog(#"Eyelids blinked");
_whiteGuy_EyelidsNormal = [CCSprite spriteWithImageNamed:#"EyelidsNormal_iPhone4.png"];
_whiteGuy_EyelidsNormal.position = ccp(self.contentSize.width/2,self.contentSize.height/2);
[_whiteGuy_EyelidsNormal setScale:0.5];
[self addChild:_whiteGuy_EyelidsNormal];
}
I can see the first blink, but I can't see any others after that. My NSLog is printing in the console every second, so I know the eyelidsBlink method is being called.
Can anyone help me figure out why I can't see any blinks after the first? Let me know if you need more information, or if you can suggest any tests to troubleshoot.
There's practically no time for the blink sprite to be rendered because you remove it the instant it was added. You'd have to schedule another selector once, ie eyeLidsBlinkOff that runs 0.1 seconds later and hides the blink sprite.
Note: This code is very inefficient. Creating sprites is a relatively slow operation. Instead keep both sprites as children but set one sprite's visible property to NO. While blinking simply flip each sprite's visible flag. This will make the code a lot shorter too.
I'm basically trying to code a "find the ball under the cups" game for practice.
So there is 3 cups, and one target to find. All using SKSpriteNode. The target is randomly a child of one cup, and follows rotations as the parent rotate around an SKNode.
Between each game, the program is supposed to show where is the target, by simply animate it up, then down. Here the sequence code :
//THE ANIMATIONS
SKAction *moveUp = [SKAction moveByX:0.0 y:100 duration:1];
SKAction *moveDown = [SKAction moveByX:0.0 y:-100 duration:1];
SKAction *wait = [SKAction waitForDuration:0.5];
_presentTargetSequence = [SKAction sequence:#[moveUp,wait,moveDown]];
And the method using it :
- (void) presentTarget
{
NSLog(#"presentTarget()");
[_target runAction:_presentTargetSequence completion:^{
_canMove = YES;
}];
}
The code works fine, but only the first time, after that, the method is called but never go through [_target runAction ...].
BUT it's working if the target does get in the rotation/swap.
So my question is : is there anything that can make a node ignore it run action method ? NSLog(#"presentTarget()") is called as I said, but not reaction.
I find that every time I'm confused about an action not running, it's because the object running the action is not currently in the node hierarchy for the scene. There is no error for this, it just doesn't run the action. So double check to make sure that the object is added to the current scene at the time you ask it to run the action.
I am using cocos2d and have 2 AI sprites called TheEvilOne and TheEvilTwo. These 2 sprites call the same bloc of code from the same class sending the sprite as a parameter. However the game gets buggy as I run this code as only one of the sprites preforms the actions and my code stops working. My question is is it possible to call the same bloc of code for multiple sprites simultaneously or is there something wrong with my code. Below is an example of what I am running on my program.
-(void)LoadingEvilActions:(id)sender { //This is Getting Constantly Called
if(loaded == NO) {
theEvilOne = [CCSprite spriteWithFile:#"Evil_Alt_Idle_00.png"]; //These sprites have been declared in my .h file
[self addChild:theEvilOne z:200];
theEvilTwo = [CCSprite spriteWithFile:#"Evil_Alt_Idle_00.png"];
[self addChild:theEvilTwo z:200];
loaded = YES;
}
[self CheckCollision:theEvilOne];
[self setCenterofScreen:theEvilOne];
[self StayonScreen:theEvilOne];
[self AiCharacter:theEvilOne];
[self CheckCollision:theEvilTwo];
[self setCenterofScreen:theEvilTwo];
[self StayonScreen:theEvilTwo];
[self AiCharacter:theEvilTwo];
}
-(void)AiCharacter:(id)sender {
CCSprite *EvilCharacter = (CCSprite *)sender;
if (aiactionrunning == NO){
.... Do More Stuff // For E.G.
id jump_Up = [CCJumpBy actionWithDuration:0.6f position:ccp(randomjump, EvilCharacter.contentSize.height)
height:25 jumps:1];
id jump_Down = [CCJumpBy actionWithDuration:0.42f position:ccp(randomjump,-EvilCharacter.contentSize.height)
height:25 jumps:1];
id seq = [CCSequence actions:jump_Up,jump_Down, [CCCallFuncN actionWithTarget:self selector:#selector(stopAiAction:)], nil];
[EvilCharacter stopAllActions];
[EvilCharacter runAction:seq];
aiactionrunning = YES;
}
}
-(void)stopAiAction:(id)sender {
aiactionrunning = NO;
}
EDIT
I'm running random number generators within my AI Character method and many actions.
The Game already works with just 1 enemy sprite and I decided to try and implement a way to create many.
EDIT 2
I just added the part of the method that stops the sprites being constantly, I was trying to simplify all the code that was irrelevant to my question and forgot to add that part.
I changed the way I am calling my methods like suggested by LearnCocos2d not solving my problem but making it much simpler. I am also not using ARC
One of my sprites is the only one preforming the majority of actions however in some instances the other sprite may preform an actions aswell, but is mainly preformed one sprite. I think my main question is can I call the same method using different sprites passing the sprite as a parameter.
EDIT 3
I have figured out that my problem is there is a Boolean value flag that is enclosing my AiCharacter method, where when one sprite runs through the method it stops the other sprite running the method. Is there some way I can implement an array of records or such so each sprite have their own Boolean flags.
With making this 'array' is it possible to change the Boolean for TheEvilOne and TheEvilTwo using the temp sprite EvilCharacter without doing both separately.
If I understand your question correct you are trying to take one action and run it on more than one character at a time, like:
id move = [CCMoveTo actionWithDuration:0.5f position:ccp(0,0)];
[theEvilOne runAction:move];
[theEvilTwo runAction:move];
That wouldn't work because Evil 1 will not have anything performed on it since it was moved to running on Evil 2. When an action is running it is running on one target. Instead you would:
id move = [CCMoveTo actionWithDuration:0.5f position:ccp(0,0)];
[theEvilOne runAction:move];
[theEvilTwo runAction:[move copy]];
Assuming you are using arc. If not then you'd want to release the copy of course. When it comes to your particular example, why are you trying actions to call methods. Why not just call the method? It seems pointless as LearnCocos2D has pointed out.
Another potential problem you have is that you are constantly creating more and more sprites each time the method is called. Is that on purpose or are there only suppose to be one Evil 1 and one Evil 2? If so then you shouldn't be constantly creating more sprites.
Edit:
Remove your bool. Do something like this instead:
CCSprite *EvilCharacter = (CCSprite *)sender;
if ([EvilCharacter numberOfRunningActions] == 0){
...
}
In an attempt to further familiarize myself with SpriteKit, I made a simple little app that would spawn a random (3 choices) ball at whatever location you click or touch (which automatically despawn after 7 seconds), then apply physics and gravity, and finish with manipulating gravity at set intervals. It was simple enough and worked quite well. However, I decided to add sound to it so the balls would make noise when hitting both the walls or each other.
While it worked, it brought up an issue I couldn't figure out on my own: the soundless version would lag if you spawned so many balls it couldn't handle it, but the sound version continued to lag thereafter. I checked out Xcode's trusty Debug Navigator and found that the memory of the app continued to expand with every ball added to the scene. The soundless version didn't expand on the memory NEARLY as much and additionally reclaimed some bits after the balls had been removed from their parents.
I can't help but think there was something wrong in my implementation that's not only keeping the sound files around in memory after they've been used, but keeping multiples of each file.
I'll happily share my project with anyone who requests, but this is the approximation of the methods that happen:
initialization, sound actions are created and stored in the scene class so they can be accessed later:
#interface REP_Balls () {
SKAction* ballSound01;
}
#end
#implementation REP_Balls {
...
...
...
-(void) setUpSounds {
NSArray* array = #[ #"ball_hit_01.wav",]; //I have more in the array, I'm just simplifying code for stackoverflow
ballSound01 = [SKAction playSoundFileNamed:[array objectAtIndex:0] waitForCompletion:NO];
}
I then have a contact listener activate a method that randomizes from the available sound actions and returns one of them:
-(void) didBeginContact:(SKPhysicsContact *)contact {
SKPhysicsBody *firstBody, *secondBody;
firstBody = contact.bodyA;
secondBody = contact.bodyB;
SKAction* randomSound;
//blah blah blah if categories match and whatnot, do this:
REP_BallSpawn* ball = (REP_BallSpawn*) firstBody.node;
randomSound = [self soundBankRandomizer:YES];
[ball runAction:randomSound];
And the method that randomizes:
-(SKAction*) soundBankRandomizer:(BOOL)isWallHit {
NSArray* array;
switch (isWallHit) {
case YES:
array = #[ //according sound actions
];
break;
case NO:
array = #[
ballSound01, //and other sound actions
];
default:
array = #[
//if neither case is true somehow, just choose from a bank of all sound actions
];
break;
}
int randomChoice = arc4random() % [array count];
SKAction* sound = (SKAction*)[array objectAtIndex:randomChoice];
return sound;
}
Beyond that, after 7 seconds the balls automatically despawn themselves, and I would assume any children with them (which I also assume should include actions, such as sound actions).
I even made a method to confirm no nodes are sticking around after they despawn:
[self enumerateChildNodesWithName:#"//*" usingBlock:^(SKNode *node, BOOL *stop) {
string = [NSString stringWithFormat:#"%# (())(()) %#", string, node];
}];
The result is then copied to the clipboard which results in a list of all nodes and children in the scene at the moment. (the " (())(()) " is in there for a unique pattern to search for in find/replace to make new lines)
Edit: I also noticed that CPU usage remains high after everything has despawned as well.
Please tell me I did something wrong!
===================================================
So I made some changes to my code. As mentioned below, I got it to work right, but upon some further testing, it appears to be finicky as to how exactly it gets implemented. Maybe someone else will see why, but it makes no sense to me.
METHOD THAT WORKS
Same as above with these changes:
-(void) setUpSounds {
NSArray* array = #[
#"ball_hit_01.m4a",
#"ball_hit_02.m4a",
#"ball_hit_03.m4a",
#"ball_hit_04.m4a",
#"wall_hit_01.m4a",
#"wall_hit_02.m4a",
#"floor_hit_01.m4a",
#"floor_hit_02.m4a",
];
SKAction* ballSound01 = [SKAction playSoundFileNamed:[array objectAtIndex:0] waitForCompletion:NO];
// etc
sounds = [NSArray arrayWithObjects:ballSound01, ballSound02, ballSound03, ballSound04, wallSound01, wallSound02, floorSound01, floorSound02, nil]; //is declared class wide
}
-(SKAction*) soundBankRandomizer:(BOOL)isWallHit {
int randomChoice = arc4random() % ([sounds count] / 2);
if (isWallHit) {
randomChoice += ([sounds count] / 2);
}
return [sounds objectAtIndex:randomChoice];
}
The contact listener is nearly identical (I removed the casting for the ball and applied the action to the world node instead). I've also tried it using a dictionary instead of an array and it works fine.
METHOD THAT DOESN'T WORK FOR SOME REASON
I would remove the soundBankRandomizer method, as well as remove any action calls from the contact listener. Instead, I added this method:
-(void)playSound:(BOOL)isWallHit {
int randomChoice = arc4random() % ([sounds count] / 2);
if (isWallHit) {
randomChoice += ([sounds count] / 2);
}
SKAction* randomSound = [sounds objectAtIndex:randomChoice];
[bgNull runAction:randomSound];
}
and then called it from the contact listener via:
[self playSound:YES]; //for wall hit
[self playSound:NO]; //for ball on ball action
For some reason, this resulted in the exact same performance issues I had initially. Does it make sense to anyone?!
I see a couple issues....By declaring the SKAction ballSound01 it'll stick around to be used again. Also I follow your thinking that by removing every node, it would remove every action, but again I think by declaring it, that won't happen. Also SKActions could be applied to multiple nodes, so I don't think you can really consider them children of the object they run on.
I think you are better off declaring an array which holds onto those SKActions, instead of creating it each time in the soundBankRandomizer.
Also I've never seen a switch statement with a bool variable like that. Just doing this...
if (isWallHit == YES) {
} else{
}
Is more to the point because the default block is never going to run anyway. Since isWallHit is either YES or NO, those two conditions are it, no need for the default.
You might want to consider making a Singleton class as a sound manager and keeping it totally separate from the nodes. So instead of calling...
randomSound = [self soundBankRandomizer:YES];
[ball runAction:randomSound];
You could call something like...
[[SoundManager sharedSounds] playRandomWallSound];
No need to make a specific node part of playing the sound.