八叶一刀·无仞剑

万物流转,无中生有,有归于无

导航

Cocos动画系统

Posted on 2020-01-30 12:02  闪之剑圣  阅读(394)  评论(0编辑  收藏  举报

动画系统也是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动画系统的执行逻辑。