【CC2DX引擎分析】Action动作的执行流程源码分析
cocos2dx内Action动作的管理与执行流程在引擎源码上的分析。
本文旨在自己对cocos2dx引擎学习的一个笔记总结,对Action动作源码进行分析,加深对动作执行流程的把握,学习架构并之后更好的提高代码质量。
分析总览
从main
函数中的Application::getInstance()->run();
开始作为入口分析。进入主循环mainLoop
的drawScene
,主循环中的其他函数此处暂不做具体分析,这里更关注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
类,大致流程如下。
- 通过
target
在哈希链表找到对应的节点element
。通过下断点看了一下,这个target代表的是执行这个动作的节点(精灵)。
- 如果没找到就开内存扩容新的节点element,这个taget(精灵)引用计数+1,加入到哈希链表中。
- 如果找到了(没找到创建了,步骤2)就给这个element扩容放进去action。并检查action的唯一性,只允许这个精灵动作的唯一性,不然Assert。
- 开始这个动作
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之后才会被释放,这样做的原因是可能当前这个动作并没有播放完,防止意外释放。
释放的简要步骤
- 判断释放字段是否为true,true则释放。
- 判断动作是否播放完成。
--- 完成 - 完成则置空
_currentTarget->currentAction = nullptr
防止被用,不过我查了后续的函数也没找到在哪有继续调用这个变量,可能是做了防御性吧。 - 释放这个动作,进到
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