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.
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);
}
What's the best way to execute a series of commands in a certain order? I keep getting frustrated by the darn concurrency of execution in ObjC. This is one of these cases where I realize that I'm a designer, not a "real" coder.
I'm experimenting with SpriteKit on iOS, and I want a sequence of things to occur when an energy gauge reaches <=0.
Call a method to create an explosion at the final contact. The method takes arguments about position and size of explosion.
Call another method afterwards that calls up a new scene, a results screen.
My problem occurs when the new scene gets called before I get a chance to see the last explosion.
Here's the relevant code:
- (void) doGameOver
{
damageIndicator.progress = 0;
energyLeft.text = #"Energy:0%";
GameOver *newScene = [[GameOver alloc]initWithSize:self.size];
newScene.timeElapsed = [started timeIntervalSinceNow];
[self.view presentScene:newScene transition:[SKTransition fadeWithColor:[SKColor whiteColor] duration:1]];
[damageIndicator removeFromSuperview];
}
- (void) makeExplosionWithSize:(float)myBoomSize inPosition:(CGPoint)boomPosition
{
NSString *myFile = [[NSBundle mainBundle] pathForResource:#"explosion" ofType:#"sks"];
SKEmitterNode *boom = [NSKeyedUnarchiver unarchiveObjectWithFile:myFile];
boom.position = boomPosition;
boom.particleSize = CGSizeMake(myBoomSize, myBoomSize);
[self addChild:boom];
[self runAction:self.playMySound];
}
- (void)adjustScoreWithDamage:(float)hitDamage atPosition:(CGPoint)pos
{
_damage = _damage -(hitDamage);
if (_damage < 0) {
//these are the two things I need to execute sequentially
[self makeExplosionWithSize:500 inPosition:pos];
[self doGameOver]
}
}
I've tried schemes using bools (gameOver = YES), but think I may need to create a completion handler, which just makes my head spin.
Can anyone suggest the easiest way to accomplish this?
Thank you in advance.
Easiest (not best) probably would be to replace
[self doGameOver];
with
[self performSelector:#selector(doGameOver) withObject:nil afterDelay:2.0];
I may misunderstand what you're going for, but it sounds like what should happen is:
The explosion begins.
There's a pause of [n] seconds.
The Game Over screen is presented.
To accomplish that, you might want to just fire "doGameOver" with an NSTimer rather than worry about having it fire immediately after the explosion completes.
Here's an example with a 3 second delay:
NSTimer *gameOverTimer = [NSTimer timerWithTimeInterval:3.0 target:self selector:#selector(doGameOver:) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:gameOverTimer forMode:NSDefaultRunLoopMode];
Just to clarify: this is not a question about sequences or groups of SKActions.
I have 7-20 sprites representing monsters on a 2D game board. I would like them to act one after another. The action can be a move, or move and attack.
The issue that I'm running in is that the first few monsters seem to act normally, - one monster moves, attacks, then the next one, etc. But after a few of them (4-5) the sequence breaks and multiple monsters start to act at the same time.
Is this an issue with the recursive move/AI methods I have below or if something within Sprite Kit is interfering with the sequence?
I see that the call stack in xCode seems to grow, and I see multiple calls to processMonsterAI, but it's not a 1 call per monster situation.
In particular I'm interested if I should use NSTimers or Delayed dispatch queue blocks instead of relying on Sprite Kit's completion blocks?
Every turn, my AI has to move monsters on the board:
-(void)processMonsterAI
{
CharacterModelNode* monster = [allMonstersCopy lastObject];
[allMonstersCopy removeLastObject];
AIBase* ai = monster.character.AI;
ai.delegate = self;
ai doAIAction];
}
AI frequently makes decisions to move around the board, which are animated as move from one cell to the next, step by step until the monster is out of moves or destination is reached:
//do recursive movement animation
-(void)recursiveMoveWithCharacter:(CharacterModelNode*)characterModel path:(NSMutableArray*)path
{
//check if we ran out of moves
if(path.count == 0)
{
//notify delegate
[self moveCompleteForCharacter:characterModel];
}else
{
NSNumber* tileIndex = path[0];
CGPoint destination = [MapOfTiles positionForTileAtIndex:tileIndex.intValue];
[path removeObjectAtIndex:0];
[self runAction:[SKAction moveTo:destination duration:1]
completion:^{
characterModel.character.currentMoves = characterModel.character.currentMoves-1;
[self recursiveMoveWithCharacter:characterModel path:path];
}];
}
}
Once the animation completes, the AI is notified that the monster is in a correct position, and attack sequence can be performed:
//once animation is complete, call
-(void)moveComplete
{
if(self.bestTarget)
{
[self.actor attack:self.bestTarget];
}
//notify whoever is the delegate that we are done here and the next AI can do it's magic.
[self finishAction];
}
//AI is done, notify delegate to process next monster
-(void)finishAction
{
NSLog(#"Finished :%#",self.actor.character.name);
[self.actor debugFlashRed];
if([self.delegate respondsToSelector:#selector(didFinishAIAction:)])
{
[self.delegate didFinishAIAction:self];
}
}
Proceed to move/attack with the next monster
//either repeat the process, or terminate if all monsters acted
-(void)didFinishAIAction:(AIBase*)AI
{
if(allMonstersCopy.count==0)
{
//finished with monster turn
[self preventUserInteraction:NO];
[[GameDataManager sharedInstance] processEndTurn];
}else
{
[self processMonsterAI];
}
}
UPDATE:
Here's the call stack, It hits the assertion that finishAction gets called by some AI more than once:
Does doAIAction possibly go through multiple paths that may call finishAction more then once? That does seem to be a possibility. There isn't really any branching in the code you have shown that would lead to multiple processMonsterAI calls.
I'm building a simple 2D game in Cocos2d which involves having enemies cross the screen across different, predefined paths. The nextFrame method has the following code:
int indexCount = 0;
for (Enemy *e in enemies) {
if ([cocosGuy doesCollideWithRect: [e boundingBox]])
{
for (Enemy *e in enemies)
{
[self removeChild: e cleanup: YES];
}
[self startGame];
}
// ^ This code not relevant to the question
if ([e numberOfRunningActions] == 0)
{
[e setPosition: [[enemy_positions objectAtIndex:indexCount] CGPointValue]];
[e runAction: [beziers objectAtIndex: indexCount]];
}
++indexCount;
}
The code in the second if statement above is intended to take a CGPoint from the 'enemy_positions' array and a CCActionInterval from the 'beziers' array. It works - when an enemy completes its path, it is repositioned and the action reruns. But why doesn't this break after the action runs for the first time? Aren't CCActions supposed to be one time only?
I ask because I want to refactor the position and action into a single struct, and I want to make sure I know what's going on first. Am I misunderstanding the CCAction class?
Also, here is the current factory method for generating the 'beziers' array:
-(NSArray*) makeBeziers {
ccBezierConfig bezierconf1;
bezierconf1.controlPoint_1 = ccp(-200, 5);
bezierconf1.controlPoint_2 = ccp(300, 100);
bezierconf1.endPosition = ccp(1000,5);
ccBezierConfig bezierconf2;
bezierconf2.controlPoint_1 = ccp(-200, 5);
bezierconf2.controlPoint_2 = ccp(300, 100);
bezierconf2.endPosition = ccp(1000,5);
ccBezierConfig bezierconf3;
bezierconf3.controlPoint_1 = ccp(-200, 5);
bezierconf3.controlPoint_2 = ccp(300, 100);
bezierconf3.endPosition = ccp(1000,5);
NSArray *myarray;
myarray = [[NSArray arrayWithObjects: [CCBezierBy actionWithDuration:3 bezier: bezierconf1],
[CCBezierBy actionWithDuration:3 bezier: bezierconf2],
[CCBezierBy actionWithDuration:3 bezier: bezierconf3],
nil] retain];
return myarray;
}
This works "by accident". Actions are supposed to be one time only. After they've run, they will be released.
However since you store the actions in a separate array, those actions are retained. Therefore you can re-run them. This might work for some actions, other actions may show subtle issues, and some actions may not do anything, leak memory or crash immediately if you do so.
Re-using actions is generally considered bad practice, unless you know the code of each action and you have verified that reusing it doesn't do anything "bad".