Checking if action within a sequence is done

Hi,
consider this code

Sprite* sprite = Sprite::create("blue.png");
_layer->addChild(sprite);
        
action = MoveBy::create(5.0, Vec2(100.0, 100.0));
action->retain();
        
sprite->runAction(action);

when action is finished action->isDone() returns true, but when action is wrapped in a sequence, for example

Sprite* sprite = Sprite::create("blue.png");
_layer->addChild(sprite);
        
action = MoveBy::create(5.0, Vec2(100.0, 100.0));
action->retain();
        
sequence = Sequence::create(action, NULL);
sequence->retain();
        
sprite->runAction(sequence);

sequence->isDone() returns true but action->isDone() returns false. I expected all the actions in a sequence be finished when the sequence is finished.

Is it a bug or expected behaviour?

I am using cocos2d-x 3.1.1.

thanks

I did some digging through the code and I know why this is. However, I don’t know if this is a bug or intended functionality. Anyway, (somewhat) simply:

  1. Every action has a step and an update function
  2. step is called every frame with the delta time
  3. update is called every frame with a value between 0 and 1 (which is the normalized time of the action)
  4. For all ActionInterval based actions (of which, MoveBy and Sequence are), most DO NOT override the step function - instead ActionInterval::step is used
  5. ActionInterval::step calls the update function
  6. Container actions like Sequence and Spawn call their internal actions’ update function from their own update function
  7. ActionInterval::step increments an _elapsed variable, which is used in isDone to test if the action is complete

This means that for your Sequence example, the Sequence action’s step function is being called every frame, which in turn calls the Sequence action’s update function. This update function calls the inner actions’ update functions directly. Their step functions are never called. This means the _elapsed variable is never updated for the inner actions, and isDone will always return false.

There is a comment above the step function in the header file that reads: “DON’T override unless you know what you are doing.”

You could try opening an issue and see if someone is willing to look at possibly changing this functionality.

I tried to highlight all the class and function names in the vain of making this post a little easier to read… but I cannot tell if I made it worse. If it’s harder to read, I’ll change it!

While @toojuice laid out the details, I would take your question down a different path.

I would use a Sequence w/CallFunc to determine the end of any finite action or sequence. Also, you shouldn’t need to manually retain each action as the node (or maybe action manager) should retain it when you call ->runAction().

Sprite* sprite = Sprite::create("blue.png");
_layer->addChild(sprite);

auto action = MoveBy::create(5.0, Vec2(100.0, 100.0));
auto callback = CallFunc::create( [this]() {
    this->actionFinished();
});
auto sequence = Sequence::create(action, callback, NULL);
sprite->runAction(sequence); 

void ClassName::actionFinished() 
{
   // do something on complete
   CCLOG("action completed!");
}

If you don’t want to use a lambda, the callback can be created like this

auto callback = CallFunc::create( this, callfunc_selector(ClassName::actionFinished) );
2 Likes

@stevetranby - good point. I was too focused on answering why it was happening that I didn’t ask why @softmine wanted to do it that way.

I would be interested in hearing if he has a good reason to be checking isDone as opposed to your suggestion.

@toojuice

Seems you are right as debugger shows _elapsed always equals to 0. Very good explanation, it improved my understanding of how actions work.

I’ve created bug in issues tracker http://www.cocos2d-x.org/issues/5772

@stevetranby

Thanks for your advise, but it does not fit my needs. I didn’t explain it in my first post just to keep things simple.

What I want to do is run a few actions simultaneously and do something after all are done. So actually I use callback exactly as you proposed, but I have a few sequences and in my callback I need to check if all the actions are done. My idea was to store somewhere all the actions and in callback check if all are done. Unfortunately it is impossible because isDone() works how it works. Second idea was to check if the sequences are done (instead of the actions), but obviously in callback they are not finished yet (they are just after callback). So I ended up with integer counter. Before running the sequence I increment counter and in callback I decrement counter and then check if counter equals to zero. If so, all the actions are finished.

Secondly action and sequence are members of this class. It seems that when an action is finished it is released. Without retain() access to action causes access violation error. Alternatively it can be stored in cocos2d::Vector, which will do retain() in pushBack().

@softmine - I think you should still be able to do this using @stevetranby’s suggestion by using a combination of Spawn and Sequence. For illustration, here is some code:

Vector<FiniteTimeAction *> allActions; // Vector to store all actions to run simultaneously

allActions.pushBack(action1);
allActions.pushBack(action2);
allActions.pushBack(action3);

CallFunc *callback = ... // fill in with your call back details

Spawn *simultaneousActions = Spawn::create(allActions);
Sequence *actionsPlusCallback = Sequence::create(simultaneousActions, callback, NULL);

sprite->runAction(actionsPlusCallback);

The Spawn action will run all of your actions simultaneously. These actions can even include other Spawn or Sequence actions. Then wrapping the Spawn and the CallFunc in a Sequence allows you to call the callback once after all actions have completed.

This would work great, if I had one sprite. Unfortunately I want to run each action with different sprite.

Ok, I think I can still work with this. What about using TargetedAction?

TargetedAction *targetedAction1 = TargetedAction::create(sprite1, action1);
TargetedAction *targetedAction2 = TargetedAction::create(sprite2, action2);
...
TargetedAction *targetedActionN = TargetedAction::create(spriteN, actionN);

Vector<FiniteTimeAction *> allActions; // Vector to store all actions to run simultaneously

allActions.pushBack(targetedAction1);
allActions.pushBack(targetedAction2);
...
allActions.pushBack(targetedActionN);

Spawn *simultaneousActions = Spawn::create(allActions);
Sequence *actionsPlusCallback = Sequence::create(simultaneousActions, callback, NULL);

this->runAction(actionsPlusCallback);
4 Likes

Yes, this works great. It’s probably the best solution for my case. Thank you very much for your help.

PS. I believe this code could be placed in Wiki or in cocos2d_tests project, it’s a good example of advanced actions usage.