一、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
    • CallFunc, OrbitCamera, Follow, Tween

      二、执行原理

      (一)线性插值

      大部分游戏引擎的动画系统都是基于线性插值原理。线性插值指已知某个时间段两端的端值,和该时间段中的一个时间点,求该时间点在两个端值之间对应的值。

      1、标量插值

      端值为标量,在游戏中标量可以表示游戏对象的属性,如透明度、旋转角度、在一个范围内的攻击力。 ```cpp

// 时间段: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

  1. <a name="rlskL"></a>
  2. ### 2、矢量插值
  3. 端值是多维标量构成的矢量,矢量可表示一个游戏对象的坐标、RGB值等。<br />对矢量的插值,就是对矢量中每个标量的插值。
  4. ```cpp
  5. // 比如二维矢量插值
  6. Vec2 lerp(Vec2 v0, Vec2 v1, float t){
  7. return v0 * (1.0 - t) + v1 * t;
  8. }

3、变换矩阵插值

一般用于3D,咱不展开。

(二)Action中的插值

  1. class CC_DLL Action : public Ref, public Clonable {
  2. public:
  3. // 步进回调,每帧被调用一次,dt表示距离上次调用的时间间隔。
  4. // 通过这一步,可以计算到Action的执行进度,也就是线性插值的时间点(0.0~1.0之间)
  5. virtual void step(float dt);
  6. // 在这里进行线性插值求值
  7. // 时间段固定都是0.0~1.0
  8. // 时间点time,在时间段0.0~1.0之间,从时间上衡量动画的完成度。
  9. // 由step步进回调调用,在这里对Node进行属性修改操作
  10. // 比如 MoveBy/To,在这里应该是node->setPosition
  11. // 比如 TintBy/To,在这里应该是node->setColor
  12. virtual void update(float time);
  13. };
  14. //----------------------以MoveBy为例----------------------
  15. // MoveBy是间隔动画,继承自ActionInterval
  16. void ActionInterval::step(float dt) {
  17. if (_firstTick) { // 第一次执行
  18. _firstTick = false;
  19. // _elapsed,表示该Action距离开始执行过去的过去的时间
  20. // 注意!!!从这一步我们可以知道,每个Action都是带有状态的,记录自身的执行完成度
  21. // 因此,同一个Action是不能同时作用于多个Node上,必须使用Action:clone()来复制一个Action
  22. // 这也是使用原型设计模式的原因。
  23. _elapsed = MATH_EPSILON; // MATH_EPSILON是一个近似于0的值
  24. }
  25. else {
  26. _elapsed += dt; // 累加执行时间,
  27. }
  28. // 求线性插值时间点updateDt
  29. // updateDt = _elapsed / _duration ,且在[0.0f, 1.0f]区间。
  30. float updateDt = std::max(0.0f,std::min(1.0f, _elapsed / _duration) );
  31. // 在子类MoveBy:update中进行线性插值,和对node进行属性修改操作。
  32. this->update(updateDt);
  33. }
  34. void MoveBy::update(float t) { //t是线性插值时间点,[0.0f, 1.0f]区间。
  35. if (_target) {
  36. #if CC_ENABLE_STACKABLE_ACTIONS // 多个MoveBy作用于同一个node会有叠加效果。原理比较简单。
  37. Vec3 currentPos = _target->getPosition3D();
  38. Vec3 diff = currentPos - _previousPosition;
  39. _startPosition = _startPosition + diff;
  40. // _positionDelta 总位移
  41. // _positionDelta * t = 从0到t事件点发生的位移
  42. Vec3 newPos = _startPosition + (_positionDelta * t);
  43. _target->setPosition3D(newPos);
  44. _previousPosition = newPos;
  45. #else // 多个MoveBy作用于同一个node不会有叠加效果。以最后一个执行的MoveBy为最终效果
  46. _target->setPosition3D(_startPosition + _positionDelta * t);
  47. #endif // CC_ENABLE_STACKABLE_ACTIONS
  48. }
  49. }

(三)主循环“驱动”动画

Action的最终执行还是要靠cocos的主循环来“驱动”ActionManager,由ActionManager来执行所有Action。
点击查看【processon】
相关源码:

  1. //--------------CCDirector.cpp
  2. bool Director::init() {
  3. ......
  4. _scheduler = new (std::nothrow) Scheduler(); // scheduler
  5. _actionManager = new (std::nothrow) ActionManager(); // default action manager
  6. // 动画是由Scheduler每帧调用ActionManager.update执行
  7. // 优先级 : Scheduler::PRIORITY_SYSTEM,它是最高的优先级,保证了动画在所有schedule中最先执行。
  8. _scheduler->scheduleUpdate(_actionManager, Scheduler::PRIORITY_SYSTEM, false);
  9. ......
  10. }
  11. //--------------CCScheduler.cpp
  12. // Priority level reserved for system services.
  13. const int Scheduler::PRIORITY_SYSTEM = INT_MIN;
  14. // Minimum priority level for user scheduling.
  15. const int Scheduler::PRIORITY_NON_SYSTEM_MIN = PRIORITY_SYSTEM + 1;
  16. //--------------CCActionManager.cpp
  17. void ActionManager::update(float dt) {
  18. // 执行每个!paused的Action,dt为距离上次调用的间隔时间,用于各Action累加时间。
  19. ......
  20. _currentTarget->currentAction->step(dt);
  21. ......
  22. }

