遊戲引擎覓真諦

前言

子安這兩天公司下了一個新需求,給遊戲添加一個道具系統。既然是道具自然是在遊戲中使用的,爲了讓玩家看到道具使用的效果,因此我們需要精心的組織一系列的action來進行展示。包括一些列的動作和動畫等。

其中有個小的功能點便是道具要支持玩家能夠選擇多連發,在這裏出現了一個小插曲,需要執行的action有的是需要永久執行的,有的是需要按順序執行的。因此,子安將Sequence、Spawn與RepeatForever一起使用時,發現了RepeatForever動作會失效,經過多方查閱資料才發現原來Sequence、Spawn與RepeatForever不能一起使用,要想達到我們需要的需求。Spawn只需要用這個節點分別執行這兩個action即可,而Sequence則必須使用CallBackFunc創建action的方式來實現。好了接下來便是子安要討論的內容了。

正文

cocos中的定時器

定時器在cocos引擎中可謂是非常重要的,cocos整個遊戲的刷新、重繪都是在定時器中進行的。而子安要實現的功能——道具多連發,由於Repeat只能是等道具這一次發送完成之後纔會開啓第二次發送,因此必須使用定時器schedule來實現道具多發(那種前赴後繼的效果)。

1.schedule類與函數指針

在使用schedule時,子安對這個類產生了極大的興趣,於是子安決定探究一下源代碼,因爲schedule一般都是節點Node在使用,因此,子安在Node類中發現了一下這麼多schedule重載函數:

”’

void Node::schedule(SEL_SCHEDULE selector)
{
    this->schedule(selector, 0.0f, CC_REPEAT_FOREVER, 0.0f);
}

void Node::schedule(SEL_SCHEDULE selector, float interval)
{
    this->schedule(selector, interval, CC_REPEAT_FOREVER, 0.0f);
}

void Node::schedule(SEL_SCHEDULE selector, float interval, unsigned int repeat, float delay)
{
    CCASSERT( selector, "Argument must be non-nil");
    CCASSERT( interval >=0, "Argument must be positive");

    _scheduler->schedule(selector, this, interval , repeat, delay, !_running);
}

void Node::schedule(const std::function<void(float)> &callback, const std::string &key)
{
    _scheduler->schedule(callback, this, 0, !_running, key);
}

void Node::schedule(const std::function<void(float)> &callback, float interval, const std::string &key)
{
    _scheduler->schedule(callback, this, interval, !_running, key);
}

void Node::schedule(const std::function<void(float)>& callback, float interval, unsigned int repeat, float delay, const std::string &key)
{
    _scheduler->schedule(callback, this, interval, repeat, delay, !_running, key);
}

void Node::scheduleOnce(SEL_SCHEDULE selector, float delay)
{
    this->schedule(selector, 0.0f, 0, delay);
}

void Node::scheduleOnce(const std::function<void(float)> &callback, float delay, const std::string &key)
{
    _scheduler->schedule(callback, this, 0, 0, delay, !_running, key);
}

”’

首先子安發現Node類中deschedule都調用了_scheduler->schedule(…)這個函數,於是查看_schedule到底是個什麼東東。

從上圖可以看出來,_schedule是通過導演類獲得的一個Schedule類單例對象。既然Node類的schedule都調用了Schedule類的schedule,我們便直接分析Schedule類的schedule函數。

”’

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

void schedule(const ccSchedulerFunc& callback, void *target, float interval, bool paused, const std::string& key);

void schedule(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused);

void schedule(SEL_SCHEDULE selector, Ref *target, float interval, bool paused);

 /** 這個定時器方法將會被每隔interval秒被調用一次.
@param callback 被調用函數,稍後討論.
@param target 被調用函數所屬的類.
@param interval 調用時間間隔/秒,如果爲0,則每一幀都會被調用.
@param paused 是否停止這個定時器.
@param repeat 重複調用這個callback repeat次.
@param delay 隔多少秒調用一次callback,只對第一次調用有效,後面的則有interval確定
@param 標記這個調用函數, 作爲停止這個定時器時使用,因爲沒有別的方法可以標識 std::function<>仿函數.
@since 從cocos3.0加入這個方法
*/

”’

我們可以看到Schedule一共爲我們提供了四個創建定時器的方法,其中上面兩個是cocos3.0新加入的,和後面的兩個創建定時器的方法唯一不同的便是第一個參數和第二個參數,其中最後一個參數是用來標記std::function<>的,這個我們稍後再討論,先看第一個參數。後面兩個函數的第一個參數爲SEL_SCHEDULE selector,我們進入其定義

很熟悉的感覺,這裏cocos爲我們定義了一組函數指針,用來處理回調
”’

