Cocos2d-x入門之旅[1]HelloWorld


在游戲開發過程中,你可能需要一個主菜單,幾個關卡和一個END的界面,如何組織管理這些東西呢?

和其他游戲引擎類似,Cocos也使用了場景(Scene) 這個概念,我們的HelloWorld界面就是一個場景

一部電影或是番劇,就是由不同地點或不同時間線組成的,這些部分就是一個又一個的場景

參考:https://www.cnblogs.com/NightFrost/p/11688854.html

場景的存儲結構

為了解釋場景的結構,我們先不看我們過於簡單的helloworld場景,看下面這個官方文檔的場景:

這是一個主菜單場景,這個場景是由很多小的對象拼接而成,所有的對象組合在一起,形成了你看到的結果

場景是被渲染器(renderer)畫出來的,渲染器負責渲染精靈和其它的對象進入屏幕,那渲染器怎么知道什么東西要渲染在后,什么東西要渲染在前呢?

答案是通過場景圖(Scene Graph)實現

場景圖(Scene Graph)

Cocos2d-x使用場景圖(Scene Graph)這一數據結構來安排場景內渲染的對象,場景內所有的節點(Node)都包含在一個樹(tree)上:

Cocos2d-x使用 中序遍歷,先遍歷左子樹,然后根節點,最后是右子樹

中序遍歷下圖的節點,能得到 A, B, C, D, E, F, G, H, I 這樣的序列

現在我們再看這個游戲場景:

分解這場景為5個部分

抽象成數據結構就是:

z-order

樹上的每個元素都會存儲一個z-order,z-order為負的元素,z-order為負的節點會被放置在左子樹,非負的節點會被放在右子樹,實際開發的過程中,你可以按照任意順序添加對象,他們會按照你指定的 z-order 自動排序

在 Cocos2d-x 中,通過 SceneaddChild() 方法構建場景圖

// Adds a child with the z-order of -2, that means
// it goes to the "left" side of the tree (because it is negative)
scene->addChild(title_node, -2);

// When you don't specify the z-order, it will use 0
scene->addChild(label_node);

// Adds a child with the z-order of 1, that means
// it goes to the "right" side of the tree (because it is positive)
scene->addChild(sprite_node, 1);

渲染時 z-order 值大的節點對象后繪制值小的節點對象先繪制,如果兩個節點對象的繪制范圍有重疊,z-order 值大的可能會覆蓋 z-order 值小的,這才實現了我們的需求

HelloWorld場景

現在我們回看我們運行出來的HelloWorld場景,並且具體到代碼操作

場景中有一個我們自己的圖片,一個關閉按鈕,一個HelloWorld的字樣,這些東西都是在HelloWorld::init()中生成的

場景初始化

我們向HelloWorld場景添加東西之前,需要先調用基類Scene類的初始化函數,然后獲得visibleSizeorigin備用

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

關閉按鈕的生成

相關代碼如下

bool HelloWorld::init()
{
	...
        
    /////////////////////////////
    // 2. add a menu item with "X" image, which is clicked to quit the program
    //    you may modify it.

    // add a "close" icon to exit the progress. it's an autorelease object
    auto closeItem = MenuItemImage::create(
                                           "CloseNormal.png",
                                           "CloseSelected.png",
                                           CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));

	if (closeItem == nullptr ||
        closeItem->getContentSize().width <= 0 ||
        closeItem->getContentSize().height <= 0)
    {
        problemLoading("'CloseNormal.png' and 'CloseSelected.png'");
    }
    else
    {
        float x = origin.x + visibleSize.width - closeItem->getContentSize().width/2;
        float y = origin.y + closeItem->getContentSize().height/2;
        closeItem->setPosition(Vec2(x,y));
    }

    // create menu, it's an autorelease object
    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);
    
	...
}

cocos里很多對象在生成的時候都會使用create這個靜態工廠方法,我們創建圖片精靈的時候就用到了auto mySprite = Sprite::create("xxxxxx.png"),HelloWorld這個場景也不例外

MenuItemImage的create方法傳入默認狀態的close按鈕的圖片點擊狀態下的close按鈕的圖片以及一個回調,回調指的是程序對按鈕被按下這個事件做出的響應,看不懂沒關系,照着寫就好

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

然后就是計算出x和y的值,也就是右下角的按鈕的坐標,getContentSize()獲得對象的尺寸,最后使用setPosition設置按鈕的坐標

