cocos2d-x v3.3 ActionManager

ActionManager通过hash表的方式管理所有需要执行的动作,每一个hash元素存储动作的相关信息。这里有必要看一下hash元素的结构体:

typedef struct _hashElement
{
struct _ccArray *actions; // 精灵需要执行的动作集合。这里可以看出,可以为一个精灵分配多个动作。
Node *target; // 需要执行动作的精灵。
int actionIndex; // 动作的索引。这个是为了removeActionAtIndex()与update()动作的过程中配合使用的,下面ActionManager::update()的关键点总结中细说。
Action *currentAction; // 精灵当前正在执行的动作。
bool currentActionSalvaged; // 精灵当前正在执行的动作是否可以被干掉。
bool paused; // 精灵当前正在执行的动作是否处于暂停状态。
UT_hash_handle hh; // 引擎使用uthash的hash表实现,在使用时每个hash元素中必须包含此项。
} tHashElement;

可以看出,ActionManager是以精灵为单位管理动作的。一个精灵可以执行多个动作,多个执行动作的精灵串联在一起形成hash表,这样ActionManager就可以轻松的管理所有需要执行的动作了。

ActionManager的继承关系如下:

1、成员变量:

protected:
struct _hashElement *_targets; // 所有需要执行动作的精灵的hash集合(我觉得这里就可以理解为一个hash表的表头)。
struct _hashElement *_currentTarget; // 当前正在执行动作的精灵。
bool _currentTargetSalvaged; // 当前正在执行动作的精灵是否可以被干掉。

2、成员方法:

(1) void addAction(Action *action, Node *target, bool paused)

该方法用于将动作加入到精灵的动作数组中,即将动作与精灵绑定。

action:需要执行的动作。

target:需要执行动作的精灵。

paused:动作与精灵绑定后是处于暂停状态还是运行状态。

实例:

mySprite->runAction(MoveBy::create(1, 100));

上面的语句内部调用的是:_actionManager->addAction(action, this, !_running);。action是MoveBy;this是mySprite;!_running是以暂停的模式。

实现源码:

void ActionManager::addAction(Action *action, Node *target, bool paused)
{
    CCASSERT(action != nullptr, "");
    CCASSERT(target != nullptr, "");

    tHashElement *element = nullptr;
    // we should convert it to Ref*, because we save it as Ref*
    Ref *tmp = target;
    HASH_FIND_PTR(_targets, &tmp, element);   // 在所需要执行动作的精灵的hash集合中寻找当前精灵是否在其中,如果找到了通过element返回。
    if (! element)   // 如果不在其中,下面就需要新创建一个对应于该精灵的hash元素。
    {
        element = (tHashElement*)calloc(sizeof(*element), 1);    // 为hash元素分配空间。
        element->paused = paused;    // 赋值运行状态。
        target->retain();    // 当前精灵被ActionManager使用,需要“保留下来”。这个在分析引擎的引用计数机制的时候再深究。
        element->target = target;    // 赋值动作的执行者。
        HASH_ADD_PTR(_targets, target, element);    // 将新创建的hash元素加入到大家庭中。
    }

     actionAllocWithHashElement(element);    // 查看element中的actions数组是否有足够的空间,如果没有则为其分配空间。

     CCASSERT(! ccArrayContainsObject(element->actions, action), "");    // 不能重复增加相同的动作。
     ccArrayAppendObject(element->actions, action);    // 前面都是辅助性工作,这里真正的将动作加入到精灵的动作数组中。

     action->startWithTarget(target);    // 调用具体动作的startWithTarget(),多态。
}

void ActionManager::actionAllocWithHashElement(tHashElement *element)
{
    // 4 actions per Node by default
    if (element->actions == nullptr)    // 如果未分配过空间。
    {
        element->actions = ccArrayNew(4);    // 则默认为其分配4个单位的空间,可用于存储4个动作。
    }else 
    if (element->actions->num == element->actions->max)    // 如果空间不足。
    {
        ccArrayDoubleCapacity(element->actions);    // 则重新分配一个为当前空间2倍大小的空间。
    }

}

关键点总结:

♂ 执行动作的精灵下挂着需要执行的动作,ActionManager通过管理执行动作的精灵来管理所有动作。


这个actionAllocWithHashElement(),空间不够就重新分配原先2倍大小的空间,分配空间的上限没有限制?

(2) void ActionManager::removeActionAtIndex(ssize_t index, tHashElement *element)

该方法用于移除精灵需要执行的第index个动作。

index:待移除的动作索引。

element:执行动作的精灵所对应的hash元素。

