1. 程式人生 > >‎Cocos2d-x 3.x 學習筆記(三):Scheduler Timer 排程與定時

‎Cocos2d-x 3.x 學習筆記(三):Scheduler Timer 排程與定時

‎1. 概述

Cocos2d-x 的 Scheduler 離不開 Timer。Timer 類是定時器,用來規定一個回撥函式應該在何時被觸發。Timer 封裝了已執行時間、重複次數、已執行次數、延遲秒數、時間間隔、要觸發的回撥函式等等,都是與一個回撥函式觸發相關的成員。

Scheduler 是排程器,用來對 Timer 進行排程,Timer 只是定義了回撥函式的觸發條件、觸發次數等,真正的觸發動作由 Scheduler 執行。

2. Timer 和 TimerTargetSelector、TimerTargetCallback

Timer 的成員:

class CC_DLL Timer : public Ref
{void setupTimerWithInterval(float seconds, unsigned int repeat, float delay); // 
    void setAborted() { _aborted = true; }
    bool isAborted() const { return _aborted; }
    bool isExhausted() const;
    
    virtual void trigger(float dt) = 0;
    virtual void cancel() = 0;
    
    void update(float dt);

    Scheduler* _scheduler; // weak ref
    float _elapsed; // 已執行時間
    bool _runForever; // 是否永遠執行
    bool _useDelay; // 是否使用延遲
    unsigned int _timesExecuted; // 已執行次數
    unsigned int _repeat; // 規定執行次數, 0 = once
    float _delay; // 延遲
    float _interval; // 時間間隔
    bool _aborted; // fff
};

Timer 有兩個子類 TimerTargetSelector、TimerTargetCallback。兩個子類的成員有所不同:

// TimerTargetSelector
Ref* _target;
SEL_SCHEDULE _selector;
// TimerTargetCallback
void* _target;
ccSchedulerFunc _callback;
std::string _key;

這兩個類回撥函式的函式型別不同和 target 型別不同,對應了兩個 Scheduler::schedule(...) 方法的回撥函式指標引數的不同。兩個schedule(...)方法定義如下:

void schedule(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused);
void schedule(const ccSchedulerFunc& callback, void *target, float interval, bool paused, const std::string& key);

TimerTargetSelector 對應的 schedule 方法引數的函式指標為 ccSchedulerFunc& callback,同時引數中包含 key,key 是 Timer 的標誌,有唯一性 。ccSchedulerFunc的定義如下:

typedef std::function<void(float)> ccSchedulerFunc;

這是一個函式型別,函式滿足返回值為空,引數為1個 float。

TimerTargetCallback 對應的 schedule 方法引數的函式指標為 SEL_SCHEDULE selector,不包含 key。為什麼這裡就不含 key 了呢?

看 SEL_SCHEDULE 的定義:

typedef void (Ref::*SEL_SCHEDULE)(float);

SEL_SCHEDULE 是函式指標,函式是 Ref 物件的成員函式,滿足返回值為空,引數為1個float。

不含 key 的原因是這裡定義的是指向類成員函式的指標。指向類成員函式的指標與普通函式指標的區別是,前者不僅要匹配函式的引數型別個數和返回值型別,還要匹配所屬的類的物件。也就是說,selector 起到了 key 的作用,通過 selector 和 target 能找到某個類的物件對應的 Timer,而 callback 和 target 不行,所以 TimerTargetCallback 要加上 Key。

Timer 的 update(float dt) 方法是 Timer 計算時間和執行次數,判斷是否觸發回撥函式和是否銷燬 Timer所執行的函式。在符合觸發條件時呼叫子類的 trigger(_delay) 方法,trigger(_delay)在兩個子類中,呼叫了回撥函式 *_selector 或者 _callback。當符合 isExhausted() 條件,即 Timer 不永遠執行且已重複次數大於等於規定的重複次數數時,呼叫子類的 cancel() 方法,即呼叫子類對應的 unschedule(...) 方法。

3. Scheduler 排程器內 Timer 的定義與銷燬

3.1 Scheduler 2種排程方式

預設排程方式:每幀排程,每幀更新,是不帶間隔的排程,間隔是 interval 變數,使用 scheduleUpdate()

使用者自定義排程方式:帶間隔排程,不每幀更新,使用者通過間隔決定更新時機,使用 schedule(...)

3.2 Scheduler 3個結構體成員

// 每幀排程使用
typedef struct _listEntry
{
    struct _listEntry   *prev, *next;
    ccSchedulerFunc     callback;
    void                *target;
    int                 priority;
    bool                paused;
    bool                markedForDeletion; 
} tListEntry;

typedef struct _hashUpdateEntry
{
    tListEntry          **list;        // Which list does it belong to ?
    tListEntry          *entry;        // entry in the list
    void                *target;
    ccSchedulerFunc     callback;
    UT_hash_handle      hh;
} tHashUpdateEntry;

// 自定義排程使用,帶間隔
typedef struct _hashSelectorEntry
{
    ccArray             *timers;
    void                *target;
    int                 timerIndex;
    Timer               *currentTimer;
    bool                paused;
    UT_hash_handle      hh;
} tHashTimerEntry;

tHashTimerEntry 包含了 UT_hash_handle 型別變數。在結構體中使用 UT_hash_handle 型別,就實現了雜湊連結串列。通過雜湊連結串列連線,每個 Entry 以 void * 型別的 target 作為“key”,Entry 作為“value”。在下面的 schedule(...) 方法中可以知道,對於Entry 的查詢正是通過“key”,即指標 target 來進行的。通過“key”(target)快速找到對應的 Entry。Entry 中包括了 ccArray * 型別的 timers,每個 target 的眾多 Timer 按 ccArray 資料結構排列,timers 是指向這個儲存 Timer 的資料結構的指標。 