#define CC_CALLFUNC_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_CallFunc>(&_SELECTOR)
#define CC_CALLFUNCN_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_CallFuncN>(&_SELECTOR)
#define CC_CALLFUNCND_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_CallFuncND>(&_SELECTOR)
#define CC_CALLFUNCO_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_CallFuncO>(&_SELECTOR)
#define CC_MENU_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_MenuHandler>(&_SELECTOR)
#define CC_SCHEDULE_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_SCHEDULE>(&_SELECTOR)

”’

子安把這個單獨拿出來討論是因爲記憶中筆者記得_SELECTOR與&_SELECTOR應該都是函數地址的,爲什麼還要取地址符和強轉呢,經過多方查閱資料和求證才發現原來是子安的基礎掌握的不夠紮實。函數名和對函數名取地址雖然指向的都是同一塊地址,但是其意義還是有些不同的_SELECTOR指的是這個函數實體,僅僅這個函數,而&_SELECTOR代表的確實函數指針,它可以指向任何這種類型的函數實體;就如同數組名是數組第一個元素地址、&數組名是整個數組的地址一般。而在以前c語言中確實可以直接用函數名對函數指針進行賦值,原來能夠賦值的都爲左值,並且在賦值的過程中編譯器自動爲我們進行了一個類型轉換——將_SELECTOR轉換成&_SELECTOR,而在c++標準中,類的非靜態成員函數不爲左值,不能進行隱式的類型轉換,如果我們想要爲函數指針賦值,則必須在函數名前加&並進行顯示的類型轉換。

2.函數指針的調用

cocos中的函數指針理解之後我們繼續進入schedule函數的內部查看,因爲類成員函數指針的使用最終肯定會是this->*func()這種形式,我們最終要驗證自己的猜測。在schedule函數的最後我們發現了這個

繼續進入查看

在這裏,cocos將SEL_SCHEDULE selector, Ref* target的值都傳給了對應的成員變量,我們繼續進行搜索。

果不其然我們找到了這個函數指針的調用,而這個trigger函數便是在update函數中調用的。

cocos c++11新標準

cocos自從3.0之後便全面支持了c++11新標準,其中最爲稱道的便是std::function仿函數的加入了。

C++標準是這樣定義的

類模版std::function是一種通用、多態的函數封裝。std::function的實例可以對任何可以調用的目標實體進行存儲、複製、和調用操作,這些目標實體包括普通函數、Lambda表達式、函數指針、以及其它函數對象等。std::function對象是對C++中現有的可調用實體的一種類型安全的包裹(我們知道像函數指針這類可調用實體,是類型不安全的)

通俗來說仿函數就是用來取代單不僅僅只是函數指針的,它比函數指針更爲安全也更爲靈活。

在上面的兩個schedul函數中,第一個參數const ccSchedulerFunc& callback,我們查看其定義。發現

其就是爲void(float)這個仿函數類起了一個別名.

再次發現了這個函數

繼續進入

我們再次尋找_callback

這一次直接調用就行了,沒有使用函數指針

然後我們在傳入回調函數時如果是類的非靜態成員函數,還必須使用std::bind進行綁定,因爲類的成員函數是屬於具體對象的,我們必須要傳入類的對象才能夠調用成功,例如CCMeniItem源碼

例如我們調用schedule便可以採用這種形式:

Lambda表達式

既然仿函數可以包裹Lambda表達式,我們便可直接採用Lambda表達式來代替std::bind進行綁定,同時也減少了成員函數的定義,輕量級的回調成員函數更加方便於閱讀和修改等優點,代碼如下:

”’

schedule([=](float dt)
    {
        //定時器回調函數的內容
    }, "aaa");

”’
這種方式是不是要比std::bind簡單的多呢,還不用定義回調函數。

既然如此,想起了cocos創建action的類也可以是回調函數CallFuncN。查看其定義發現其create函數爲

”’

static CallFuncN * create(const std::function<void(Node*)>& func);

”’
其定義也是一個std::function仿函數,因此我們直接使用仿函數

”’

sp->runAction(Sequence::create(EaseSineIn::create   (bez),
    [=](Node *node){
        node->removeFromParent();
    }, NULL));

”’
發現程序報錯

判斷應該是創建動作時出現的錯誤,是不是直接用Lambda表達式不行呢,顯示的強轉成std::function試試。

”’

sp->runAction(Sequence::create(EaseSineIn::create(bez),
    std::function<void(Node *)>([=](Node *node){
        node->removeFromParent();
    }), NULL));

”’
發現繼續報錯

這個錯誤就很明顯了,CallFuncN是一個繼承自Ref的類,這樣它就加入了cocos的自動內存管理機制中去了,而我們單純的用一個std::function作爲參數傳進去是沒有自動管理內存機制的,因此,只需將代碼改成這樣

”’

sp->runAction(Sequence::create(EaseSineIn::create(bez),
CallFuncN::create([=](Node *node){
    node->removeFromParent();
    }), NULL));

”’
就能成功的執行了,因此我們以後可以在遊戲中愉快的使用仿函數和Lambda表達式了

——Urolzeen寫於2016.12.03

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章