实例:

很多ActionManager中的移除动作方法底层均是调用的该方法,例如ActionManager::removeAction()、ActionManager::removeActionByTag()、ActionManager::removeAllActionsByTag()。

实现源码:

void ActionManager::removeActionAtIndex(ssize_t index, tHashElement *element)
{
    Action *action = (Action*)element->actions->arr;    // 找到该精灵需要删除的动作。

    if (action == element->currentAction && (! element->currentActionSalvaged))    // 如果待移除的动作为当前精灵正在执行的动作,并且该动作不能被干掉。
    {
        element->currentAction->retain();    // 则“保留”这个动作。
        element->currentActionSalvaged = true;    // 并将其标记为(在动作执行结束后)可以被干掉。
    }

    ccArrayRemoveObjectAtIndex(element->actions, index, true);    // 将待移除动作从精灵的动作数组中移除。

    // update actionIndex in case we are in tick. looping over the actions
    
    /* 这里是为了和ActionManager::update()配合,保证更新精灵下的每个动作,不遗漏。
     * ActionManager::update()更新动作的流程是:
     * 从ActionManager::_target中获取第一个需要执行动作的精灵,之后依次遍历出该精灵下挂着的所有待执行动作,每找到一个动作就调用其step()以更新该动作。
     * 当该精灵下挂着的所有动作均更新完成后,继续从ActionManager::_target中获取第二个需要执行动作的精灵,更新其下挂着的所有动作,
     * 这样直至更新了ActionManager::_target中所有精灵的所有动作。这个遍历每个精灵的过程,就是使用element->actionIndex作为索引的。
  • element->actionIndex是当前正在ActionManager::update()中更新着的动作的索引;index是已经被删除的动作的索引。
    * 例如当前element->actionIndex为3,即在ActionManager::update()中正在更新该精灵的第四个动作。此时如果index为2,即想移除该精灵的第三个动作,
    * 那么在上面ccArrayRemoveObjectAtIndex()移除动作后,就要在这里对element->actionIndex–。
    * 因为在ccArrayRemoveObjectAtIndex()中移除一个动作后,将该动作之后的动作使用memmove()整体向前移动,那么原先第五个动作现在变成了第四个动作,
    * 而element->actionIndex如果在ActionManager::update()继续++的话,下一个会更新现在的第五个动作,那么现在的第四个动作就被跳过了,没更新。
    * 所以在这种情况下,就要对element->actionIndex–,让索引回退一个,下一个继续更新现在的第四个动作。
    * 而如果删除的动作索引在当前更新的动作索引之后,就没有任何影响了,element->actionIndex依次++,直到找不到动作就好了。
    * 此处还有个附加的内容,见“关键点总结”中的第二条。
    */
    if (element->actionIndex >= index)
    {
    element->actionIndex–;
    }

      if (element->actions->num == 0)    // 如果删除的是该精灵仅有的最后一个动作,那么该精灵也就无需再被管理了,可以被干掉了。
      {
          if (_currentTarget == element)    // 如果该精灵还在ActionManager::update()中被更新着动作。
          {
              _currentTargetSalvaged = true;    // 则将其标记为(在动作执行结束后)可以被干掉。
          }
          else
          {
              deleteHashElement(element);    // 从hash大家庭中移除该精灵,没有挂着动作的精灵无需再管理。
          }
      }
    

    }

    void ActionManager::deleteHashElement(tHashElement *element)
    {
    ccArrayFree(element->actions); // 将hash元素actions数组所占用空间释放。
    HASH_DEL(_targets, element); // 从hash大家族中删除该hash元素。
    element->target->release(); // 将该hash元素绑定的精灵释放。
    free(element); // 释放该hash所占用的空间。
    }

关键点总结:

♂ 还记得上面说到的ActionManager::actionAllocWithHashElement()?ActionManager通过数组的方式管理动作,所以index可以直接当作动作的索引使用。同时,ccArrayRemoveObjectAtIndex()中也可以看到当减少一个动作后,该动作之后的动作使用memmove()整体向前移动,这也是由于ActionManager通过数组的方式管理动作。

♂ 上面实现源码中着重分析的if语句块部分并没有判断当前操纵的element是否是_currentTarget。举个例子,当前正在更新精灵1的动作,此时删除精灵2的某一个动作,在if语句块部分是在判断精灵2的actionIndex是否大于等于精灵2待删除动作的index,此时的判断并没有什么意义。不过这对ActionManager::update()并没有影响,当轮到精灵2开始更新动作时,精灵2的actionIndex会从0开始。

