Cocos2d-x入門之旅[1]場景
在遊戲開發過程中,你可能需要一個主選單,幾個關卡和一個END的介面,如何組織管理這些東西呢?
和其他遊戲引擎類似,Cocos也使用了場景(Scene) 這個概念
試想象一部電影或是番劇,你不難發現它是被分解為不同場景或不同時間線的,這些部分就是一個又一個的場景
參考: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 中,通過 Scene
的 addChild()
方法構建場景圖
// 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
類的初始化函式,然後獲得visibleSize
和origin
備用
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的建立
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物件
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
深入探索HelloWorld場景
場景入口
首先,遊戲場景的入口是導演類的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裡進行了什麼操作呢?
- 使用
new
生成物件 - 使用
init
初始化物件 - 使用
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能滿足上面三個功能即