1. 程式人生 > >cocos2d-x 系統學習cocos(1)

cocos2d-x 系統學習cocos(1)

簡析HelloWorld場景

以前使用cocos2d-x 3.14的時候,HelloWorld並不是一個場景類,而是一個圖層類,當時的HelloWorld::createScene()是長這樣的

Scene* HelloWorld::createScene()
{
    auto scene = Scene::create();
    auto layer = HelloWorld::create();
    scene->addChild(layer);
    return scene;
}

而現在的3.17的HelloWorld::createScene()長這樣

Scene* HelloWorld::createScene()
{
    return HelloWorld::create();
}

區別就是HelloWorld本身已經是一個場景了,不需要另外生成一個場景再將HelloWorld加到場景中作為子節點

HelloWorld的佈局

HelloWorld場景中有一個cocos的logo,一個關閉按鈕,一個HelloWorld的字樣,這些小物體都是在HelloWorld::init()中生成的

基類的初始化

我們向HelloWorld場景新增東西之前,需要先呼叫基類Scene類的初始化函式,然後獲得一下visibleSize和origin備用

bool HelloWorld::init()
{

    if ( !Scene::init() )
    {
        return false;
    }
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
.................
}

關閉按鈕的生成

bool HelloWorld::init()
{
.................
    auto closeItem = MenuItemImage::create(
                                           "CloseNormal.png",
                                           "CloseSelected.png",
                                           CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));

    float x = origin.x + visibleSize.width - closeItem->getContentSize().width/2;
    float y = origin.y + closeItem->getContentSize().height/2;
    closeItem->setPosition(Vec2(x,y));

    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);
.................
}

這裡的程式碼可能看起來會很複雜,其實不然,我們可以發現cocos裡很多物件在生成的時候都會使用create這個靜態工廠方法,HelloWorld這個場景也不例外,create將生成遊戲物件所需要的引數填進去構造一個物件的指標返回

MenuItemImage的create方法傳入預設狀態的close按鈕的圖片點選狀態下的close按鈕的圖片以及一個回撥,回撥指的是程式對按鈕被按下這個事件做出的響應,使用CC_CALLBACK_1巨集加上一個void(T::*)(Ref*)型別的成員函式的函式指標以及呼叫這個成員函式的物件的指標組成一個回撥,看不懂沒關係,我們照著寫就好
然後就是計算出x和y的值,也就是右下角的按鈕的座標,getContentSize()獲得物件的尺寸,最後使用setPosition設定按鈕的座標

注意,按鈕是不可以直接新增到場景中的,按鈕需要依賴選單,也就是Menu物件,所以我們建立一個包含了closeItem的選單,並設定座標為(0,0),最後才能使用addChild將選單新增到場景中

字型的生成

bool HelloWorld::init()
{
.................
    auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);
    label->setPosition(Vec2(origin.x + visibleSize.width/2,
                                origin.y + visibleSize.height - label->getContentSize().height));
    this->addChild(label, 1);
.................
}

這個也很好理解,createWithTTF返回一個Label物件的指標,顯示的字串字型字型大小作為函式的引數,也是使用addChild新增到場景中,這裡的1比0高一層,我們試著把文字的座標設定到場景中央

bool HelloWorld::init()
{
.................
    auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);
    label->setPosition(Vec2(origin.x + visibleSize.width/2,
                                origin.y + visibleSize.height/2));
    this->addChild(label, 1);
.................
}

文字是在logo上方的,證明圖層越高,渲染得越晚,先渲染的被壓在後渲染的物體下面

精靈的生成

bool HelloWorld::init()
{
.................
    auto sprite = Sprite::create("HelloWorld.png");
    sprite->setPosition(Vec2(visibleSize.width / 2 + origin.x, visibleSize.height / 2 + origin.y));
    this->addChild(sprite, 0);
    return true;
.................
}

這個就更簡單了,使用一張圖片生成一個精靈,同樣也是加到場景中,最後要記得return true,如果init函式不返回true的話程式會崩掉的

深入探索HelloWorld場景

我一直都認為cocos2dx是學習c++的一個非常好的教材,cocos2dx使用了很多面向物件的特性,c++的特性,還有一些設計模式的思想,對一個新手程式設計師的綜合性成長有很大的幫助

遊戲開始的地方

首先,遊戲場景的入口是導演類的runWithScene,開啟AppDelegate.cpp,找到AppDelegate::applicationDidFinishLaunching()函式,我們可以看到這樣的程式碼

bool AppDelegate::applicationDidFinishLaunching() {
    auto director = Director::getInstance();
..............
    auto scene = HelloWorld::createScene();

    director->runWithScene(scene);

    return true;
}

Director類是一個單例類,使用getInstance可以獲得它的例項,單例實際上是通過把建構函式私有化,把物件的訪問許可權交給一個靜態函式實現的,一般我們會使用懶載入來使用這種單例,扯遠了,也就是說Director使用了單例模式
Director通過runWithScene執行HelloWorld場景,並讓HelloWorld以及HelloWorld的子節點工作