(3) void ActionManager::removeAction(Action *action)
void ActionManager::removeActionByTag(int tag, Node *target)
void ActionManager::removeAllActionsByTag(int tag, Node *target)

此3个方法分别用于移除指定的动作、移除指定的精灵上第一个tag为指定的tag的动作、移除指定的精灵上所有tag为指定的tag的动作。

action:指定的待移除的动作。

tag:指定的待移除的动作的tag。

target:指定的待被移除动作的精灵。

实现源码:

void ActionManager::removeAction(Action *action)
{
    // explicit null handling
    if (action == nullptr)
    {
        return;
    }

    tHashElement *element = nullptr;
    Ref *target = action->getOriginalTarget();    // 通过动作获取到绑定的精灵。
    HASH_FIND_PTR(_targets, &target, element);    // 在hash大家族中找寻该精灵。
    if (element)
    {
        auto i = ccArrayGetIndexOfObject(element->actions, action);    // 确定待移除的动作在该精灵的动作数组中的索引。
        if (i != CC_INVALID_INDEX)    // 如果不是无效索引。其实代表待移除的动作在该精灵的动作数组中找到了。
        {
            removeActionAtIndex(i, element);    // 通过索引移除该精灵的动作。
        }
    }
    else
    {
        CCLOG("cocos2d: removeAction: Target not found");
    }
}

void ActionManager::removeActionByTag(int tag, Node *target)
{
    CCASSERT(tag != Action::INVALID_TAG, "");
    CCASSERT(target != nullptr, "");

    tHashElement *element = nullptr;
    HASH_FIND_PTR(_targets, &target, element);   // 在所需要执行动作的精灵的hash集合中寻找当前精灵是否在其中,如果找到了通过element返回。

    if (element)    // 如果找到了。
    {
        auto limit = element->actions->num;    // 获取该精灵下一共挂着多少个动作。
        for (int i = 0; i < limit; ++i)
        {
            Action *action = (Action*)element->actions->arr*;    // 依次遍历每个动作。

            if (action->getTag() == (int)tag && action->getOriginalTarget() == target)    // 该动作的tag是否与指定的tag一致,并且该动作所绑定的精灵是否为指定的精灵。
            {
                removeActionAtIndex(i, element);    // 通过索引移除该精灵的动作。
                break;    // 注意这里跳出了,说明只删除了第一个tag为指定tag的动作。ActionManager::removeAllActionsByTag()中对应的这里会continue。
            }
        }
    }
}

// 大部分与ActionManager::removeActionByTag()相同,只注明不同的部分。
void ActionManager::removeAllActionsByTag(int tag, Node *target)
{
    CCASSERT(tag != Action::INVALID_TAG, "");
    CCASSERT(target != nullptr, "");

    tHashElement *element = nullptr;
    HASH_FIND_PTR(_targets, &target, element);

    if (element)
    {
        auto limit = element->actions->num;
        for (int i = 0; i < limit;)
        {
            Action *action = (Action*)element->actions->arr*;
        
            if (action->getTag() == (int)tag && action->getOriginalTarget() == target)
            {
                removeActionAtIndex(i, element);
                --limit;    // 注意这里将精灵所挂动作的总数减1,之后继续删除tag为指定tag的动作。ActionManager::removeActionByTag()中对应的这里会break。
            }
            else
            {
                ++i;
            }
        }
    }
}

关键点总结:*

♂ if (action->getTag() == (int)tag && action->getOriginalTarget() == target)这句话感觉第二个限定条件没有什么必要。element是通过target找到的,element中包含target和action,那么action和target是否绑定还需要判定吗?难道在删除的过程中绑定关系有可能被改变?

(4) void removeAllActions()

该方法用于将所有精灵绑定的所有动作都移除。

实现源码:

void ActionManager::removeAllActions()
{
    for (tHashElement *element = _targets; element != nullptr; )    // 与下面hh.next配合,循环遍历出ActionManager中记录的所有绑定了动作的精灵。
    {
        auto target = element->target;
        element = (tHashElement*)element->hh.next;
        removeAllActionsFromTarget(target);    // 将该精灵身上绑定的动作全部移除。
    }
}