if (closeItem == nullptr ||
    closeItem->getContentSize().width <= 0 ||
    closeItem->getContentSize().height <= 0)
{
    problemLoading("'CloseNormal.png' and 'CloseSelected.png'");
}
else
{
    float x = origin.x + visibleSize.width - closeItem->getContentSize().width/2;
    float y = origin.y + closeItem->getContentSize().height/2;
    closeItem->setPosition(Vec2(x,y));
}

但是按鈕是不可以直接添加到場景中的,按鈕需要依賴菜單,也就是Menu對象

我們創建一個包含了closeItem的菜單,並設置坐標為(0,0),最后才能使用addChild將菜單添加到場景中

// create menu, it's an autorelease object
auto menu = Menu::create(closeItem, NULL);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1); 

字體的生成

bool HelloWorld::init()
{
	...
        
    auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);
    //Label::createWithTTF(顯示的字符串,字體,字體大小);
    if (label == nullptr)
    {
        problemLoading("'fonts/Marker Felt.ttf'");
    }
    else
    {
        // position the label on the center of the screen
        label->setPosition(Vec2(origin.x + visibleSize.width/2,
                                origin.y + visibleSize.height - label->getContentSize().height));

        // add the label as a child to this layer
        this->addChild(label, 1);
    }
	
    ...
}

這個也很好理解,Label::createWithTTF返回一個Label對象的指針,顯示的字符串字體字體大小作為函數的參數,也是使用addChild添加到場景中,這里的1比0高一層,我們試着把文本的坐標設置到場景中央,修改成如下:

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上方的,驗證了 z-order 值大的節點對象后繪制,值小的節點對象先繪制,先渲染的被壓在后渲染的物體下面

精靈的生成

bool HelloWorld::init()
{
	...
        
	auto sprite = Sprite::create("sinnosuke.png");
    if (sprite == nullptr)
    {
        problemLoading("'HelloWorld.png'");
    }
    else
    {
        // position the sprite on the center of the screen
        sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));
		// Vec2(visibleSize.width/4 + origin.x, visibleSize.height/2 + origin.y)
        // add the sprite as a child to this layer
        this->addChild(sprite, 0);
    }
	
    ...
}

更簡單了,使用一張圖片生成一個精靈,同樣也是加到場景中,最后要記得return true

深入探索場景

場景入口

首先,游戲場景的入口是導演類的runWithScene,打開AppDelegate.cpp,找到AppDelegate::applicationDidFinishLaunching()函數,可以看到:

Copybool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    auto director = Director::getInstance();
	
    ...
    
    // create a scene. it's an autorelease object
    auto scene = HelloWorld::createScene();

    // run
    director->runWithScene(scene);

    return true;
}

Director類是一個單例類,使用getInstance可以獲得它的實例,(單例模式保證系統中應用該模式的類一個類只有一個對象實例)我們需要Director實例來運行運行HelloWorld場景(通過runWithScene),並讓HelloWorld以及HelloWorld的子節點工作

Node類

Node類是HelloWorld場景里我們使用的大部分類的基類(其實Scene類也是一個Node)

游戲世界中的對象實際上大部分都是Node,就像我們一開始提到的,Node和Node通過父子關系聯系起來,形成一棵樹,父節點使用addChild將子節點加到自己管理的子節點隊列中,游戲運行的時候,導演Director就會遍歷這些Node讓他們進行工作

比如我們的HelloWorld場景:HelloWorld場景是根節點,精靈sprite,文本label,菜單menu是HelloWorld的子節點,按鈕closeItem是菜單menu的子節點

Ref類

Ref類是用於引用計數的類,負責對象的引用計數,Ref類是Node類的基類,也就是說所有的Node都是使用cocos2dx的引用計數內存管理系統進行內存管理的,這也是為什么我們生成對象不是用new和delete,而是用create生成對象的原因

簡單來說,引用計數法的理論是,當對象被引用的時候,對象的引用計數會+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

#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

CopySprite* 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

autorelease是Ref類的方法,查看一下它的定義

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

又看到了getInstance,說明PoolManager也是一個單例類,這段代碼的意思很明顯,將Ref加入到當前內存池中管理

我們在后續的開發中經常需要客制化create,只要我們的create能滿足上面三個功能即可


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM