Cocos2dx Action动画机制

1 动画执行过程

cocos中的动画通过节点执行,如:
node:runAction( cc.Sequence:create(cc.DelayTime:create(1.4),cc.CallFunc:create(removeThis)))
runAction方法将动画添加至动画管理器中。动画管理器将节点的动画信息全部关联至element中,并以节点作为key,element作为value生成hash表便于检索。

void ActionManager::addAction(Action *action, Node *target, bool paused)
{
    if(action == nullptr || target == nullptr)
        return;

    tHashElement *element = nullptr;
    Ref *tmp = target;
    // _targets: target节点作为key,element作为value
    HASH_FIND_PTR(_targets, &tmp, element);
    if (! element)
    {
        element = (tHashElement*)calloc(sizeof(*element), 1);
        element->paused = paused;
        // 当element销毁时 target将执行release
        target->retain();
        element->target = target;
        // 加入至当前hash表
        HASH_ADD_PTR(_targets, target, element);
    }

     actionAllocWithHashElement(element);
     // 将action添加至element中
     ccArrayAppendObject(element->actions, action);
     // 动画关联至节点
     action->startWithTarget(target);
}

addAction方法负责加入动画,update方法负责动画执行的核心逻辑。该方法完成了几个十分重要的工作:动画的更新操作step()、动画的移除与已完成动画的析构。

// main loop
void ActionManager::update(float dt)
{
    for (tHashElement *elt = _targets; elt != nullptr; )
    {
        _currentTarget = elt;
        _currentTargetSalvaged = false;//标志是否release该动画

        if (! _currentTarget->paused)
        {
            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;
}
  • 动画析构
    currentActionSalvaged 用以标志是否析构该动画。为什么需要标志?因为在step方法结束前可能存在移除action的操作。如果在移除动画时就析构动画,就会造成step方法的后续执行存在问题。cocos的做法是:移除action时暂时先暂时保存好这个action以使得step正常执行完成,等待完成后再进行析构。

    void ActionManager::removeActionAtIndex(ssize_t index, tHashElement *element)
    {
        Action *action = static_cast<Action*>(element->actions->arr[index]);
    
        if (action == element->currentAction && (! element->currentActionSalvaged))
        {
            // 暂不析构 加上析构标志
            // 之所以要retain,因为后续的ccArrayRemoveObjectAtIndex会进行一次release
            element->currentAction->retain();
            element->currentActionSalvaged = true;
        }
    
        ccArrayRemoveObjectAtIndex(element->actions, index, true);
    
        // update actionIndex in case we are in tick. looping over the actions
        if (element->actionIndex >= index)
        {
            element->actionIndex--;
        }
    
        if (element->actions->num == 0)
        {
            if (_currentTarget == element)
            {
                _currentTargetSalvaged = true;
            }
            else
            {
                deleteHashElement(element);
            }
        }
    }
  • step : 在时间线上更新action
    ActionManager中的update并非直接调用动画中的update完成动画关联节点的属性更新操作,而是间接调用step来执行。step方法内部将时间折算成0.0-1.0的动画进度,再将进度值传入到具体action的update中。这么做的好处有两个:1)每个action的update方法可以完全忽略时间关联,属性值的计算完全取决于进度值,这种理念使得计算十分便捷,很多时候我们只需计算起点与终点的偏移量,每个进度内的值基于偏移量进行一次线性插值即可;2)极易控制action的效果:如后面将提及的缓动函数:一个节点从某点移动至目标点,先加速、后匀速、后降速到达,此时缓动动画接收到动画的执行进度后利用缓动函数(其实就是一些简单的数学公式)进行一次非线性变化,最后将变换后的进度值供实际action使用。大家通过后续详细介绍的各种动画update函数会发现,将动画的累积执行时间转换为执行的进度会给计算带来很大的便利性。

    void ActionInterval::step(float dt)
    {
        if (_firstTick)
            // 第一次执行step 初始化变量
            _firstTick = false;
            _elapsed = 0;
        }
        else
        {
            // 累积时间
            _elapsed += dt;
        }
        // 对于[0,1]区间,MAX相当于0<=v, MIN相当于v<=1
        // 因此下述操作就是将进度 _elapsed/_duration 限值在[0,1]区间内
        float updateDt = MAX (0,  // needed for rewind. elapsed could be negative
                               MIN(1, _elapsed / _duration)
                              );
        if (sendUpdateEventToScript(updateDt, this)) return;
        // 调用action的update, 将进度作为参数传入
        this->update(updateDt);
    }
    