void ActionManager::removeAllActionsFromTarget(Node *target)
{
    // explicit null handling
    if (target == nullptr)
    {
        return;
    }

    tHashElement *element = nullptr;
    HASH_FIND_PTR(_targets, &target, element);   // 在所需要执行动作的精灵的hash集合中寻找当前精灵是否在其中,如果找到了通过element返回。
    if (element)    // 如果找到了。
    {
        // 如果该精灵当前有动作正在运行着,并且当前运行的这个动作不能被干掉。
        if (ccArrayContainsObject(element->actions, element->currentAction) && (! element->currentActionSalvaged))
        {
            element->currentAction->retain();    // 则“保留”这个动作。
            element->currentActionSalvaged = true;    // 并将其标记为(在动作执行结束后)可以被干掉。
        }

        ccArrayRemoveAllObjects(element->actions);    // 将actions数组中的元素全部释放掉。
        if (_currentTarget == element)    // 如果当前正在update()着这个精灵的动作。
        {
            _currentTargetSalvaged = true;    // 则将该精灵标记为(在动作执行结束后)可以被干掉。
        }
        else
        {
            deleteHashElement(element);    // 否则直接从hash表中移除这个精灵。
        }
    }
    else
    {
//        CCLOG("cocos2d: removeAllActionsFromTarget: Target not found");
    }
}

(5) void update(float dt)

该方法用于更新动作。它会调用具体动作的step()方法,当发现动作执行完成后会调用具体动作的stop()方法。

dt:动作的进度。比如10s的动作,当5s的时候调用了update(),此时time应该传入0.5。

实例:

Director::init()中:
    _scheduler->scheduleUpdate(_actionManager, Scheduler::PRIORITY_SYSTEM, false);
Scheduler::scheduleUpdate()的定义:
template <class T>
void scheduleUpdate(T *target, int priority, bool paused)
{
    this->schedulePerFrame((float dt){
        target->update(dt);    // 这里调用的是ActionManager::update()。
    }, target, priority, paused);
}

实现源码:

void ActionManager::update(float dt)
{
    for (tHashElement *elt = _targets; elt != nullptr; )    // 从hash大家族中逐个遍历出动作执行者。
    {
        _currentTarget = elt;    // 这里不直接使用elt,而使用_currentTarget操作,是为了配合其他移除动作的相关方法,详见它们的分析。
        _currentTargetSalvaged = false;    // 该动作执行者正在更新着动作,不能被干掉。(其他移除动作的方法在将该动作执行者的所有动作都移除后,会将该标志置为true)

        if (! _currentTarget->paused)    // 如果当前动作执行者处于非暂停状态。
        {
            // 在下面的循环中,该动作执行者的actions数组有可能会发生变化(有动作被移除)。
            for (_currentTarget->actionIndex = 0; _currentTarget->actionIndex < _currentTarget->actions->num;
                _currentTarget->actionIndex++)    // 使用actionIndex作为当前更新的动作的索引,为了配合ActionManager::removeActionAtIndex(),详见其分析。
            {
                _currentTarget->currentAction = (Action*)_currentTarget->actions->arr;    // 这个动作即将被更新。
                if (_currentTarget->currentAction == nullptr)
                {
                    continue;
                }

                _currentTarget->currentActionSalvaged = false;    // 即将被更新的动作不能被干掉。(当相关移除动作的方法想移除这个动作时,该标志会被置true的)

                _currentTarget->currentAction->step(dt);    // 这里是更新动作的关键点。调用具体动作的step(),step()再调用具体动作的update()以实现动作的更新。

                if (_currentTarget->currentActionSalvaged)    // 如果在更新动作的过程中,该动作被某个移除动作方法标记为可以被干掉,则下面己真的把它干掉了 -_-!
                {
                    // 如果动作在更新过程中要被移除,移除方法中会先retain()这个动作。现在step()结束了,可以安全了移除这个动作了。
                    _currentTarget->currentAction->release();
                } else
                if (_currentTarget->currentAction->isDone())    // 如果当前动作已经执行完成(不需要再被更新)了。
                {
                    _currentTarget->currentAction->stop();    // Action::_target会被置NULL。

                    Action *action = _currentTarget->currentAction;
                    _currentTarget->currentAction = nullptr;    // 这里将其置空是为了防止ActionManager::removeActionAtIndex()将该动作再retain()下来。
                    removeAction(action);    // 移除该动作(不需要被管理了)。
                }

                _currentTarget->currentAction = nullptr;
            }
        }

        // elt, at this moment, is still valid
        // so it is safe to ask this here (issue #490)
        elt = (tHashElement*)(elt->hh.next);    // 先找到下一个需要更新动作的动作执行者。

        // only delete currentTarget if no actions were scheduled during the cycle (issue #481)
        if (_currentTargetSalvaged && _currentTarget->actions->num == 0)    // 如果当前动作执行者可以被干掉并且当前动作执行者下已没有动作。
        {
            deleteHashElement(_currentTarget);    // 删除该动作执行者(不需要被管理了)。
        }
    }

    // issue #635
    _currentTarget = nullptr;    // _currentTarget代表当前正在更新的动作。动作都更新完成了,该成员变量置NULL。
}

