1. 程式人生 > >【cocos2d-x 原始碼解析】幀動畫

【cocos2d-x 原始碼解析】幀動畫

前言

幀動畫是以序列幀輪放的方式來表現一個動畫,就像膠片電影一樣,一張張畫面進行切換,當切換的間隔足夠小時,人眼就看不出中間的間隔,而是一個流暢的視訊。cocos2d-x 中的幀動畫涉及到三個類 AnimationFrame,Animation 和 Animate。AnimationFrame 是對精靈幀 SpriteFrame 的再次封裝,儲存了一幀畫面的資訊;Animation 是一個動畫的資料集合,儲存了所有動畫幀及其它資料;Animate 是在 Animation 上封裝的一個動作,播放動畫時是精靈執行這個動作的過程。

AnimationFrame

AnimationFrame 是動畫幀,它只是對 SpriteFrame 的簡單封裝,它定義三個屬性

SpriteFrame *_spriteFrame;
float _delayUnits;
ValueMap _userInfo;

_delayUnits 姑且稱之為延遲單元數,表示這一幀畫面將持續多少單元時間,一般給它賦值 1,表示一個單元時間,一個單元時間指每一幀的間隔時間;_userInfo 表示這一幀畫面顯示時要廣播的資料,這個屬性暫時沒用到,傳一個空的字典 ValueMap 就行。

AnimationFrame 最重要的函式是 initWithSpriteFrame,用於設定上面那三個屬性的值

bool AnimationFrame::initWithSpriteFrame(SpriteFrame* spriteFrame, float delayUnits, const ValueMap& userInfo)
{
    set
SpriteFrame(spriteFrame); setDelayUnits(delayUnits); setUserInfo(userInfo); return true; }

Animation

Animation 是一個動畫資料類,它定義了下面六個屬性

float _totalDelayUnits;
float _delayPerUnit;
float _duration;
Vector<AnimationFrame *> _frames;
bool _restoreOriginalFrame;
unsigned int _loops;

這六個屬性中比較重要的三個屬性是 _frames,_delayPerUnit 和 _loops;_frames 是動畫幀集合,這是一個 Vector,儲存了所有的 AnimateFrame;_delayPerUnit 是一幀畫面的間隔時間,也就是上面提到的一個單元時間;_loops 是動畫播放時的迴圈次數。

另外三個屬性並不是不重要,而是一般不需要呼叫者關心而已;_restoreOriginalFrame 表示動畫播放完是否恢復到第一幀的畫面,預設是 false,可以通過介面來修改它的值;另外兩個屬性一般不需要呼叫者去設定或讀取它的值,_totalDelayUnits 是指動畫一共需要多少個單元時間,這個數是新增動畫幀的時候自動計算的,前面說到動畫幀有一個屬性延遲單元個數 _delayUnits 表示這個動畫幀需要的單元時間個數,把所有動畫幀的延遲單元個數加起來就是這個動畫需要的總延遲單元個數,動畫幀的延遲單元個數一般為 1,所以動畫的總延遲單元個數一般等於動畫幀的個數;_duration 是指動畫持續的時間,這個時間的計算也很簡單,由總延遲單元個數乘以一個單元時間即可。

Animation 最重要的函式是初始化函式,這個函式設定三個對外屬性 動畫幀單元時間迴圈次數 的值,然後通過遍歷動畫幀計算出 總延遲單元個數

bool Animation::initWithAnimationFrames(const Vector<AnimationFrame*>& arrayOfAnimationFrames, float delayPerUnit, unsigned int loops)
{
    _delayPerUnit = delayPerUnit;
    _loops = loops;

    setFrames(arrayOfAnimationFrames);

    for (auto& animFrame : _frames)
    {
        _totalDelayUnits += animFrame->getDelayUnits();
    }
    return true;
}

獲取動畫持續時間 _duration 的時候通過 總延遲單元個數單元時間 計算出總持續時間,所以,其實上面的 _duration 定義之後並沒有使用,cocos2d-x 的原始碼質量其實並不高~

float Animation::getDuration(void) const
{
    return _totalDelayUnits * _delayPerUnit;
}