Node類

Node類是HelloWorld場景裡我們使用的大部分類的基類,事實上Scene類也是一個Node,很好理解,遊戲世界中的物件實際上大部分都是Node,Node和Node通過父子關係聯絡起來,遊戲裡的物件的模型是一棵樹,父節點使用addChild將子節點加到自己管理的子節點佇列中,遊戲執行的時候,導演就會遍歷這些Node讓他們進行工作,比如說HelloWorld場景,HelloWorld場景是根節點,精靈sprite,文字label,選單menu是HelloWorld的子節點,按鈕closeItem是選單menu的子節點

Ref類

Ref類是用於引用計數的類,負責物件的引用計數,Ref類是Node類的基類,也就是說所有的Node都是使用cocos2dx的引用計數記憶體管理系統進行記憶體管理的,這也是為什麼我們生成物件不是用new和delete,而是用create生成物件的原因。這裡涉及到了GC的知識。簡單來說,引用計數法的理論是,當物件被引用的時候,物件的引用計數會+1,取消引用的時候就-1,當計數為0的時候就將物件銷燬,感興趣可以瞭解一下智慧指標和RAII

create

這個函式我們可以認為它是一個工廠,這個工廠把我們生成物件之前需要做的工作先做好了,在文章達到最開頭有這樣一段程式碼

Scene* HelloWorld::createScene()
{
    return HelloWorld::create();
}

然後HelloWorldScene.h是這樣的

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"

class HelloWorld : public cocos2d::Scene
{
public:
    static cocos2d::Scene* createScene();

    virtual bool init();
    
    void menuCloseCallback(cocos2d::Ref* pSender);
    
    CREATE_FUNC(HelloWorld);
};

#endif

誒,奇怪了,為什麼沒有看到create函式,難道是在基類裡?嗯呣,靜態成員函式是不能繼承的,所以問題出現在CREATE_FUNC上,沒錯!我們看看CREATE_FUNC的定義

#define CREATE_FUNC(__TYPE__) \
static __TYPE__* create() \
{ \
    __TYPE__ *pRet = new(std::nothrow) __TYPE__(); \
    if (pRet && pRet->init()) \
    { \
        pRet->autorelease(); \
        return pRet; \
    } \
    else \
    { \
        delete pRet; \
        pRet = nullptr; \
        return nullptr; \
    } \
}

可以看出來,CREATE_FUNC是一個可以讓你偷懶不用手動編寫create函式的巨集
當然有的類需要客製化create,比如說Sprite的create

Sprite* Sprite::create()
{
    Sprite *sprite = new (std::nothrow) Sprite();
    if (sprite && sprite->init())
    {
        sprite->autorelease();
        return sprite;
    }
    CC_SAFE_DELETE(sprite);
    return nullptr;
}

create裡進行了什麼操作呢?
1.使用new生成物件
2.使用init初始化物件
3.使用autorelease將這個Ref類交給引用計數系統管理記憶體
看到這個init我們是不是想到了什麼,HelloWorld場景的佈局就是在init中實現的,而init由create呼叫,也就是說,在HelloWorld進行create的時候就已經將文字,按鈕,精靈等物件建立並加入到場景中,而這些物件也是通過create建立的,也就是說,場景建立的時候會呼叫所有物件的init,這樣我們對cocos2dx的遊戲流程是不是有了更深的理解
autorelease是Ref類的方法,檢視一下它的定義

Ref* Ref::autorelease()
{
    PoolManager::getInstance()->getCurrentPool()->addObject(this);
    return this;
}

又看到了getInstance,說明PoolManager也是一個單例類,這段程式碼的意思很明顯,將Ref加入到當前記憶體池中管理
我們在後續的開發中經常需要客製化create,只要我們的create能滿足上面三個功能即可

總結

這節我們通過研究cocos2dx新工程自帶的HelloWorld程式碼瞭解到了很多東西,設計模式,GC,遊戲物件結構的設計思路,還有c++的各種小知識,用巨集偷懶啦,巨集保護避免重複編譯啦
嗯?巨集保護是什麼?

巨集保護

這裡順便講一下巨集保護這個小知識點,巨集保護使用來避免.h檔案被重複編譯的,這裡以HelloWorldScene.h為例

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
............
#endif

#這個符號開頭的程式碼是預處理命令,在程式編譯之前會進行預處理工作,這幾行程式碼的意思是,如果沒有定義__HELLOWORLD_SCENE_H__這個符號就定義__HELLOWORLD_SCENE_H__並且編譯到endif為止的內容,當__HELLOWORLD_SCENE_H__被定義過一次,前處理器下一次遇到這條預處理命令的時候就不會再把下面的程式碼作為編譯目標,當然現在vs有一條#pragma once的命令,保證該檔案只編譯一次,也可以達到我們的目