关键点总结:

♂ 逐一更新动作的循环中使用_currentTarget->actionIndex作为动作索引是为了配合ActionManager::removeActionAtIndex(),保证删除动作不会导致遗漏需要更新的动作。

♂ fps --> _scheduler->scheduleUpdate() --> ActionManager::update() --> 动作::step() --> 动作::update()。每帧更新动作就是这么个流程,例如每秒24帧,连起来就形成了动画。

(6) Action* getActionByTag(int tag, const Node *target) const

该方法用于在target中找到第一个tag为指定的tag的动作。

tag:待寻找的动作的tag。

target:从该精灵中寻找。

实现源码:

Action* ActionManager::getActionByTag(int tag, const Node *target) const
{
    CCASSERT(tag != Action::INVALID_TAG, "");

    tHashElement *element = nullptr;
    HASH_FIND_PTR(_targets, &target, element);    // 找到该精灵所对应的hash元素。

    if (element)
    {
        if (element->actions != nullptr)
        {
            auto limit = element->actions->num;
            for (int i = 0; i < limit; ++i)
            {
                Action *action = (Action*)element->actions->arr*;

                if (action->getTag() == (int)tag)    // 找到待寻找的动作,返回。
                {
                    return action;
                }
            }
        }
        //CCLOG("cocos2d : getActionByTag(tag = %d): Action not found", tag);
    }
    else
    {
        // CCLOG("cocos2d : getActionByTag: Target not found");
    }

    return nullptr;
}

关键点总结:*

♂ 感觉该方法的流程与多数移除动作相关方法中通过tag找动作的流程相似,觉得应该统一起来。

(7) ssize_t getNumberOfRunningActionsInTarget(const Node *target) const

该方法用于获得管理着指定精灵中多少个动作(需要被更新的动作才会被管理,运行完成的动作无需被管理)。

实现源码:
ssize_t ActionManager::getNumberOfRunningActionsInTarget(const Node *target) const
{
tHashElement *element = nullptr;
HASH_FIND_PTR(targets, &target, element); // 从hash大家族中找到精灵所对应的hash元素。
if (element)
{
return element->actions ? element->actions->num : 0; // 如果该精灵的动作数组还存在(这么多并发问题要考虑 -
-!),返回数组中元素的个数。
}

return 0;

}

关键点总结:

♂ 该方法声明部分上面的注释需要关注:组合起来的动作(例如Sequence)视为1个action。

(8) void pauseTarget(Node *target)
void resumeTarget(Node *target)

这两个方法分别用于暂停和恢复精灵的动作。

target:需要被暂停或者恢复动作的精灵。

实现源码:

void ActionManager::pauseTarget(Node *target)
{
    tHashElement *element = nullptr;
    HASH_FIND_PTR(_targets, &target, element);    // 找到该精灵所对应的hash元素。
    if (element)
    {
        element->paused = true;    // 置该精灵的暂停标志为true。
    }
}

void ActionManager::resumeTarget(Node *target)
{
    tHashElement *element = nullptr;
    HASH_FIND_PTR(_targets, &target, element);    // 找到该精灵所对应的hash元素。
    if (element)
    {
        element->paused = false;    // 置该精灵的暂停标志为false。
    }
}

(9) Vector<Node*> pauseAllRunningActions()
void resumeTargets(const Vector<Node*>& targetsToResume)

这两个方法分别用于暂停所有精灵的所有运行状态的动作,返回一个容器,容器中存储暂停了哪些动作;这个列表可以传递给resumeTargets()用于恢复这些动作。

实现源码:

Vector<Node*> ActionManager::pauseAllRunningActions()
{
    Vector<Node*> idsWithActions;    // 存储暂停了哪些动作的容器。

    for (tHashElement *element=_targets; element != nullptr; element = (tHashElement *)element->hh.next)    // 遍历每个hash元素。
    {
        if (! element->paused)    // 如果处于运行状态。
        {
            element->paused = true;    // 置暂停标志。
            idsWithActions.pushBack(element->target);    // 将该精灵压入容器中。
        }
    }    

    return idsWithActions;    //  返回容器。
}

void ActionManager::resumeTargets(const Vector<Node*>& targetsToResume)
{
    for(const auto &node : targetsToResume) {    // 遍历容器中每个精灵。
        this->resumeTarget(node);    // 恢复。
    }
}