還有一個初始化函式是直接傳精靈幀集合進來,然後根據精靈幀建立動畫幀儲存下來,同樣也會計算 總延遲單元個數 _totalDelayUnits

bool Animation::initWithSpriteFrames(const Vector<SpriteFrame*>& frames, float delay/* = 0.0f*/, unsigned int loops/* = 1*/)
{
    _delayPerUnit = delay;
    _loops = loops;

    for (auto& spriteFrame : frames)
    {
        auto animFrame = AnimationFrame::create(spriteFrame, 1, ValueMap());
        _frames.pushBack(animFrame);
        _totalDelayUnits++;
    }

    return true;
}

上面兩個初始化函式都是一次性初始化動畫幀資料,還有一個函式是一次新增一幀資料;這個函式平時用得比較多,不用去建立一個 Vector,也不用等建立完所有動畫幀或精靈幀時再初始化動畫,可以每得到一個精靈幀就新增進來;另外,上面兩個批量初始化函式匯出的 lua 函式好像有問題,所以我一般都是用這個函式來設定動畫資料

void Animation::addSpriteFrame(SpriteFrame* spriteFrame)
{
    AnimationFrame *animFrame = AnimationFrame::create(spriteFrame, 1.0f, ValueMap());
    _frames.pushBack(animFrame);

    // update duration
    _totalDelayUnits++;
}

Animate

Animation 只是一個數據類,它包含了一個幀動畫所需的所有必要資料,但它不是一個可執行的動作,真正用於執行幀動畫的動作是 Animate。cocos2d-x 中的所有動作都有一個共同的基類 Action, Action 下還有一個子類 FiniteTimeAction,表示有限時間內完成的動作;FiniteTimeAction 下有兩個子類 ActionInstant 和 ActionInterval,分別代表瞬時動作和持續動作。毫無疑問,幀動畫是一個持續動作,所以 Animate 繼承自 ActionInterval。

Action

要想解析 Animate,必須先解析它的幾個父類,首先是最頂層的基類 Action;Action 其實很簡單,就定義該動作作用在哪個結點上而已

Node    *_originalTarget;
Node    *_target;

這裡定義了兩個 target,其實指向的是同一個結點,只不過 _target 在開始執行動作的時候賦值,停止動作時會被清空;而 _originTarget 則會一直儲存它的值

void Action::startWithTarget(Node *aTarget)
{
    _originalTarget = _target = aTarget;
}

void Action::stop()
{
    _target = nullptr;
}

startWithTarget 很明顯是開始執行動作,這裡只是設定了作用的結點而已,但其子類肯定會做更多操作的,我們後面再看。

FiniteTimeAction

FiniteTimeAction 顧名思義,有限時間動作表示這個動作是可以在有限時間內完成的;這個類特別簡單,只是定義了一個持續時間的屬性而已

inline float getDuration() const { return _duration; }
inline void setDuration(float duration) { _duration = duration; }

ActionInterval

有限時間也分為兩種,一種是時間持續為 0,立即完成的動作,也就是 ActionInstant,另一種就是時間不為 0,動作會持續一段時間的動作,也就是 ActionInterval。ActionInterval 定義兩個屬性

float _elapsed;
bool   _firstTick;

_elapsed 儲存該動作從執行到現在用了多少時間,_firstTick 表示該動作是否剛執行,動作未執行時該屬性為 true,動作一旦執行了每一步,該屬性就被置為 false。ActionInterval 重寫 Action 的開始執行動作函式,給這個屬性賦初始值

void ActionInterval::startWithTarget(Node *target)
{
    FiniteTimeAction::startWithTarget(target);
    _elapsed = 0.0f;
    _firstTick = true;
}

ActionInterval 另一個重要的函式是動作每執行一步會呼叫的函式 step

void ActionInterval::step(float dt)
{
    if (_firstTick)
    {
        _firstTick = false;
        _elapsed = 0;
    }
    else
    {
        _elapsed += dt;
    }

    float updateDt = MAX(0, MIN(1, _elapsed / MAX(_duration, FLT_EPSILON)));

    if (sendUpdateEventToScript(updateDt, this))
        return;

    this->update(updateDt);
}

