【CC2DX引擎分析】Action动作的执行流程源码分析

cocos2dx内Action动作的管理与执行流程在引擎源码上的分析。

本文旨在自己对cocos2dx引擎学习的一个笔记总结,对Action动作源码进行分析,加深对动作执行流程的把握,学习架构并之后更好的提高代码质量。

分析总览

main函数中的Application::getInstance()->run();开始作为入口分析。进入主循环mainLoopdrawScene,主循环中的其他函数此处暂不做具体分析,这里更关注Action相关内容。

用语言概括大致的流程,即ActionManage作为Action动作的核心管理类,生成了定时器update函数,在每个tick里去执行update函数,遍历hash链表,得到存储的Action动作并执行。
顺便一提,用户自定义的定时器是在_eventDispatcher->dispatchEvent(_eventAfterUpdate);里。

大致流程图如下

我们开始逐步分析,从简入繁

何时初始化的ActionManager?

在导演类进行初始化时,ActionManager定时器就已经被初始化并赋值,_scheduler即代表ActionManager的定时器内容,后续在主循环中执行update函数。

// CCDirector.cpp
bool Director::init()
{
    // some code...

    // scheduler
    _scheduler = new (std::nothrow) Scheduler();
    // action manager
    _actionManager = new (std::nothrow) ActionManager();
    _scheduler->scheduleUpdate(_actionManager, Scheduler::PRIORITY_SYSTEM, false);

    // some code...

  return true;
}

主循环中执行update函数

// CCDirector.cpp
void Director::drawScene()
{
   // some code...

    //tick before glClear: issue #533
    if (! _paused)
    {
        _eventDispatcher->dispatchEvent(_eventBeforeUpdate);
        // 这里就进行了Action的动作管理.
        _scheduler->update(_deltaTime);
        _eventDispatcher->dispatchEvent(_eventAfterUpdate);
    }
    // some code...
    return;
}

实际做事的逻辑step

进入update之后,可以找到step函数,step是基类Action的函数,实际上这里运行的是各个继承类的step,为什么需要这么多的step?
因为有些动作需要去执行update,这些动作往往有连续性,比如MoveTo()。有些动作是即时性的,在一个tick内就完成了,比如FlipX()

遍历Action

所有的Action动作在runAction之后,实际上会被添加到ActionManager管理的hash链表里。
在ActionManager::update中遍历链表的每个节点,对节点里的动作数组再进行遍历,转换成Action之后执行对应的step方法。
当执行完成后进行资源释放处理。

执行step。

