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:
- Every action has a
step
and an update
function
-
step
is called every frame with the delta time
-
update
is called every frame with a value between 0 and 1 (which is the normalized time of the action)
- For all
ActionInterval
based actions (of which, MoveBy
and Sequence
are), most DO NOT override the step
function - instead ActionInterval::step
is used
-
ActionInterval::step
calls the update
function
- Container actions like
Sequence
and Spawn
call their internal actions’ update
function from their own update
function
-
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.