动画系统也是Cocos的UI中一个重要的模块,今天对它的运作进行解析。
Action类的介绍
一个动画的基类是Action,其声明如下:
class CC_DLL Action : public Ref, public Clonable
{
public:
virtual std::string description() const;
virtual bool isDone() const;
virtual void startWithTarget(Node *target);
virtual void stop();
virtual void step(float dt);
virtual void update(float time);
Node* getTarget() const { return _target; }
void setTarget(Node *target) { _target = target; }
Node* getOriginalTarget() const { return _originalTarget; }
void setOriginalTarget(Node *originalTarget) { _originalTarget = originalTarget; }
int getTag() const { return _tag; }
void setTag(int tag) { _tag = tag; }
unsigned int getFlags() const { return _flags; }
void setFlags(unsigned int flags) { _flags = flags; }
CC_CONSTRUCTOR_ACCESS:
Action();
virtual ~Action();
protected:
Node *_originalTarget;
Node *_target;
int _tag;
unsigned int _flags;
};
基类中主要包含播放动画的节点_target,用于记录动画信息的_tag,以及step、update等虚函数。
Action又继承出ActionInstant和ActionInterval,其中ActionInstant指的是那些立即执行的操作,例如visible,flip等操作;ActionInterval指的是那些要持续一段时间的操作,在Cocos的UI工程中设置的Scale、Rotate、变色、透明度等大部分都是这种。
它们的类图关系如下:
动画的执行逻辑
接下来我们来看一看Cocos在运行时是如何播放动画的。
Cocos有一个统一的ActionManager来记录当前所有的动画,并执行它们的播放操作。具体的逻辑是,Director中存储了actionManager对象,在初始化的时候将它的update方法注册到了scheduler中,这样就保证了每一帧都会由scheduler调用actionManager的update方法:
bool Director::init(void)
{
// some code....
// action manager
_actionManager = new (std::nothrow) ActionManager();
_scheduler->scheduleUpdate(_actionManager, Scheduler::PRIORITY_SYSTEM, false);
// some code....
}
然后我们看看ActionManager的update函数:
// main loop
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;
}
它会遍历当前所有附加了Action的节点,调用它们的step函数,并传入这一帧的delta time。
我们以一个简单的动画效果Scale为例,来看看step里到底都做了什么,先看看ScaleTo类的声明:
class CC_DLL ScaleTo : public ActionInterval
{
public:
static ScaleTo* create(float duration, float s);
static ScaleTo* create(float duration, float sx, float sy);
static ScaleTo* create(float duration, float sx, float sy, float sz);
virtual void update(float time) override;
CC_CONSTRUCTOR_ACCESS:
ScaleTo() {}
virtual ~ScaleTo() {}
bool initWithDuration(float duration, float s);
bool initWithDuration(float duration, float sx, float sy);
bool initWithDuration(float duration, float sx, float sy, float sz);
protected:
float _startScaleX;
float _startScaleY;
float _startScaleZ;
float _endScaleX;
float _endScaleY;
float _endScaleZ;
float _deltaX;
float _deltaY;
float _deltaZ;
private:
CC_DISALLOW_COPY_AND_ASSIGN(ScaleTo);
};
这个类里记录了目标scale的XYZ、初始scale的XYZ以及中间的变化ScaleXYZ。
它没有step函数的定义,其实这个函数是在它的父类里:
void ActionInterval::step(float dt)
{
if (_firstTick)
{
_firstTick = false;
_elapsed = 0;
}
else
{
_elapsed += dt;
}
float updateDt = std::max(0.0f, // needed for rewind. elapsed could be negative
std::min(1.0f, _elapsed / _duration));
if (sendUpdateEventToScript(updateDt, this)) return;
this->update(updateDt);
_done = _elapsed >= _duration;
}
可以看到,step函数会把dt化为[0,1]之间的比例,最后传入Action自己定义的update函数中,ScaleTo函数的update还是比较简单的:
void ScaleTo::update(float time)
{
if (_target)
{
_target->setScaleX(_startScaleX + _deltaX * time);
_target->setScaleY(_startScaleY + _deltaY * time);
_target->setScaleZ(_startScaleZ + _deltaZ * time);
}
}
其余的Action函数原理也类似。
总而言之,Cocos在scheduler中注册actionManager的方法,在每一帧开头调用actionManager的update函数。该函数会计算所有绑定了action的UI节点,将传入的dt转换为[0,1]之间的值,最后由具体的Action来执行相应的插值操作,计算出UI节点的属性值并在之后进行渲染。这就是CocosUI动画系统的执行逻辑。