2 动画中的update机制

2.1 Sequence

  • 创建
    sequence动画将一些列自动化串联起来顺序执行,下一个动画总是等待上一个动画完成后再开始执行。每个sequence携带两个动作。当用多个子action创建一个sequence时,sequence内部对自动化进行两两叠加,组成多个sequence的嵌套。

    Sequence* Sequence::createWithVariableList(FiniteTimeAction *action1, va_list args)
    {
        FiniteTimeAction *now;
        FiniteTimeAction *prev = action1;
        bool bOneAction = true;
    
        while (action1)
        {
            // 循环取子action 取出后与上一个动画组合成新的sequence
            now = va_arg(args, FiniteTimeAction*);
            if (now)
            {
                prev = createWithTwoActions(prev, now);
                bOneAction = false;
            }
            else
            {
                // If only one action is added to Sequence, make up a Sequence by adding a simplest finite time action.
                if (bOneAction)
                {
                    // 值得借鉴的处理手法:即使只有一个action,也会创建一个空的action
                    // 这能够保证一个sequence中总包含两个子动作,保持了统一性,避免后续不必要的麻烦。
                    prev = createWithTwoActions(prev, ExtraAction::create());
                }
                break;
            }
        }
    
        return ((Sequence*)prev);
    }
  • 串联逐个执行
    sequence中总进度区间由两个子动画组成。执行期间,初始化一个_split变量来记录action[0]的区间相对于总区间的占比。

    void Sequence::update(float t)
    {
        int found = 0; // 记录当前正在执行哪个动画:0 or 1
        float new_t = 0.0f; // 记录子action 完成进度
    
        // 进度统计
        if( t < _split )//当前进度落在第一个动画区间内 表明正在执行第一个动画
        {
            // action[0]
            found = 0;
            if( _split != 0 )
                new_t = t / _split;// action[0]区间占比不为0 计算action[0]目前已完成的进度
            else
                new_t = 1; // action[0]区间占比为0(瞬时完成,如回调函数之类),action[0]进度升至1(已完成状态)
    
        }
        else // 当前进度落在第二个动画区间内
        {
            // action[1]
            found = 1;
            if ( _split == 1 ) // _split=1 表明action[0]区间占比为1, action[2]区间占比为0
                new_t = 1; // action[1]进度直接升至1
            else
                new_t = (t-_split) / (1 - _split ); // 计算action[1]目前已完成进度
        }
    
        // 执行相关action
        if ( found==1 )
        {
            if( _last == -1 ) // 当前正在执行action[1] 但上一次并未执行action[0]
            {
                // action[0] was skipped, execute it.
                _actions[0]->startWithTarget(_target);
                if (!(sendUpdateEventToScript(1.0f, _actions[0])))
                    _actions[0]->update(1.0f); // 即刻完成第一个动画
                _actions[0]->stop();
            }
            else if( _last == 0 ) // action[0]已执行过
            {
                // switching to action 1. stop action 0.
                if (!(sendUpdateEventToScript(1.0f, _actions[0])))
                    _actions[0]->update(1.0f); // 即刻完成
                _actions[0]->stop();
            }
        }
        else if(found==0 && _last==1 ) // 上一次执行了action[1], 当前开始执行action[0],这一情形不处理
        {
            // Reverse mode ?
            // FIXME: Bug. this case doesn't contemplate when _last==-1, found=0 and in "reverse mode"
            // since it will require a hack to know if an action is on reverse mode or not.
            // "step" should be overridden, and the "reverseMode" value propagated to inner Sequences.
            if (!(sendUpdateEventToScript(0, _actions[1])))
                _actions[1]->update(0); // 不处理 直接跳过
            _actions[1]->stop();
        }
        // Last action found and it is done.
        if( found == _last && _actions[found]->isDone() )
        {
            return;
        }
    
        // Last action found and it is done
        if( found != _last )
        {
            _actions[found]->startWithTarget(_target);
        }
        if (!(sendUpdateEventToScript(new_t, _actions[found])))
            _actions[found]->update(new_t);
        _last = found;
    }

