一、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->setColor
virtual void update(float time);
};
//----------------------以MoveBy为例----------------------
// MoveBy是间隔动画,继承自ActionInterval
void 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.cpp
bool 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.cpp
void 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,详细结构见下面_hashElement
struct _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 number
Node *target;
// 如果是合法值,即在[0, actions.num)范围内,则表示该target的第actionIndex个action正在执行update
// 如果不是,则该target的action没有在执行update。
int actionIndex;
// 意义同actionIndex
Action *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的所有action
if (! _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的Actions
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;
}