// CCActionManage.cpp
void ActionManager::update(float dt)
{
    for (tHashElement *elt = _targets; elt != nullptr; )
    {
        _currentTarget = elt;
        _currentTargetSalvaged = false;

        if (! _currentTarget->paused)
        {
            // The 'actions' MutableArray may change while inside this loop.
            for (_currentTarget->actionIndex = 0; _currentTarget->actionIndex < _currentTarget->actions->num;
                _currentTarget->actionIndex++)
            {
                _currentTarget->currentAction = static_cast<Action*>(_currentTarget->actions->arr[_currentTarget->actionIndex]);
                if (_currentTarget->currentAction == nullptr)
                {
                    continue;
                }

                _currentTarget->currentActionSalvaged = false;

                _currentTarget->currentAction->step(dt);

                if (_currentTarget->currentActionSalvaged)
                {
                    // The currentAction told the node to remove it. To prevent the action from
                    // accidentally deallocating itself before finishing its step, we retained
                    // it. Now that step is done, it's safe to release it.
                    _currentTarget->currentAction->release();
                } else
                if (_currentTarget->currentAction->isDone())
                {
                    _currentTarget->currentAction->stop();

                    Action *action = _currentTarget->currentAction;
                    // Make currentAction nil to prevent removeAction from salvaging it.
                    _currentTarget->currentAction = nullptr;
                    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);
        }
        //if some node reference 'target', it's reference count >= 2 (issues #14050)
        else if (_currentTarget->target->getReferenceCount() == 1)
        {
            deleteHashElement(_currentTarget);
        }
    }

    // issue #635
    _currentTarget = nullptr;
}

连续性动作与即时性动作

动作分为连续性动作与即时性动作,前者会经过一段时间的过程才执行完,后者往往在1个tick内就执行完成。

动作的主逻辑一般都写在了update内,为了区分是连续性还是即时性,在Action的基础上分为了ActionInterval(连续性)和CCActionInstant(即时性)。
在引擎循环只调用这两个类的step,继而调用到内部的update实现动作逻辑的完成。

连续性动作

执行update函数的step, speed类的step也会指向到这里。执行持续性动作。

在连续性动作的step中,需要关注的一个点是updateDt的计算。
在这里调用update的时候,对dt重新计算了一遍,值在 0 ~ 1 中间,代表了当前时间点在整个持续动作过程中的某个时间点位置比例
(注意:在Director中,dt是代表游戏已经持续的tick(scheduler中的update参数),而step(即本函数)的dt是已经持续的tick。)

这里做了std::max处理,可能会出现时间倒流,_elapsed为负值时的情况,如果这种情况出现,dt为0。

// CCActionInterval.cpp
void ActionInterval::step(float dt)
{
    if (_firstTick)
    {
        _firstTick = false;
        _elapsed = MATH_EPSILON;
    }
    else
    {
        _elapsed += dt;
    }
    
    // needed for rewind. elapsed could be negative
    float updateDt = std::max(0.0f, std::min(1.0f, _elapsed / _duration));

    if (sendUpdateEventToScript(updateDt, this)) return;
    
    this->update(updateDt);

    _done = _elapsed >= _duration;
}

即时性动作

FlipX举例,它的动作逻辑写在update内,实际里面只是对精灵做了翻转,没有其他内容。

尝试runAction了这个动作,可以发现堆栈也是从step进入。

// CCActionInstant.cpp
void FlipX::update(float time)
{
    ActionInstant::update(time);
    static_cast<Sprite*>(_target)->setFlippedX(_flipX);
}

不执行update函数的step。
下文代码里的update是即时性动作的update。this代表当前的Action动作类。不是ActionInstant::update迷惑了,因为this不同,此时的this不指向ActionInstant,是遍历的动作类!

// CCActionInstant.cpp
void ActionInstant::step(float /*dt*/)
{
    float updateDt = 1;
#if CC_ENABLE_SCRIPT_BINDING
    if (_scriptType == kScriptTypeJavascript)
    {
        if (ScriptEngineManager::sendActionEventToJS(this, kActionUpdate, (void *)&updateDt))
            return;
    }
#endif
    update(updateDt);
}

void ActionInstant::update(float /*time*/)
{
    _done = true;
}

不同的update

既然有了上述架构后,CC2DX在制作动作类的时候,只需要关心这个动作的核心逻辑在update里怎么写就好了。

本文暂时只写一些Action在引擎内的执行流程,是怎么被执行的以及执行过程中一些有意思的点,至于某个动作效果是怎么实现的,后续有时间会在填坑~~

使用动作与动作的回收

runAction

在使用动作时,往往常用runAction进行动作的播放。细究代码,会发现在这个函数里其实并没有对动作进行直接的播放,而是将动作放到了哈希表里addAction

// CCNode.cpp
Action * Node::runAction(Action* action)
{
    CCASSERT( action != nullptr, "Argument must be non-nil");
    _actionManager->addAction(action, this, !_running);
    return action;
}

依旧是ActionManager类,大致流程如下。

  1. 通过target在哈希链表找到对应的节点element。通过下断点看了一下,这个target代表的是执行这个动作的节点(精灵)。
  2. 如果没找到就开内存扩容新的节点element,这个taget(精灵)引用计数+1,加入到哈希链表中。
  3. 如果找到了(没找到创建了,步骤2)就给这个element扩容放进去action。并检查action的唯一性,只允许这个精灵动作的唯一性,不然Assert。
  4. 开始这个动作startWithTarget

需要注意的是,一个动作只能给一个精灵使用,如果后来者也runAction了这个动作,那么这个动作就会被后来者使用(唯一性)。
我写过一篇简要的随笔,可见:https://www.cnblogs.com/hatsuzuki/p/18158287

//CCActionManager.cpp
void ActionManager::addAction(Action *action, Node *target, bool paused)
{
    CCASSERT(action != nullptr, "action can't be nullptr!");
    CCASSERT(target != nullptr, "target can't be nullptr!");
    if(action == nullptr || target == nullptr)
        return;

    tHashElement *element = nullptr;
    // we should convert it to Ref*, because we save it as Ref*
    Ref *tmp = target;
    HASH_FIND_PTR(_targets, &tmp, element);
    if (! element)
    {
        element = (tHashElement*)calloc(sizeof(*element), 1);
        element->paused = paused;
        target->retain();
        element->target = target;
        HASH_ADD_PTR(_targets, target, element);
    }

     actionAllocWithHashElement(element);
 
     CCASSERT(! ccArrayContainsObject(element->actions, action), "action already be added!");
     ccArrayAppendObject(element->actions, action);
 
     action->startWithTarget(target);
}

removeAction

在上面的update里,可以见到这里做了释放处理,流程图没有补全的这一块在这里补全。

在这里并没有直接释放,_currentTarget->currentActionSalvaged被标记为true之后才会被释放,这样做的原因是可能当前这个动作并没有播放完,防止意外释放。

释放的简要步骤

  1. 判断释放字段是否为true,true则释放。
  2. 判断动作是否播放完成。
    --- 完成
  3. 完成则置空_currentTarget->currentAction = nullptr防止被用,不过我查了后续的函数也没找到在哪有继续调用这个变量,可能是做了防御性吧。
  4. 释放这个动作,进到ActionManager::removeAction(Action *action)
    4.1 和addAction差不多,在哈希表中找到节点element和下标调用removeActionAtIndex进行标记释放字段。如果当前动作正在执行则标记并计数+1,在下一个tick内释放。
    4.2 在本elelment内删除此action。
    4.3 检查element释放能释放,能则释放。
    --- 未完成 -> pass
if (_currentTarget->currentActionSalvaged)
{
  // The currentAction told the node to remove it. To prevent the action from
  // accidentally deallocating itself before finishing its step, we retained
  // it. Now that step is done, it's safe to release it.
  _currentTarget->currentAction->release();
} else
if (_currentTarget->currentAction->isDone())
{
  _currentTarget->currentAction->stop();

  Action *action = _currentTarget->currentAction;
  // Make currentAction nil to prevent removeAction from salvaging it.
  _currentTarget->currentAction = nullptr;
  removeAction(action);
}

参考链接

https://www.jianshu.com/p/f9f550b1f0d5
https://www.cnblogs.com/wickedpriest/p/12242421.html
https://www.cnblogs.com/pk-run/p/4185559.html

posted @ 2024-06-18 19:03  阿初  阅读(23)  评论(0编辑  收藏  举报