剩下兩個結構體在3.6節介紹。

3.3 Scheduler 的 schedule(...) 成員方法

按間隔排程使用Scheduler 的 schedule(...) 方法,該方法可以對 Entry 和 Entry 內部的定時器 Timer 進行定義、修改。

該方法看似有很多過載,實際只根據 Timer 兩個子類,分為兩種,在第2節也有介紹:

// 針對 TimerTargetSelector
void schedule(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused);
// 針對 TimerTargetCallback
void schedule(const ccSchedulerFunc& callback, void *target, float interval, bool paused, const std::string& key);

下面是 TimeTargetSelector 的 schedule(...) 方法內執行的流程圖:

第1節有介紹,因為 TimeTargetSelector 用 selector 對 Timer 有唯一性,所以在判斷 target 的每一個 Timer 是否為要找的 Timer 時,用 selector == timer->getSelector() 進行判斷。而在 TimerTargetCallback 對應的 schedule(...) 方法中,這步判斷改為

key == timer->getKey(),因為此情況下 key 對 Timer有唯一性,故用 key 進行判斷。

3.4 Scheduler 的 unschedule(...) 成員方法

unschedule(...) 根據 Timer 兩個子類,分為兩種:

void unschedule(const std::string &key, void *target);
void unschedule(SEL_SCHEDULE selector, Ref *target);

下面是 TimerTargetSelector 的 unschedule(...)方法執行流程:

Timer 有兩個子類,用 key 或 selector 判斷 Timer 不再贅述。

3.5 Scheduler 的 schedulePerFrame(...) 方法

每幀排程用到的是 schedulePerFrame(...) 方法:

void schedulePerFrame(const ccSchedulerFunc& callback, void *target, int priority, bool paused);

對該方法的呼叫過程如下,從上向下進行:

// ABCScene::init()
this->scheduleUpdate();

// Node::scheduleUpdate()
scheduleUpdateWithPriority(0);

// Node::scheduleUpdateWithPriority(int priority)
_scheduler->scheduleUpdate(this, priority, !_running);

// Scheduler
template <class T>
    void scheduleUpdate(T *target, int priority, bool paused)
    {
        this->schedulePerFrame([target](float dt){
            target->update(dt);
        }, target, priority, paused);
    }

3.2節提到,每幀排程用到了兩個結構體變數:

// 每幀排程使用
typedef struct _listEntry
{
    struct _listEntry   *prev, *next;
    ccSchedulerFunc     callback;
    void                *target;
    int                 priority;
    bool                paused;
    bool                markedForDeletion; 
} tListEntry;

typedef struct _hashUpdateEntry
{
    tListEntry          **list;        // Which list does it belong to ?
    tListEntry          *entry;        // entry in the list
    void                *target;
    ccSchedulerFunc     callback;
    UT_hash_handle      hh;
} tHashUpdateEntry;

tListEntry 以雙向連結串列方式相連,markedForDeletion 標記告訴 Schedule 是否刪除該 Entry,同時儲存了回撥函式、優先順序等。

每幀排程中,一個 target 繫結一個回撥函式,一對一的關係,因為是每幀排程,不需考慮排程的間隔、次數等,所以 target 和回撥函式直接繫結在一個結構體變數,也可以理解成一個 target 綁定了一個 Timer,Timer 中只定義了回撥函式。

而按間隔排程一個 target 繫結多個回撥函式,一對多的關係,因為每個回撥函式排程的時機不同,所以用到 Timer 進行區分,眾多 Timer 組合在一起成為 timers,和一個 target 繫結在一起。

tHashUpdateEntry 通過雜湊連結串列連線,可以快速地通過“key”(target),找到指向 tListEntry 的指標 entry,list 變數用以區分該雜湊連結串列中的 Entry 優先順序與0的大小關係,list 分為3類。

schedulePerFrame(...) 方法執行流程如下:

4. Scheduler 執行排程

終於寫到 update(float dt)方法了。

該方法被呼叫到的順序如下:

Application::run()
Director::mainLoop()
Director::drawScene()
    calculateDeltaTime();
    _scheduler->update(_deltaTime);

update(...) 方法執行的大致流程:

一些 bool 變數的大致作用:

_updateHashLocked 在 Scheduler 建構函式中置 false。在 Scheduler 的 upadte 方法開始置 true,方法結束置 false,該變數。在 Scheduler::removeUpdateFromHash(struct _listEntry *entry) 方法中,當該變數為 false 時,可以刪除一個每幀排程型別的Entry。

if (!_updateHashLocked)
            CC_SAFE_DELETE(element->entry);
        else
        {
            element->entry->markedForDeletion = true;
            _updateDeleteVector.push_back(element->entry);
        }

如果該變數為 true,則排程器正在 update,此時不能直接刪除 Entry,需要把 Entry 加入到 _updateDeleteVector 中,在 update 方法結束前,即所有更新結束後進行刪除。

_currentTargetSalvaged 在建構函式中置 false。在 Scheduler 的 upadte 方法中,把當前遍歷到的 Entry 作為正在執行的 Entry 後,_currentTargetSalvaged 置 false。在 unscheduleAllForTarget(void *target) 和unschedule(const std::string &key, void *target)方法中,如果選擇的 target (Entry)正在執行,則該變數置 true,不在執行則成功刪除 Entry。其餘操作在 Action相關檔案中,暫未學習,在學習後對此處補