當執行第一步時會將 _firstTick 置為 false,將 _elapsed 置為 0,之後每執行一步都會累加過去的時間 _elapsed;然後計算這一步需要的時間,呼叫 update 函式執行相應的操作,update 函式在具體的子類中例項化

Animate

接下來就是看我們的主角 Animate 了,先看一下它的資料域

std::vector<float>* _splitTimes;
int             _nextFrame;
int             _currFrameIndex;
SpriteFrame*    _origFrame;
unsigned int    _executedLoops;

Animation*      _animation;
EventCustom*    _frameDisplayedEvent;
AnimationFrame::DisplayedEventInfo _frameDisplayedEventInfo;

_splitTimes 表示每一幀開始時間佔總時間的比例;_nextFrame 表示下次要顯示的幀下標,\currFrameIndex 表示當前顯示的幀下標;_origFrame 儲存目標精靈原來的精靈幀,如果 _restoreOriginalFrame = true,則動畫播放結束時會使用 _origFrame 來重置精靈的精靈幀;_executedLoops 表示當前動畫執行了幾次。


bool Animate::initWithAnimation(Animation *animation)
{
    CCASSERT(animation != nullptr, "Animate: argument Animation must be non-nullptr");

    float singleDuration = animation->getDuration();

    if (ActionInterval::initWithDuration(singleDuration * animation->getLoops()))
    {
        _nextFrame = 0;
        setAnimation(animation);
        _origFrame = nullptr;
        _executedLoops = 0;

        _splitTimes->reserve(animation->getFrames().size());

        float accumUnitsOfTime = 0;
        float newUnitOfTimeValue = singleDuration / animation->getTotalDelayUnits();

        auto &frames = animation->getFrames();

        for (auto &frame : frames)
        {
            float value = (accumUnitsOfTime * newUnitOfTimeValue) / singleDuration;
            accumUnitsOfTime += frame->getDelayUnits();
            _splitTimes->push_back(value);
        }
        return true;
    }
    return false;
}

在 Animate 的初始化函式 initWithAnimation 中設定資料域的值,首先儲存 Animation 資料,然後重置 _nextFrmae、_origFrame 和 _executedLoops,_currFrameIndex 好像只有在切換精靈幀的時候用到,其實不用儲存為資料域的。最後計算 _splitTimes 的值,newUnitOfTimeValue 的值由動畫總持續時間除以總延遲單元個數,其實就是一個單元時間 _delayPerUnit;accumUnitOfTime 儲存當前幀之前的總延遲單元個數,而 accumUnitOfTime * newUnitOfTimeValue 則是當前幀開始的時間,除以 _duration 就得到當前幀開始時間與總時間的比例。

void Animate::startWithTarget(Node *target)
{
    ActionInterval::startWithTarget(target);
    Sprite *sprite = static_cast<Sprite *>(target);

    CC_SAFE_RELEASE(_origFrame);

    if (_animation->getRestoreOriginalFrame())
    {
        _origFrame = sprite->getSpriteFrame();
        _origFrame->retain();
    }
    _nextFrame = 0;
    _executedLoops = 0;
}

在開始執行函式中,把目標精靈的原始精靈幀儲存下來,然後重置下一幀下標和當前迴圈次數。

void Animate::stop()
{
    if (_animation->getRestoreOriginalFrame() && _target)
    {
        static_cast<Sprite *>(_target)->setSpriteFrame(_origFrame);
    }

    ActionInterval::stop();
}

在停止播放函式中判斷是否要恢復到原始幀,如果要的話則使用前面儲存的 _origFrame 來重置目標精靈的精靈幀。

