I stumbled upon a problem and I can't find the answer for it. I am working with the SK template from Xcode to create an iOS game. I am a beginner, so bear with me.
Basically I have this code:
SKAction *releaseBubbles = [SKAction sequence:#[
[SKAction performSelector:#selector(createBubbleNode)onTarget:self],
[SKAction waitForDuration:speed]]];
[self runAction: [SKAction repeatAction:releaseBubbles
count:300]];
which executes in
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
I change the level to my game in -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { and when I change the level it should also change that speed parameter. Of course, this doesn't work because I believe that my action is starting when the scene is initialised and I never get to switch the parameter.
What I need to do is populate the screen continuously with bubbles appearing at a certain pace (relative to the level).
I really have no clue how to fix this, because it seems to me like I need to stop and restart the action sequence somehow...
Looking forward to your valuable input.
To continuously populate the screen with bubbles you can use the update: method of your SKScene. Here is how to do it.
First, add a property that will store a date when you last added a bubble.
#property(nonatomic, strong) NSDate *lastBubbleCreationDate;
Then, change your update: method to:
-(void)update:(CFTimeInterval)currentTime
{
// Create new bubble every 5s.
if (ABS([_lastBubbleCreationDate timeIntervalSinceNow]) > 5)
{
[self createBubbleNode];
}
}
Finally, in your createBubbleNode method you have to store the time when you created last bubble:
-(void)createBubbleNode
{
// Your code here
// Set the date to now.
_lastBubbleCreationDate = [NSDate date];
}
You also need to call createBubbleNode to set the initial value of the _lastBubbleCreationDate. You can do this in didMoveToView: method. Just add this method to your scene implementation:
- (void)didMoveToView:(SKView *)view
{
// Creates first bubble and sets the initial value of the _lastBubbleCreationDate
[self createBubbleNode];
}
In next levels you can just change the 5s value to create bubbles more often which will make the game more difficult.
Related
Problem
When the node hierarchy is encoded, as is common during application state preservation or a “game save”, nodes running SKAction actions with code blocks must be handled specially, since the code blocks cannot be encoded.
Example 1: Delayed Callback after Animation
Here, an orc has been killed. It is animated to fade out and then remove itself from the node hierarchy:
SKAction *fadeAction = [SKAction fadeOutWithDuration:3.0];
SKAction *removeAction = [SKAction removeFromParent];
[orcNode runAction:[SKAction sequence:#[ fadeAction, removeAction ]]];
If the orc node is encoded and then decoded, the animation will restore properly and complete as expected.
But now the example is modified to use a code block that runs after the fade. Perhaps the code cleans up some game state once the orc is (finally) dead.
SKAction *fadeAction = [SKAction fadeOutWithDuration:3.0];
SKAction *removeAction = [SKAction removeFromParent];
SKAction *cleanupAction = [SKAction runBlock:^{
[self orcDidFinishDying:orcNode];
}];
[orcNode runAction:[SKAction sequence:#[ fadeAction, removeAction, cleanupAction ]]];
Unfortunately, the code block will not encode. During application state preservation (or game save), if this sequence is running, a warning will be issued:
SKAction: Run block actions can not be properly encoded,
Objective-C blocks do not support NSCoding.
After decoding, the orc will fade and be removed from parent, but the cleanup method orcDidFinishDying: will not be called.
What is the best way to work around this limitation?
Example 2: Tweening
The SKAction customActionWithDuration:actionBlock: seems a beautiful fit for tweening. My boilerplate code for this kind of thing is this:
SKAction *slideInAction = [SKAction customActionWithDuration:2.0 actionBlock:^(SKNode *node, CGFloat elapsedTime){
CGFloat normalTime = (CGFloat)(elapsedTime / 2.0);
CGFloat normalValue = BackStandardEaseInOut(normalTime);
node.position = CGPointMake(node.position.x, slideStartPositionY * (1.0f - normalValue) + slideFinalPositionY * normalValue);
}];
Unfortunately, customActionWithDuration:actionBlock: cannot be encoded. If the game is saved during the animation, it will not restore properly on game load.
Again, what is the best way to work around this limitation?
Imperfect Solutions
Here are solutions I have considered but don’t like. (That said, I’d love to read answers that successfully champion one of these.)
Imperfect Solution: Use performSelector:onTarget: rather than runBlock: in the animation. This solution is imperfect because arguments cannot be passed to the invoked selector; context for the call can only be expressed by the target and the name of the selector. Not great.
Imperfect Solution: During encoding, remove the SKAction sequence from any relevant nodes and advance the program state as if the sequence had completed. In the first example, that would mean setting the node alpha immediately to 0.0, removing the orc node from parent, and calling orcDidFinishDying:. This is an unfortunate solution for at least two reasons: 1) It requires special handling code during encoding; 2) Visually, the node won’t get a chance to finish its animation.
Imperfect Solution: During encoding, remove the SKAction code blocks from any relevant nodes, and recreate them during decoding. This is non-trivial.
Imperfect Solution: Never use SKAction code blocks, especially after a delay. Never rely on the completion of an animation in order to restore good app state. (If you need to schedule a future event in an encodable way, build your own event queue not using code blocks.) This solution is imperfect because runBlock and customActionWithDuration:actionBlock: are just so damn useful, and it would be a shame (and a recurring trap for newbies) to consider them evil.
Encodable lightweight objects can model the kinds of SKAction code blocks that we want to use (but can’t).
Code for the below ideas is here.
Replacement for runBlock
The first encodable lightweight object replaces runBlock. It can make an arbitrary callback with one or two arguments.
The caller instantiates the lightweight object and sets its properties: target, selector, and arguments.
The lightweight object is triggered in a runAction animation by the standard no-argument [SKAction performSelector:onTarget:]. For this triggering action, the target is the lightweight object and the selector is a designated “execute” method.
The lightweight object conforms to NSCoding.
As a bonus, the triggering SKAction retains a strong reference to the lightweight object, and so both will be encoded along with the node running the actions.
A version of this lightweight object could be made that retains the target weakly, which might be nice and/or necessary.
Here is a draft of a possible interface:
#interface HLPerformSelector : NSObject <NSCoding>
- (instancetype)initWithTarget:(id)target selector:(SEL)selector argument:(id)argument;
#property (nonatomic, strong) id target;
#property (nonatomic, assign) SEL selector;
#property (nonatomic, strong) id argument;
- (void)execute;
#end
And an accompanying implementation:
#implementation HLPerformSelector
- (instancetype)initWithTarget:(id)target selector:(SEL)selector argument:(id)argument
{
self = [super init];
if (self) {
_target = target;
_selector = selector;
_argument = argument;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
_target = [aDecoder decodeObjectForKey:#"target"];
_selector = NSSelectorFromString([aDecoder decodeObjectForKey:#"selector"]);
_argument = [aDecoder decodeObjectForKey:#"argument"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_target forKey:#"target"];
[aCoder encodeObject:NSStringFromSelector(_selector) forKey:#"selector"];
[aCoder encodeObject:_argument forKey:#"argument"];
}
- (void)execute
{
if (!_target) {
return;
}
IMP imp = [_target methodForSelector:_selector];
void (*func)(id, SEL, id) = (void (*)(id, SEL, id))imp;
func(_target, _selector, _argument);
}
#end
And an example of using it:
SKAction *fadeAction = [SKAction fadeOutWithDuration:3.0];
SKAction *removeAction = [SKAction removeFromParent];
HLPerformSelector *cleanupCaller = [[HLPerformSelector alloc] initWithTarget:self selector:#selector(orcDidFinishDying:) argument:orcNode];
SKAction *cleanupAction = [SKAction performSelector:#selector(execute) onTarget:cleanupCaller];
[orcNode runAction:[SKAction sequence:#[ fadeAction, removeAction, cleanupAction ]]];
Replacement for customActionWithDuration:actionBlock:
A second encodable lightweight object replaces customActionWithDuration:actionBlock:. This one is not so simple, however.
Again, it is triggered by the no-argument [SKAction performSelector:onTarget:], invoking a designated execute method.
A customActionWithDuration:actionBlock: has a duration. But the triggering performSelector:onTarget: does not. The caller must insert a companion waitForDuration: action into her sequence if it depends on duration.
The lightweight object is initialized with a target, selector, node, and duration.
When it is triggered, the lightweight object tracks its own elapsed time and periodically calls the selector on the target, passing it the node and the elapsed time.
The lightweight object conforms to NSCoding. On decoding, if already triggered, it resumes calling the selector for the remainder of its configured duration.
Limitations
I have implemented a version of these proposed classes. Through light use I've already found an important limitation: Nodes encoded with a running SKAction sequence restart the sequence from the beginning upon decoding.
I've a player of Sprite * type in cocos2dx V3, I want it to run different animation on different time interval, I could not find method to pause and then resume a specific animation(Action). Although I can Pause and Resume all actions of Sprite Simultaneously using _player->pauseSchedulerAndActions().I'm using "CCRepeatForever" actions on sprite, so, I must have to pause one to resume other.Please help to Pause an action by tag or by any other method.
Thanks In Advance.
Oops
I made the assumption that this was Objective-C but #Droppy has informed me that it is not.
I didn't realise cocos2d-x was different. However, because this is a fairly high level framework the concept behind what I've done in the answer will still work. I'll keep the answer here for now.
The answer
It's been a while since I've done any Cocos2D stuff but I can give you the idea.
Instead of creating an action and repeating it forever you should have a method something like this...
- (void)twirlAround
{
// only create and perform the actions if variable set to YES
if (self.twirling) {
// this will do your action once.
CCAction *twirlAction = // create your twirl action (or whatever it is)
// this will run this function again
CCAction *repeatAction = [CCActionCallBlock actionWithBlock:^{
[self twirlAround];
}];
// put the action and method call in sequence.
CCActionSequence *sequence = [CCActionSequence actions:#[twirlAction, repeatAction]];
[self runAction:sequence];
}
}
This will run repeatedly as long as the twirling property is set to YES.
So, somewhere else in your code (probably where you are currently adding your repeating action) you can do this...
self.twirling = YES;
[self twirlAround];
This will start the repeated twirling.
To stop it you can then do...
self.twirling = NO;
This will stop the twirling.
Alternative method
- (void)twirlAround
{
// this will do your action once.
CCAction *twirlAction = // create your twirl action (or whatever it is)
// this will run this function again
CCAction *repeatAction = [CCActionCallBlock actionWithBlock:^{
if (self.twirling) {
[self twirlAround];
}
}];
// put the action and method call in sequence.
CCActionSequence *sequence = [CCActionSequence actions:#[twirlAction, repeatAction]];
[self runAction:sequence];
}
based on Fogmeister advice, this is cocos2d-x version of that
void MySprite::jumpForever(){
if (!twirling) return;
auto jump = JumpBy::create(0.5, Vec2(0, 0), 100, 1);
auto endCallback = CallFuncN::create(CC_CALLBACK_1(MySprite::jumpForever,this));
auto seq = Sequence::create(jump, endCallback, nullptr);
runAction(seq);
}
i don't completely understand the best choice in sprite kit animation;
1) Apple in "Adventure" example use this methodology, they store in memory animation as pictures in nsarray :
static NSArray *sSharedTouchAnimationFrames = nil;
- (NSArray *)touchAnimationFrames {
return sSharedTouchAnimationFrames;
}
- (void)runAnimation
{
if (self.isAnimated) {
[self resolveRequestedAnimation];
}
}
- (void)resolveRequestedAnimation
{
/* Determine the animation we want to play. */
NSString *animationKey = nil;
NSArray *animationFrames = nil;
VZAnimationState animationState = self.requestedAnimation;
switch (animationState) {
default:
case VZAnimationStateTouch:
animationKey = #"anim_touch";
animationFrames = [self touchAnimationFrames];
break;
case VZAnimationStateUntouch:
animationKey = #"anim_untouch";
animationFrames = [self untouchAnimationFrames];
break;
}
if (animationKey) {
[self fireAnimationForState:animationState usingTextures:animationFrames withKey:animationKey];
}
self.requestedAnimation = VZAnimationStateIdle;
}
- (void)fireAnimationForState:(VZAnimationState)animationState usingTextures:(NSArray *)frames withKey:(NSString *)key
{
SKAction *animAction = [self actionForKey:key];
if (animAction || [frames count] < 1) {
return; /* we already have a running animation or there aren't any frames to animate */
}
[self runAction:[SKAction sequence:#[
[SKAction animateWithTextures:frames timePerFrame:self.animationSpeed resize:YES restore:NO],
/* [SKAction runBlock:^{
[self animationHasCompleted:animationState];
}]*/]] withKey:key];
}
I appreciate this methodology, but i can't understand. Is storing SKAction in memory not better choice and use animation always like this ?
[self runAction:action];
without making always new SKAction;
[SKAction animateWithTextures:frames timePerFrame:self.animationSpeed resize:YES restore:NO]
Storing SKTextures in an NSArray is the recommended methodology to be used for animation. I can't say that I found SpriteKit's documentation to be lacking on the subject either, as SKAction has the method animateWithTextures.
You could indeed have a SKAction that has a given animation defined by a given NSArray, and maybe store those in a NSMutableDictionary with an animation name for a key. However this methodology I see above just has the animateTextures line of code once in their fireAnimationState method, and you can pass parameters to it such as the animationState and a key.
I think you would need to take more than a surface look at their methodology to determine why they chose to go this route. You can see that the animationState was being utilized upon completion of the animation and likely triggered something else.
[self runAction:action] is indeed simple, however it's also easy to see that it's not in any way managing an animationState or key, which I have to assume they decided their game needed to do.
Also keep in mind that their methodology likely has a higher level where in a given player class it's calling a flexible method for enacting an animation for a given game entity and doing other things besides just changing the animation. So in their high level coding they might be doing something more like this :
[self runAnimation:#"jump"];
or even
[self jump];
And because they have designed a low level system that manages animation states and adds keys for a given management design, they don't ever have to code the long line that you are pointing out except once in that method you see above.
A few reasons I have found it useful to store the NSArray of frames and not storing wrapped in a SKAction is because I sometimes want to manipulate the start frame of the animation or run the animation at a different speed. But you may or may not have the need to manipulate those aspects of your animation.
I have a sprite that goes from left to right on the screen, when it goes off screen it won't be back. Where is the best place to deallocate it? In the -update method? Or?
you can use the didSimulatePhysics method. It is called after the update method. That we you can be sure that whatever needs to be finished in the update methods gets done, and then clean your objects or cycle through them and figure out which ones need to be removed.
- (void)didSimulatePhysics {
[self removeAllObstacles];
}
- (void)removeAllObstacles {
[blocks removeAllChildren];
[gameLayer enumerateChildNodesWithName:#"baddies" usingBlock:^(SKNode *node, BOOL *stop) {
if (node.position.x < - 100)
[node removeFromParent];
}];
}
Yes, It has to be added in the update method only. Because The update: method will be called automatically by Sprite Kit for each frame.
I've added an additional CCLayer to my "GameScene" that becomes visible ([self addChild:_congratsScreen]) whenever my character collects a given amount of objects on the screen.
Within my GameScene.h I've declared my child layer (CClayer *congratsScreen) and I'm synthesizing it on my GameScene.m. I'm allocating the child CCLayer in the GameScene's init method so it is holding the reference to the child layer in this instance variable.
On my GameScene I have a few CCParticleSystemQuad instances, and it's super simple to invoke both stopSystem and resetSystem to replay my particles animation, but if I try to do the same thing on the CCParticleSystemQuad that was initialized on the child layer, the resetSystem doesn't work after I remove the child from my GameScene and add it back again. Does something happens with the CCLayer's components once it is removed from a parent layer's scene?
I don't have the code at the moment so I will try to write some pseudo-code to illustrate how it's being done:
How it is being initialized on ChildLayer.m:
_sparkling= [CCParticleSystemQuad particleWithFile:#"sparkling.plist"];
Then, somewhere on GameScene.m I have:
- (void) showCongrats {
//pathetic way to create a modal panel
[self setTouchable = NO];
[[[self _congratsLayer] _sparkling] resetSystem];
[self addChild:_congratsLayer];
}
- (void) hideCongrats {
//let them continue playing
[self setTouchable = YES];
[[[self _congratsLayer] _sparkling] stopSystem];
[self removeChild:_congratsLayer];
}
So, it works on the first time I invoke showCongrats, the reference is good and I can manipulate the particles, but once I hide the layer, continue playing the game and show the congratulations panel again, it shows a frozen animation of the particles from the last invocation, the resetSystem no longer works. Any ideas?
I would add some breakpoints in the code and walk through it but if I had to guess I would say that when you are calling removeChild you are losing the data that you had in your init method and something funky is happening.