2.2 Repeat

Repeat的update的执行分两中情形:1)总进度在下一次动作之前;2)总进度在下一次动作之后。对于第一种情形,计算当前正在执行动画的进度,计算公式为 local dt1 = fmodf(dt * _times,1.0f),dt为总进度,并基于该局部进度值更新当前动画。对于第二种情形,强制执行完当前动作,并开启下一个动作。这里有两个细节需要考虑:1. 如何正确结束最后一个动作;2. 如何放置前后两次动画不衔接导致的剧烈抖动。

// issue #80. Instead of hooking step:, hook update: 
// since it can be called by any container action like Repeat, Sequence, Ease, etc..
void Repeat::update(float dt)
{
    if (dt >= _nextDt) // _nextDt:进入下一次重复动作需要完成的进度
    {
        while (dt >= _nextDt && _total < _times) // 进入下一个重复动作阶段 但还未完成所有动作 此处用while 可将所有instant action全部执行完成
        {
            if (!(sendUpdateEventToScript(1.0f, _innerAction)))
                _innerAction->update(1.0f); // 完成前一个动作
            _total++;

            _innerAction->stop();
            _innerAction->startWithTarget(_target); // 开始下一个动作
            _nextDt = _innerAction->getDuration()/_duration * (_total+1); // 更新下一个动作进度
        }

        // fix for issue #1288, incorrect end value of repeat
        if (dt >= 1.0f && _total < _times) // 总进度已经完成 最后一次动作还未结束 直接结束
        {
            if (!(sendUpdateEventToScript(1.0f, _innerAction)))
                _innerAction->update(1.0f); // 直接结束最后一次动作

            _total++;
        }

        // don't set an instant action back or update it, it has no use because it has no duration
        if (!_actionInstant)
        {
            if (_total == _times)
            {
                _innerAction->stop(); // 终止最后一次动作
            }
            else
            {
                // issue #390 prevent jerk, use right update
                // 理论上每一个action的update操作都在下面的else中完成 但完全在else中进行动作更新可能会使得动作在某个时刻发生剧烈抖动 因此无论是dt >= _nextDt还是dt < _nextDt,都对动画进行更新。
                if (!(sendUpdateEventToScript(dt - (_nextDt - _innerAction->getDuration()/_duration), _innerAction)))
                    _innerAction->update(dt - (_nextDt - _innerAction->getDuration()/_duration));
            }
        }
    }
    else
    {
        if (!(sendUpdateEventToScript(fmodf(dt * _times,1.0f), _innerAction)))
            _innerAction->update(fmodf(dt * _times,1.0f)); // fmodf(dt * _times,1.0f):将总进度dt转换为局部进度(当前的action执行进度) _times可看作两个区间的缩放比
    }
}

2.3 RepeatForever

Repeat更新的方式是

void RepeatForever::step(float dt)
{
    _innerAction->step(dt);
    if (_innerAction->isDone()) // 检测动作是否已完成
    {
        float diff = _innerAction->getElapsed() - _innerAction->getDuration(); // 当前Action时间超出部分
        if (diff > _innerAction->getDuration())
            diff = fmodf(diff, _innerAction->getDuration()); // 超出时间大于一个周期 取模校正
        _innerAction->startWithTarget(_target);
        // to prevent jerk. issue #390, 1247
        _innerAction->step(0.0f);
        _innerAction->step(diff);
    }
}

3 缓动动画

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值