void Animate::update(float t)
{
    // if t==1, ignore. Animation should finish with t==1
    if (t < 1.0f)
    {
        t *= _animation->getLoops();

        // new loop?  If so, reset frame counter
        unsigned int loopNumber = (unsigned int)t;
        if (loopNumber > _executedLoops)
        {
            _nextFrame = 0;
            _executedLoops++;
        }

        // new t for animations
        t = fmodf(t, 1.0f);
    }

    auto &frames = _animation->getFrames();
    auto numberOfFrames = frames.size();
    SpriteFrame *frameToDisplay = nullptr;

    for (int i = _nextFrame; i < numberOfFrames; i++)
    {
        float splitTime = _splitTimes->at(i);

        if (splitTime <= t)
        {
            _currFrameIndex = i;
            AnimationFrame *frame = frames.at(_currFrameIndex);
            frameToDisplay = frame->getSpriteFrame();
            static_cast<Sprite *>(_target)->setSpriteFrame(frameToDisplay);

            const ValueMap &dict = frame->getUserInfo();
            if (!dict.empty())
            {
                if (_frameDisplayedEvent == nullptr)
                    _frameDisplayedEvent = new (std::nothrow) EventCustom(AnimationFrameDisplayedNotification);

                _frameDisplayedEventInfo.target = _target;
                _frameDisplayedEventInfo.userInfo = &dict;
                _frameDisplayedEvent->setUserData(&_frameDisplayedEventInfo);
                Director::getInstance()->getEventDispatcher()->dispatchEvent(_frameDisplayedEvent);
            }
            _nextFrame = i + 1;
        }
        // Issue 1438. Could be more than one frame per tick, due to low frame rate or frame delta < 1/FPS
        else
        {
            break;
        }
    }
}

update 函式是真正播放動畫的過程,也就是設定目標精靈的精靈幀。

runAction

雖然知道了各層級 Action 做了什麼事,但要理解動作執行的過程,還需要看 Node 類的 runAction 如何處理的。先看一下 runAction 函式的定義

Action * Node::runAction(Action* action)
{
    CCASSERT( action != nullptr, "Argument must be non-nil");
    _actionManager->addAction(action, this, !_running);
    return action;
}

runAction 只是往動作管理器 _actionManager 新增一個動作而已,動作管理器是在 Node 的建構函式中賦值的

_director = Director::getInstance();
_actionManager = _director->getActionManager();

接下來看 ActionManager 類,首先看 addAction 函式

void ActionManager::addAction(Action *action, Node *target, bool paused)
{
    CCASSERT(action != nullptr, "action can't be nullptr!");
    CCASSERT(target != nullptr, "target can't be nullptr!");

    tHashElement *element = nullptr;
    // we should convert it to Ref*, because we save it as Ref*
    Ref *tmp = target;
    HASH_FIND_PTR(_targets, &tmp, element);
    if (! element)
    {
        element = (tHashElement*)calloc(sizeof(*element), 1);
        element->paused = paused;
        target->retain();
        element->target = target;
        HASH_ADD_PTR(_targets, target, element);
    }

     actionAllocWithHashElement(element);

     CCASSERT(! ccArrayContainsObject(element->actions, action), "action already be added!");
     ccArrayAppendObject(element->actions, action);

     action->startWithTarget(target);
}

在 addAction 中為 target 建立一個 element 儲存在雜湊表中,然後在 element 的 actions 陣列中新增新的 action;然後呼叫 action 的 startWithTarget 進行動作初始化操作

到目前為止,我們仍看不到動作是怎麼執行的,runAction 做的事只是將目標和動作儲存在作管理器中,然後呼叫動作的 startWithTarget 函式,這個函式也只是做一些動作初始化工作而已。那動作到底是怎樣執行的呢,我們首先想到的就是計時器,事實上 cocos2d-x 就是使用計時器來實現動作的。在 Dirctor 的 init 函式中新建立一個動作管理器,然後為動作管理器開啟一個排程器

_actionManager = new (std::nothrow) ActionManager();
_scheduler->scheduleUpdate(_actionManager, Scheduler::PRIORITY_SYSTEM, false);

所以遊戲一開始,ActionManager 的 update 函式就會一直被呼叫

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 = (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);
        }
    }

    // issue #635
    _currentTarget = nullptr;
}

在 update 函式中遍歷所有目標精靈,然後遍歷精靈下所有動作,如果動作可以正常執行,則呼叫 step 函式,這就與前面講到的動作對應上了,也就是說最終執行動作時回撥到 Action 的 step 函式,而 step 函式會回撥到 Action 的 update 函式。