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.
Related
I've read in a few placing that changing the speed variable of an SKAction will change the speed; however, that doesn't seem to be working for me.
- (void)startAnimating {
SKAction *moveDown = [SKAction moveByX:0 y:-[CAUtilities screenSize].height duration:self.animationDuration];
[self setMoveDownAction:[SKAction repeatActionForever:moveDown]];
[self runAction:self.moveDownAction];
}
- (void)incAnimationSpeedBy:(CGFloat)aFloat {
self.moveDownAction.speed += 0.5;
NSLog(#"%f", self.moveDownAction.speed);
}
The actual value of self.moveDownAction.speed changes as seen in the NSLog call, but the actual animation doesn't change.
I have incAnimationSpeedBy: being called when the screen is tapped, so using a SKAction sequence with a runBlock won't work for my needs.
I've tried:
Having the initial moveDown as an instance variable.
Having the repeat forever action as an instance variable (seen above).
Changing the duration property rather than speed.
Any help is appreciated, thanks.
The problem was I was trying to change the speed on the different SKAction objects when I should have been changing the speed on the parent SKSpriteNode.
- (void)startAnimating {
SKAction *moveDown = [SKAction moveByX:0 y:-[CAUtilities screenSize].height duration:3.0];
[self runAction:[SKAction repeatActionForever:moveDown]];
}
- (void)incAnimationSpeedBy:(CGFloat)aFloat {
if (self.speed + aFloat < kMaxSpeed)
self.speed += aFloat;
}
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 this code:
#implementation MyScene {
SKAction *delayAction;
}
Inside a method:
delayAction = [SKAction waitForDuration:3.0];
[self runAction:[SKAction repeatActionForever: [SKAction sequence:
#[delayAction, [SKAction ...]]]]]
withKey:#"myKey"];
Then i want to decrease duration overtime. (This method is called on update:)
So i tried:
- (void)updateVelocity
{
NSLog(#"duration:%f",delayAction.duration);
delayAction.duration = delayAction.duration - 0.001;
}
And i get:
2014-04-04 11:45:05.781 FlyFish[5409:60b] duration:1.300000
2014-04-04 11:45:05.785 FlyFish[5409:60b] duration:1.299000
2014-04-04 11:45:05.800 FlyFish[5409:60b] duration:1.298000
2014-04-04 11:45:05.816 FlyFish[5409:60b] duration:1.297000
Which seems good, but my [SKAction ...] still continues repeating after 3 seconds.
I'd do this a different way. Something like this...
- (void)recursiveActionMethod
{
if (some end condition is met) {
return;
// this allows you to stop the repeating action.
}
self.duration -= 0.01;
// store duration in a property
SKAction *waitAction = [SKAction waitForDuration:self.duration];
SKAction *theAction = [SKAction doWhatYouWantHere];
SKAction *recursiveAcion = [SKAction performSelector:#selector(recursiveActionMethod) onTarget:self];
SKAction *sequence = [SKAction sequence:#[waitAction, theAction, recursiveAction]];
[self runAction:sequence];
}
This will perform your action and then come back to this function to be run again with a different wait time and again, and again, ...
You can even stop the sequence by having some end condition that would jump inside the if block and stop the loop.
I am a bit late, but you can instead set the action to SKActionTimingEaseOut. Also this is language native and should work slightly faster. (Though you cannot customize the speed changes)
This can be done similarly to this:
yourSKAction.timingMode = SKActionTimingEaseOut;
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.