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!
Related
I'm using the code below to animate an intro-animation for buttons in an iOS Swift game. This code is inside of an update function and is the same for a lot of buttons.
if self.creditsButton.size.width < 40 {
self.creditsButton.size.width += 1
self.creditsButton.size.height += 1
}
My question is; is there a better (more clean) way to animate scaling/sizes of buttons/menu's?
If the button is a subclass of SKSpriteNode then SKAction enables you to schedule the animation without needing to regularly update, e.g. (apologies for the objective-C):
SKAction *scale = [SKAction resizeToWidth:40.0 duration:0.4];
[spriteNode runAction:scale];
I have a node on which I run tow actions - one on the mouseDown event and second on the mouseUp event.
The action that is being run may take longer than left mouse button is down and I would like to continue executing this action and somehow run the second action from the mouseUp method after the action from the mouseDown method has finished. Is it possible?
You don't have code for me to work off of and I am not that great with Swift anyway so hopefully this makes sense with Objective-C
First Option
OnMouseDown:
SKAction *firstAction = [SKAction moveByX:100 y:100 duration:5];
SKAction *customAction = [SKAction customActionWithDuration:0 actionBlock:^(SKNode *node, CGFloat elapsedTime) {
if (self.runSecondAction)
{
SKAction *secondAction = [SKAction moveByX:-100 y:-100 duration:5];
[node runAction:secondAction];
self.runSecondAction = NO;
}
}];
SKAction *squence = [SKAction sequence:#[firstAction, customAction]];
[sprite runAction:squence withKey:#"moveAction"];
OnMouseUp:
if ([sprite actionForKey:#"moveAction"])
{
self.runSecondAction = YES;
}
else
{
SKAction *secondAction = [SKAction moveByX:-100 y:-100 duration:5];
[sprite runAction:secondAction];
}
The basic concept is you run a sequence with a custom action that runs a block. In that block you check to see if a bool has been set to run the second object. This will make it run after the first one is complete.
In mouseUp you want to run the action right away if the first action is done but if not you set a bool that will be check during the sequence.
Second Option
Don't run a sequence in OnMouseDown and check if there is a #"moveAction" going OnMouseUp. If there is check the state of the sprite and create the sequence at that point with what is remaining of that action. If there isn't that #"moveAction" just run the second action.
Hopefully that makes sense and is helpful.
i have an SKAction sequence involving two actions. 1) wait for time according to variable 2) spawn object
SKAction *sequence = [SKAction sequence:#[
wait,
addObject]];
this action is set to run forever. however, i want the wait duration to change according to updating the variable, but it stays constant as it does not take the new variable when running forever
[self runAction:[SKAction repeatActionForever:sequence]];
How do i make it steadily increase the value, therefore increasing the rate of object spawning?
Thanks
There are two solutions:
Create the sequence or at least the wait action anew every time you need it. Actions are supposed to be discarded and re-used frequently.
If this poses a performance problem and given that you already have a reference to the sequence, you can also change its speed variable. This ought to alter the wait time accordingly, ie if speed is 0.5 the wait time should double.
Example for solution 1:
CGFloat waitDuration = (value to be determined by you);
SKAction *sequence = [SKAction sequence:#[
[SKAction waitForDuration:waitDuration],
addObject]];
Example for solution 2:
SKAction *sequence = [SKAction sequence:#[
wait,
addObject]];
// half the speed, double the wait time
sequence.speed = 0.5;
// or if you need to derive speed from waitDuration (which must be >0.0)
sequence.speed = (1.0 / waitDuration);
In case the sequence isn't affected by speed try setting the wait action's speed instead.
So I followed LearnCocos2D's answer, but I made a slight modification, which I think works great. It works for my purpose, so I thought to put it out there.
What I did was this:
//This is the boring bits for creating the SKSpriteNode
headsNode = [SKSpriteNode spriteNodeWithTexture:[self.textureAtlas firstObject]size:CGSizeMake(100, 100)];
headsNode.position = CGPointMake(CGRectGetMidX(self.frame)*0.5, CGRectGetMidY(self.frame)-coinSize/2);
headsNode.name = #"Heads";
[self addChild:headsNode];
// I added a timer with winkWaitingTime as interval. This is the variable
// to change. Set this to something at the beginning.
NSTimer *timered= [NSTimer scheduledTimerWithTimeInterval:winkWaitingTime target:self selector:#selector(timerFired:) userInfo:nil repeats:YES];
[timered fire];
}
-(void)timerFired:(NSTimer*)timer{
//In the NSTimer Selector, I set the timer interval / SKAction waiting interval to a random
// number
winkWaitingTime =[[SharedInfo sharedManager] randomFloatBetween:3 and:8];
[headsNode removeAllActions];
//I thought it's best to remove the actions JIC :)
[headsNode runAction:[self returnActionForAnimationKey:headsNode.name]];
NSLog(#"winktimer: %f",winkWaitingTime);
}
-(SKAction*)returnActionForAnimationKey:(NSString*)animationKey{
CGFloat waitDuration;
//This is for just to prevent timer fire interfering with the SKAction
//This will make the SKAction waiting time shorter than NSTimer Fire rate
if (winkWaitingTime >4) {
waitDuration = winkWaitingTime-3;
}else{
waitDuration = 1;
}
SKAction *sequence = [SKAction sequence:#[
[SKAction waitForDuration:waitDuration],
[SKAction animateWithTextures:self.textureAtlas timePerFrame:0.1f resize:NO restore:YES]]];
return sequence;
}
As I said, it works for me, and by changing the winkWaitingTime, and a bit of prevention management, it works.
I hope you find it useful as well. In terms of memory management, my Xcode is showing stable CPU / Memory / Battery usage, so there's no increase in use.
NOTE: After receiving a comment, I thought it's best to discuss the use of NSTimer. If you need to have something done "externally", i.e. independent to the view or scene, then I'd use the NSTimer (that was my case). But you'd have to implement the pausing and unpausing for the timer as well. (You'd have to invalidate the NSTimer to stop, and reschedule a new one, or have it as a variable and reschedule the same one). Otherwise, use the SKAction and you won't need to pause and unpause the NSTimer.
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 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.