cocos2d-x 4.0 學習之路(十四)動作監聽(CallFunc)

今天我們來學習另一個動作–CallFunc,它是一種特殊的動作,它是看不見的,不像之前我們用的動作都非常具體,比如移動、跳躍、旋轉等。CallFunc的作用就是回調一個函數。
用CallFunc我們可以實現動作監聽。比如,我們想讓一個精靈移動到目的之後,通知我們它到了(顯示一個Label)。用CallFunc就很容易實現。

bool HelloWorld::init() {
    auto moveBy = MoveBy::create(1.0f, Vec2(500, 0));
    auto arrived = [&]() {
        auto visibleSize = Director::getInstance()->getVisibleSize();
        auto label = Label::createWithTTF("*** I arrived! ***", "fonts/arial.ttf", 24);
        label->setPosition(visibleSize.width / 2, visibleSize.height / 2 + 100);
        this->addChild(label);
    };
    auto callFunc = CallFunc::create(arrived);
    sprite1->runAction(Sequence::create(moveBy, callFunc, nullptr));
    return true;
}

我們看到,當精靈結束移動後,arrived()函數就會被調用,Label就會被創建顯示。效果:
在這裏插入圖片描述

auto arrived = [](){};這個回調函數其實就是Lambda函數。這是C++的,與cocos2dx無關。從cocos2dx3.0纔開始支持這種類型的函數回調。
和java裏面的Lambda類似,它是一個匿名函數。它的結構是:[] () {}
[] :表示要開始一個Lambda函數
() :裏面可以填寫函數的參數
{} :裏面是函數的內容

我們寫一個最簡單的例子:

auto func = [](){
	log("test");
};
auto callFunc = CallFunc::create(func);
this->runAction(callFunc);

這樣可以在輸出裏面看到test。有人會問,還可以this->runAction?之前不都是sprite->runAction嗎?
其實,只要是繼承於Node的類,都是可以使用runAction的。Sprite是直接繼承Node的,而this(也就是我們HelloWorld類)是繼承於Scene,Scene繼承於Node的。所以可以直接用this->runAction來讓它執行動作。

最上面的例子裏,有人會觀察到中括號裏面有個&,這個符號是用來幹啥的呢?你可以把它去掉試一下,是不是直接就編譯錯誤了。這裏涉及到一個概念,就是變量的捕獲模式。
說人話就是,在Lambda函數中,你想利用Lambda函數外面的變量時,應該怎麼辦?你不要以爲這個Lambda函數寫在你當前運行的函數中(比如當前我們運行在init中),就可以隨便訪問這個函數中的局部變量或者訪問類的全局變量,那是不可能的。爲什麼呢?因爲Lambda函數,其本質也是一個函數,計算機創建它的時候是另外開闢一段內存空間給它的,和你在哪寫的它沒有關係。也就是說,HelloWorld裏面的this(全局變量),還有sprite(局部變量)等,對於Lambda函數來說是完全看不到的。

那麼,我們還想用this, sprite它們,怎麼辦?就需要在[]中加上指定的符號,就能指定變量的捕獲模式,常用的捕獲模式如下:
[]:不截取任何變量
[&]:截取外部作用域中的所有變量,並且作爲引用在Lambda函數中使用。只要外部作用域中的變量沒有別釋放,在Lambda函數中就可以使用。所以局部變量時不能使用的,因爲局部變量會被釋放。
[=]:截取外部作用域中的所有變量,並且拷貝一份在Lambda函數中使用。即使外部變量的值改變了,在Lambda函數執行的時候,依舊是複製時候的值。
[=, &myvar]:和[=]作用一樣,但是對myvar變量使用引用(也就是[&]方式)。
[myvar]:和[=]作用一樣,但只針對於myvar一個變量,其他變量忽略。

估計大家已經暈了。還是用代碼解釋一下。
在下面的Lambda函數中,我們建立一個label,然後用this把它添加到HelloWorldScene上,那麼這個this就是外部變量,我們要使用它,設定捕獲模式[&],就是用this了。

bool HelloWorld::init() {
    auto arrived = [&]() {
        auto visibleSize = Director::getInstance()->getVisibleSize();
        auto label = Label::createWithTTF("*** I arrived! ***", "fonts/arial.ttf", 24);
        label->setPosition(visibleSize.width / 2, visibleSize.height / 2 + 100);
        this->addChild(label);
    };
    this->runAction(arrived);
    return true;
}

那我們再改一下這個代碼,把Label的定義放到Lambda函數外面:

bool HelloWorld::init() {
	auto visibleSize = Director::getInstance()->getVisibleSize();
    auto label = Label::createWithTTF("*** I arrived! ***", "fonts/arial.ttf", 24);
    label->setPosition(visibleSize.width / 2, visibleSize.height / 2 + 100);
    auto arrived = [&]() {
        this->addChild(label);
    };
    this->runAction(arrived);
    return true;
}

這樣,對於Lambda函數arrived來講,this和Label都是外部變量。那還是使用[&]可以嗎?編譯不過了。因爲this是HelloWorld的全局變量,label是init()函數的局部變量。當init()函數執行完,this是不會被釋放的,而label是會被釋放了(不存在了,你還訪問個啥)。這個地方有點微妙,其實Label這個對象本身是沒有被釋放的,只不過是label這個局部變量被釋放了。換句話說label這個指針被釋放了,但是它指向的對象沒有被釋放。(實在不明白,也不用糾結這塊)
這時候,我們得使用[=]。也就是說,給lambda函數拷貝一份label指針,就OK了。既然Lambda函數是匿名函數,我們當然就可以把函數名省略,直接寫在runAction裏面。

bool HelloWorld::init() {
	auto visibleSize = Director::getInstance()->getVisibleSize();
    auto label = Label::createWithTTF("*** I arrived! ***", "fonts/arial.ttf", 24);
    label->setPosition(visibleSize.width / 2, visibleSize.height / 2 + 100);
    this->runAction([=]() {
        this->addChild(label);
    });
    return true;
}

只要理解了[&]和[=],其他的模式也就好理解了。

關於Lambda函數就說這麼多,對於一般的需求應該是夠用了。如果想精益求精,可以自己找資料多研究研究C++的知識。

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