Cocos2d-x中的Node
一.什麼是結點
在介紹Cocos2d-x的結點系統之前,我們需要首先做一些啓蒙,什麼是樹?
定義:
一棵樹(tree)是由n(n>0)個元素組成的有限集合,其中:
(1)每個元素稱爲結點(node);
(2)有一個特定的結點,稱爲根結點或根(root);
(3)除根結點外,其餘結點被分成m(m>=0)個互不相交的有限集合,而每個子集又都是一棵樹(稱爲原樹的子樹)
如圖A:
對於樹結構有幾個概念要記一下:
度:樹的度——也即是寬度,簡單地說,就是結點的分支數。以組成該樹各結點中最大的度作爲該樹的度,如上圖的樹,其度爲3;樹中度爲零的結點稱爲葉結點或終端結點。樹中度不爲零的結點稱爲分枝結點或非終端結點。除根結點外的分枝結點統稱爲內部結點。
深度:樹的深度——組成該樹各結點的最大層次,如上圖,其深度爲3;
層次:根結點的層次爲1,其他結點的層次等於它的父結點的層次數加1.
請仔細觀察上圖這棵樹,這裏A是根結點,其餘結點均是屬於A的不同層級的子結點。我們由此圖進一步進行想像,人的身體其實也是一棵樹。
如圖B:
上圖詳細表現了人體樹結構的組織結構,左邊是人形的結構,右邊是層級關係展開。它作爲骨骼動畫的基礎理論被廣泛的應用於各類遊戲動畫中。
我們看一下這張圖,首先有一個根骨(脊椎),這個根骨即是樹結構中的根結點。根骨下有三根子骨骼(左胯,右胯,頸背),這三根子骨骼也各自有屬於自已的子骨骼樹結構,同時它們由父骨骼牽引並牽引着子骨骼,與父骨骼和第一層子骨骼保持着固定的距離。
試想一下:
當我們想把這個人移動到一個位置點時,只需要把根骨移動到相應位置,即這個人的所有骨骼都會被這種牽引關係移動到這個世界位置的相對骨骼位置。但如果我們把左胯這根骨骼去掉的話,則在移動根骨後,左胯仍停留在原地,它已經不再屬於當前骨骼樹了,而成了一棵獨立的骨骼樹。
二.實現自己的結點
看上張圖,已經比較接近我們所要講述的內容了,對於骨骼結構的理解將有助於我們掌握遠大於骨骼動畫本身的結構模式,因爲由此理論基礎我們將學會一切基於結點樹結構的系統。
下面我們來用C++的代碼構建這樣一套系統。
首先,我們創建一個基類,稱之爲結點。
#ifndef __HelloWorld__CNode__
#define __HelloWorld__CNode__
#include <stdio.h>
using namespace std;
//結點類
class CNode
{
public:
//構造
CNode();
//析構
virtual ~CNode();
public:
//更新
virtual inline void Update();
//渲染
virtual inline void Draw();
public:
//設置當前結點名稱
void SetName(const char* szName);
//取得當前結點名稱
const string& GetName();
//加入一個子結點類
void AddChild(CNode* pChildNode);
//取得子結點
CNode* GetFirstChild();
//加入一個兄弟結點類
void AddBorther(CNode* pBortherNode);
//取得兄弟結點
CNode* GetFirstBorther();
//刪除一個結點
bool DelNode(CNode* pNode);
//清空所有子結點
void DelAllChild();
//清空所有兄弟結點
void DelAllBorther();
//查詢某個子結點-- 縱向查詢
CNode* QueryChild(const char* szName);
//查詢某個兄弟結點-- 橫向查詢
CNode* QueryBorther(const char* szName);
//爲了方便檢測結點樹系統的創建結果,這裏增加了一個保存結點樹到XML文件的函數。
bool SaveNodeToXML(const char* szXMLFile);
protected:
//設置父結點
void SetParent(CNode* pParentNode);
//取得父結點
CNode* GetParent();
//保存結點樹到XML文件,這個函數是隻生成本結點的信息。
bool SaveNodeToXML(FILE* hFile);
private:
//當前結點名稱
string m_strNodeName;
//父結點
CNode* m_pParentNode;
//第一個子結點
CNode* m_pFirstChild;
//第一個兄弟結點
CNode* m_pFirstBorther;
}
;
#endif /* defined(__HelloWorld__CNode__) */
</pre><p></p><p></p><p><span style="white-space:pre"></span>cpp文件,代碼如下:</p><p></p><p> </p><pre name="code" class="cpp">#include "CNode.h"
//構造
CNode::CNode()
{
m_strNodeName[0] = '\0';
m_pParentNode = NULL;
m_pFirstChild = NULL;
m_pFirstBorther = NULL;
}
//析構
CNode::~CNode()
{
DelAllChild();
}
//更新
void CNode::Update()
{
if(m_pFirstChild)
{
m_pFirstChild->Update();
}
//在這裏增加你更新結點的處理
//…
if(m_pFirstBorther)
{
m_pFirstBorther->Update();
}
}
//直接渲染
void CNode::Draw()
{
if(m_pFirstChild)
{
m_pFirstChild->Draw();
}
//在這裏增加你渲染圖形的處理
//…
if(m_pFirstBorther)
{
m_pFirstBorther->Draw();
}
}
//設置當前結點名稱
void CNode::SetName(const char* szName)
{
m_strNodeName = szName ;
}
//取得當前結點名稱
const string& CNode::GetName()
{
return m_strNodeName;
}
//加入一個子結點類
void CNode::AddChild(CNode* pChildNode)
{
if(pChildNode)
{
if(m_pFirstChild)
{
m_pFirstChild->AddBorther(pChildNode);
}
else
{
m_pFirstChild = pChildNode;
m_pFirstChild->SetParent(this);
}
}
}
//取得子結點
CNode* CNode::GetFirstChild()
{
return m_pFirstChild ;
}
//加入一個兄弟結點類
void CNode::AddBorther(CNode* pBortherNode)
{
if(pBortherNode)
{
if(m_pFirstBorther)
{
m_pFirstBorther->AddBorther(pBortherNode);
}
else
{
m_pFirstBorther = pBortherNode;
m_pFirstBorther->SetParent(m_pParentNode);
}
}
}
//取得兄弟結點
CNode* CNode::GetFirstBorther()
{
return m_pFirstBorther ;
}
//刪除一個子結點類
bool CNode::DelNode(CNode* pTheNode)
{
if(pTheNode)
{
if(m_pFirstChild)
{
if(m_pFirstChild == pTheNode)
{
m_pFirstChild = pTheNode->GetFirstBorther();
delete pTheNode;
return true;
}
else
{
if(true == m_pFirstChild->DelNode(pTheNode))return true;
}
}
if(m_pFirstBorther)
{
if(m_pFirstBorther == pTheNode)
{
m_pFirstBorther = pTheNode->GetFirstBorther();
delete pTheNode;
return true;
}
else
{
if(true == m_pFirstBorther->DelNode(pTheNode))return true;
}
}
}
return false;
}
//清空所有子結點
void CNode::DelAllChild()
{
if(m_pFirstChild)
{
CNode * pBorther = m_pFirstChild->GetFirstBorther();
if(pBorther)
{
pBorther->DelAllBorther();
delete pBorther;
pBorther = NULL;
}
delete m_pFirstChild ;
m_pFirstChild = NULL;
}
}
//清空所有兄弟結點
void CNode::DelAllBorther()
{
if(m_pFirstBorther)
{
m_pFirstBorther->DelAllBorther();
delete m_pFirstBorther;
m_pFirstBorther = NULL;
}
}
//查詢某個子結點-- 縱向查詢
CNode* CNode::QueryChild(const char* szName)
{
if(szName)
{
if(m_pFirstChild)
{
//如果是當前子結點,返回子結點。
if(0 == strcmp(szName,m_pFirstChild->GetName().c_str()))
{
return m_pFirstChild;
}
else
{
//如果不是,查詢子結點的子結點。
CNode* tpChildChild = m_pFirstChild->QueryChild(szName);
if(tpChildChild)
{
return tpChildChild ;
}
//如果還沒有,查詢子結點的兄弟結點。
return m_pFirstChild->QueryBorther(szName);
}
}
}
return NULL;
}
//查詢某個兄弟結點-- 橫向查詢
CNode* CNode::QueryBorther(const char* szName)
{
if(szName)
{
if(m_pFirstBorther)
{
if(0 == strcmp(szName,m_pFirstBorther->GetName().c_str()))
{
return m_pFirstBorther;
}
else
{
//如果不是,查詢子結點的子結點。
CNode* tpChildChild = m_pFirstBorther->QueryChild(szName);
if(tpChildChild)
{
return tpChildChild ;
}
return m_pFirstBorther->QueryBorther(szName);
}
}
}
return NULL;
}
//設置父結點
void CNode::SetParent(CNode* pParentNode)
{
m_pParentNode = pParentNode ;
}
//取得父結點
CNode* CNode::GetParent()
{
return m_pParentNode ;
}
//保存結點樹到XML
bool CNode::SaveNodeToXML(const char* szXMLFile)
{
FILE* hFile = fopen(szXMLFile,"wt");
if(!hFile)
{
return false;
}
// fprintf(hFile,TEXT("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"));
// fprintf(hFile,TEXT("<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"));
// fprintf(hFile,TEXT("<!--Honghaier Game Tutorial -->\n"));
//
// fprintf(hFile,TEXT("<plist version=\"1.0\">\n"));
// fprintf(hFile,TEXT("<dict>\n"));
// //========================================================
// fprintf(hFile,TEXT("<key>NodeTree</key>"));
// fprintf(hFile,TEXT("<dict>"));
// if(false == SaveNodeToXML(hFile))
// {
// fclose(hFile);
// return false;
// }
//
// fprintf(hFile,TEXT("</dict>"));
// //========================================================
// fprintf(hFile,TEXT("</dict>"));
// fprintf(hFile,TEXT("</plist>\n"));
// fclose(hFile);
return true;
}
//保存結點樹到XML
bool CNode::SaveNodeToXML(FILE* hFile)
{
//========================================================
//fprintf(hFile,TEXT("<key>NodeName</key>"));
//fprintf(hFile,TEXT("<string>%s</string>"),m_strNodeName.c_str());
// fprintf(hFile,TEXT("<key>%s</key>"),m_strNodeName.c_str());
// //========================================================
// fprintf(hFile,TEXT("<dict>"));
// if(m_pFirstChild)
// {
// if(false == m_pFirstChild->SaveNodeToXML(hFile))
// {
// fclose(hFile);
// return false;
// }
// }
// fprintf(hFile,TEXT("</dict>"));
//
// if(m_pFirstBorther)
// {
// if(false == m_pFirstBorther->SaveNodeToXML(hFile))
// {
// fclose(hFile);
// return false;
// }
// }
return true;
}
二.Cocos2d-x中的精靈,層,場景與Node
在Cocos2d-x中,結點的基類是Node,它的實現遠遠超越了上面結點代碼的複雜度,不過沒關係,隨着後面相關代碼接觸的加深,你可以很明白它的全部接口函義,但現在,你所需要的只是明白它就不過是個結點,它不過是咱們上面結點類的演變,說的通俗點:不要以爲你穿個馬甲哥就認不出你了!
在Node中,有一個指針容器成員m_pChildren,它存放了當前結點下的所有子結點,我們通過addChild來增加子結點到其中。我們並沒有發現所謂的兄弟結點,爲什麼呢?那時因爲兄弟結點被“扁平化”處理了。爲了提升效率,減少遞歸調用的次數,可以將所有子結點的指針都存放在當前結點的容器中,所以子結點的兄弟結點就不必出現了。
有了結點Node,我們來看一下精靈Sprite,它在2d文件夾下的的sprite_nodes分類下。
打開CCSprite.h:
很明顯,精靈是由結點Node派生出來的子類。它的主要功能就是顯示圖形。在其函數中,涉及紋理加載和OpenGLes相關的頂點和顏色,紋理尋址的操作。
層Layer和場景Scene是被存放在2d文件夾的layers_scenes_transitions_nodes分類下。
打開CCLayer.h:
可以看到,Layer由結點Node派生,也就是說Layer也是一個Node的子類。
還是在同級目錄打開CCScene.h:
好吧,真是簡單明瞭,場景就是專門管理子結點的,或者說就是專門管理層結點的。
現在我們來看一些它們的具體應用。
打開HelloCpp工程。在Classes下我們看到有兩個類:
1 . AppDelegate:由Application派生,即Cocos2d-x的入口類。可以把它當作上面圖示中的”Root”。它的作用就是啓動一個程序,創建主窗口並初始化遊戲引擎並進入消息循環。
2 . HelloWorld:由Layer派生,即Cocos2d-x的層。對應上面圖示中“開始界面”場景中的“界面層”。它的作用是顯示背景圖和菜單及退出按鈕等精靈。在這個類裏有一個靜態函數HelloWorld::scene()創建了所用到的場景並創建HelloWorld這個層放入到場景中。
在程序的main函數中創建了AppDelegate類的實例對象並調用run運行。 之後會在AppDelegate的函數applicationDidFinishLaunching(代表程序啓動時的處理)中結尾處調用HelloWorld::scene()創建了場景。
遊戲運行起來是個什麼樣子呢?
三.Node的渲染流程
這裏我們來簡單講解一下渲染流程,只講解渲染流程,具體最後是怎麼繪製那就需要講解OpenGL的繪製原理了,也就說具體怎麼繪製就脫離了我們課程的規劃了,所以這裏只講解渲染流程,如果你很想學習OpenGL的渲染原理可以諮詢我們的全日制課程,在全日制課程中所有的內容都有哦。
首先找到cocos2d工程中的CCApplition-mac.h文件,找到run()方法,進入該方法的實現,可以看到該函數實現的代碼如下:
其他的代碼我們這裏先不用多想,直接進入紅色矩形標註的代碼,進入mainLoop()的實現,下面我們通過一個UML流程圖圖來看一下渲染流程的函數調用流程,UML流程圖如下:
通過該流程圖我們可以清晰的看出該Cocos2d-x渲染的過程函數的調用,最後通過調用各自渲染類型的相應函數函數來完成渲染。在本節課的視頻教程中我們會帶領大家好好看看渲染流程的代碼,並會做出詳細解釋,讓大家更加明白渲染的流程機制。
四.作業:
1,跟着我們Node的代碼,實現自己的一個Node。
2,跟着渲染流程圖查看相關源碼,認識Cocos2d-x的渲染流程