一、UML
点击查看【processon】
Action
所有Action动画的基类。
使用了原型设计模式,即所有Action是可以复制的,原因是每个Action都是有状态的(记录在一个target上的执行进度),Action和target是一对多的关系,不能够将一个Action同时用于多个target。
ActionInterval
间隔动画,指动画在一定时间内完成,准确的讲是动画在一定时间内的每帧执行。显著的特点就是在创建的时候需要传入时间参数,比MoveBy。
ActionInstance
即时动画,指动画是“立即地瞬间地”完成,准确的讲是动画在下一帧中完成,“立即”指的是下一帧执行,所有Action动画的执行其实就是从下一帧开始,“瞬间”指的是在下一帧的执行中就完成了,从动画启动到完成的时间在一帧之内,和ActionInterval显著的区别就是不需要时间参数,比如CallFunc。
Action可以如下分类:
- 变换动画
- Move, Rotate, Scale, Fade, Tint, etc.
- 组合Action
- Sequence, Spawn, Repeat, Reverse
- Ease Action
- Exp, Sin, Cubic, Elastic, etc.
- Func Action
// 时间段:0.0 ~ 1.0 // 时间点:t(0.0 =< t <= 1.0) // 两端值:在时间点0.0的值为v0;在时间点1.0的值为v1
// 返回在时间点t的值 float lerp(float v0, float v1, float t){ return v0 (1 - t) + v1 t; }
// 显然: // lerp(v0, v1, 0) = v0 // lerp(v0, v1, 1) = v1
<a name="rlskL"></a>### 2、矢量插值端值是多维标量构成的矢量,矢量可表示一个游戏对象的坐标、RGB值等。<br />对矢量的插值,就是对矢量中每个标量的插值。```cpp// 比如二维矢量插值Vec2 lerp(Vec2 v0, Vec2 v1, float t){return v0 * (1.0 - t) + v1 * t;}
3、变换矩阵插值
(二)Action中的插值
class CC_DLL Action : public Ref, public Clonable {public:// 步进回调,每帧被调用一次,dt表示距离上次调用的时间间隔。// 通过这一步,可以计算到Action的执行进度,也就是线性插值的时间点(0.0~1.0之间)virtual void step(float dt);// 在这里进行线性插值求值// 时间段固定都是0.0~1.0// 时间点time,在时间段0.0~1.0之间,从时间上衡量动画的完成度。// 由step步进回调调用,在这里对Node进行属性修改操作// 比如 MoveBy/To,在这里应该是node->setPosition// 比如 TintBy/To,在这里应该是node->setColorvirtual void update(float time);};//----------------------以MoveBy为例----------------------// MoveBy是间隔动画,继承自ActionIntervalvoid ActionInterval::step(float dt) {if (_firstTick) { // 第一次执行_firstTick = false;// _elapsed,表示该Action距离开始执行过去的过去的时间// 注意!!!从这一步我们可以知道,每个Action都是带有状态的,记录自身的执行完成度// 因此,同一个Action是不能同时作用于多个Node上,必须使用Action:clone()来复制一个Action// 这也是使用原型设计模式的原因。_elapsed = MATH_EPSILON; // MATH_EPSILON是一个近似于0的值}else {_elapsed += dt; // 累加执行时间,}// 求线性插值时间点updateDt// updateDt = _elapsed / _duration ,且在[0.0f, 1.0f]区间。float updateDt = std::max(0.0f,std::min(1.0f, _elapsed / _duration) );// 在子类MoveBy:update中进行线性插值,和对node进行属性修改操作。this->update(updateDt);}void MoveBy::update(float t) { //t是线性插值时间点,[0.0f, 1.0f]区间。if (_target) {#if CC_ENABLE_STACKABLE_ACTIONS // 多个MoveBy作用于同一个node会有叠加效果。原理比较简单。Vec3 currentPos = _target->getPosition3D();Vec3 diff = currentPos - _previousPosition;_startPosition = _startPosition + diff;// _positionDelta 总位移// _positionDelta * t = 从0到t事件点发生的位移Vec3 newPos = _startPosition + (_positionDelta * t);_target->setPosition3D(newPos);_previousPosition = newPos;#else // 多个MoveBy作用于同一个node不会有叠加效果。以最后一个执行的MoveBy为最终效果_target->setPosition3D(_startPosition + _positionDelta * t);#endif // CC_ENABLE_STACKABLE_ACTIONS}}
(三)主循环“驱动”动画
Action的最终执行还是要靠cocos的主循环来“驱动”ActionManager,由ActionManager来执行所有Action。
点击查看【processon】
相关源码:
//--------------CCDirector.cppbool Director::init() {......_scheduler = new (std::nothrow) Scheduler(); // scheduler_actionManager = new (std::nothrow) ActionManager(); // default action manager// 动画是由Scheduler每帧调用ActionManager.update执行// 优先级 : Scheduler::PRIORITY_SYSTEM,它是最高的优先级,保证了动画在所有schedule中最先执行。_scheduler->scheduleUpdate(_actionManager, Scheduler::PRIORITY_SYSTEM, false);......}//--------------CCScheduler.cpp// Priority level reserved for system services.const int Scheduler::PRIORITY_SYSTEM = INT_MIN;// Minimum priority level for user scheduling.const int Scheduler::PRIORITY_NON_SYSTEM_MIN = PRIORITY_SYSTEM + 1;//--------------CCActionManager.cppvoid ActionManager::update(float dt) {// 执行每个!paused的Action,dt为距离上次调用的间隔时间,用于各Action累加时间。......_currentTarget->currentAction->step(dt);......}
(四)执行Action:ActionManager
cocos所有的Action的都由ActionManager管理,同样也是由它来统一安排执行。
cocos引入了uthash库来实现hash表,详见uthash。
class CC_DLL ActionManager : public Ref {......protected:// 通过一个hash表来存储所有的Action,详细结构见下面_hashElementstruct _hashElement *_targets; // 存储所有Action的hash表struct _hashElement *_currentTarget; // 当前正在执行update的Action的target// 如果在removeAction之后,该target的actions为空了,ActionManager会把该hash元素清除// 但是,如果该target的action正在执行update(值为true),则不能立即清除,// 这时候就需要这个标记,使得该target在刚好执行完action的update的时,再进行清除。bool _currentTargetSalvaged;};// hash表中元素的结构typedef struct _hashElement{// action数组,存储一个target的所有action// actions.num 表示该数组当前存储的元素值// actions.max 表示该数组的容量// cocos在ccCArray.h中定义了相关的操作接口struct _ccArray *actions;// node节点,hash表以此为key,transforms it into a bucket numberNode *target;// 如果是合法值,即在[0, actions.num)范围内,则表示该target的第actionIndex个action正在执行update// 如果不是,则该target的action没有在执行update。int actionIndex;// 意义同actionIndexAction *currentAction;// 类似_currentTargetSalvaged// 如果removeAction的action刚好就是currentAction,则不能马上删除该action// 而是在该action的update执行完成之后在删除。bool currentActionSalvaged;// target的action是否处于pause状态bool paused;// uthash的用法,makes this structure hashable.// 不需要赋值,使用名字hh可以使用更简化的调用接口。UT_hash_handle hh;} tHashElement;
ActionManager大部分的API都是查插删Action,也就是对hash表的查插删,这部分内容在uthash文章中有详细介绍,这里不再赘述。
这里主要学习一下Action是如何被执行的。
//随着游戏主循环,每帧执行void ActionManager::update(float dt) {// 遍历hash表中的所有元素for (tHashElement *elt = _targets; elt != nullptr; ) {// _currentTarget的action正在执行update_currentTarget = elt;_currentTargetSalvaged = false;// 执行_currentTarget的所有actionif (! _currentTarget->paused) {// The 'actions' MutableArray may change while inside this loop.// 遍历_currentTarget的actions数组for (_currentTarget->actionIndex = 0;_currentTarget->actionIndex < _currentTarget->actions->num;_currentTarget->actionIndex++) {// _currentTarget->currentAction正在执行update_currentTarget->currentAction = static_cast<Action*>(_currentTarget->actions->arr[_currentTarget->actionIndex]);if (_currentTarget->currentAction == nullptr) continue;_currentTarget->currentActionSalvaged = false;// 执行update_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的stop函数并不是对action的用户提供,而是在这里被使用,属于回调性质的函数// 当前该action执行完毕时,回调。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)// 下一个hash元素,执行下一个target的Actionselt = (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;}