(四)执行Action:ActionManager

cocos所有的Action的都由ActionManager管理,同样也是由它来统一安排执行。

cocos引入了uthash库来实现hash表,详见uthash

  1. class CC_DLL ActionManager : public Ref {
  2. ......
  3. protected:
  4. // 通过一个hash表来存储所有的Action,详细结构见下面_hashElement
  5. struct _hashElement *_targets; // 存储所有Action的hash表
  6. struct _hashElement *_currentTarget; // 当前正在执行update的Action的target
  7. // 如果在removeAction之后,该target的actions为空了,ActionManager会把该hash元素清除
  8. // 但是,如果该target的action正在执行update(值为true),则不能立即清除,
  9. // 这时候就需要这个标记,使得该target在刚好执行完action的update的时,再进行清除。
  10. bool _currentTargetSalvaged;
  11. };
  12. // hash表中元素的结构
  13. typedef struct _hashElement
  14. {
  15. // action数组,存储一个target的所有action
  16. // actions.num 表示该数组当前存储的元素值
  17. // actions.max 表示该数组的容量
  18. // cocos在ccCArray.h中定义了相关的操作接口
  19. struct _ccArray *actions;
  20. // node节点,hash表以此为key,transforms it into a bucket number
  21. Node *target;
  22. // 如果是合法值,即在[0, actions.num)范围内,则表示该target的第actionIndex个action正在执行update
  23. // 如果不是,则该target的action没有在执行update。
  24. int actionIndex;
  25. // 意义同actionIndex
  26. Action *currentAction;
  27. // 类似_currentTargetSalvaged
  28. // 如果removeAction的action刚好就是currentAction,则不能马上删除该action
  29. // 而是在该action的update执行完成之后在删除。
  30. bool currentActionSalvaged;
  31. // target的action是否处于pause状态
  32. bool paused;
  33. // uthash的用法,makes this structure hashable.
  34. // 不需要赋值,使用名字hh可以使用更简化的调用接口。
  35. UT_hash_handle hh;
  36. } tHashElement;

ActionManager大部分的API都是查插删Action,也就是对hash表的查插删,这部分内容在uthash文章中有详细介绍,这里不再赘述。
这里主要学习一下Action是如何被执行的。

  1. //随着游戏主循环,每帧执行
  2. void ActionManager::update(float dt) {
  3. // 遍历hash表中的所有元素
  4. for (tHashElement *elt = _targets; elt != nullptr; ) {
  5. // _currentTarget的action正在执行update
  6. _currentTarget = elt;
  7. _currentTargetSalvaged = false;
  8. // 执行_currentTarget的所有action
  9. if (! _currentTarget->paused) {
  10. // The 'actions' MutableArray may change while inside this loop.
  11. // 遍历_currentTarget的actions数组
  12. for (_currentTarget->actionIndex = 0;
  13. _currentTarget->actionIndex < _currentTarget->actions->num;
  14. _currentTarget->actionIndex++) {
  15. // _currentTarget->currentAction正在执行update
  16. _currentTarget->currentAction = static_cast<Action*>
  17. (_currentTarget->actions->arr[_currentTarget->actionIndex]);
  18. if (_currentTarget->currentAction == nullptr) continue;
  19. _currentTarget->currentActionSalvaged = false;
  20. // 执行update
  21. _currentTarget->currentAction->step(dt);
  22. if (_currentTarget->currentActionSalvaged) {
  23. // The currentAction told the node to remove it. To prevent the action from
  24. // accidentally deallocating itself before finishing its step, we retained
  25. // it. Now that step is done, it's safe to release it.
  26. _currentTarget->currentAction->release();
  27. }
  28. else if (_currentTarget->currentAction->isDone()) {
  29. _currentTarget->currentAction->stop();
  30. // action的stop函数并不是对action的用户提供,而是在这里被使用,属于回调性质的函数
  31. // 当前该action执行完毕时,回调。
  32. Action *action = _currentTarget->currentAction;
  33. // Make currentAction nil to prevent removeAction from salvaging it.
  34. _currentTarget->currentAction = nullptr;
  35. removeAction(action);
  36. }
  37. _currentTarget->currentAction = nullptr;
  38. }
  39. }
  40. // elt, at this moment, is still valid so it is safe to ask this here (issue #490)
  41. // 下一个hash元素,执行下一个target的Actions
  42. elt = (tHashElement*)(elt->hh.next);
  43. // only delete currentTarget if no actions were scheduled during the cycle (issue #481)
  44. if (_currentTargetSalvaged && _currentTarget->actions->num == 0) {
  45. deleteHashElement(_currentTarget);
  46. }
  47. //if some node reference 'target', it's reference count >= 2 (issues #14050)
  48. else if (_currentTarget->target->getReferenceCount() == 1) {
  49. deleteHashElement(_currentTarget);
  50. }
  51. }
  52. // issue #635
  53. _currentTarget = nullptr;
  54. }