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

news/2024/9/30 13:29:08

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 #533if (! _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();} elseif (_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 negativefloat 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_BINDINGif (_scriptType == kScriptTypeJavascript){if (ScriptEngineManager::sendActionEventToJS(this, kActionUpdate, (void *)&updateDt))return;}
#endifupdate(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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/45206.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

实验一 客户端脚本编程

一、实验目的 通过设计一个个人主页网站,学习常用的HTML标记,学习使用CSS对页面进行美化,掌握JavaScript的语法和常用的浏览器对象,初步学会使用Eclipse创建网站和编辑网页的方法。 二、实验内容和要求 1) 自己设计网页内容,做一张展示自己网页。要求展示的主要内容有:基…

模拟集成电路设计系列博客——7.2.1 流水线ADC基本介绍

7.2.1 流水线ADC基本介绍 流水线ADC和逐次比较型ADC类似,通过迭代搜索查找能够精确反应模拟输入信号的数字码。但是,相比较于通过单个模拟电路执行迭代,流水线ADC有着独立的模拟级来专门执行每次转换。流水线ADC的信号流如下图所示:所有的模拟级在每个时钟周期时进行工作,…

vue2 实现可拖拽悬浮球

实现效果相关代码点击查看代码 <template><div class="float-box"><divclass="button-box"@mousedown="mousedown"@mousemove="mousemove"@touchmove="mousemove"@mouseup="mouseup"@touchstart=&…

Linux 提权-MySQL UDF

本文通过 Google 翻译 MySQL User Defined Functions – Linux Privilege Escalation 这篇文章所产生,本人仅是对机器翻译中部分表达别扭的字词进行了校正及个别注释补充。导航0 前言 1 什么是用户定义函数 (UDF) ? 2 枚举 UDF 漏洞利用条件2.1 手动枚举 UDF 漏洞利用条件2.1…

1.2 陶瓷电容(MLCC)选型----硬件设计指南(持续补充更新)

本系列文章是笔者总结多年工作经验,结合理论与实践进行整理备忘的笔记。希望能直接指导硬件工程师的设计实操,争取每一条设计要点指南都做到有理有据。既能帮助自己温习整理避免遗忘也能帮助其他需要参考的朋友。笔者也会不定期根据遇到的问题和想起的要点进行查漏补缺。如有…

Profibus转Modbus网关帮助PLC实现智能激光设备通讯

通过Profibus转Modbus网关(XD-MDPB100),可以实现PLC与激光设备之间的无缝连接,实现数据的实时传输与指令的可靠执行。本文将深入探讨PLC通过Profibus转Modbus网关(XD-MDPB100)与激光设备进行通讯的应用案例,带您一窥其中的奥秘。它简单易实现,具有良好的兼容性和可靠性…

【Spring】Bean管理

获取Bean 要从IOC容器当中来获取到bean对象,需要先拿到IOC容器对象@Autowiredprivate ApplicationContext applicationContext; //IOC容器对象Spring容器中提供了一些方法,可以主动从IOC容器中获取到bean对象,下面介绍3种常用方式:根据name获取bean Object getBean(String …

记录--createObjectURL这个API真好用,我举几个场景你们就懂了

🧑‍💻 写在开头 点赞 + 收藏 === 学会🤣🤣🤣 前言 随着我用 URL.createObjectURL 这个 API 越来越多次,越发感觉真的是一个很好用的方法,列举一下我在项目中用到它的场景吧~图片预览 以前我们想要预览图片,只能是上传图片到后端后,获取到url然后赋予给img标签,…