本節的學習目標:
(1) 瞭解結點系統,學會自行構建結點系統。
(2) 了結場景,層,精靈的組織關係與各自功能
2.1 結點系統原理入門
2.1.1 結點啓蒙:
在介紹Cocos2d-x的結點系統之前,我們需要首先做一些啓蒙,什麼是樹?
定義:
一棵樹(tree)是由n(n>0)個元素組成的有限集合,其中:
(1)每個元素稱爲結點(node);
(2)有一個特定的結點,稱爲根結點或根(root);
(3)除根結點外,其餘結點被分成m(m>=0)個互不相交的有限集合,而每個子集又都是一棵樹(稱爲原樹的子樹)
如圖A:
對於樹結構有幾個概念要記一下:
度:樹的度——也即是寬度,簡單地說,就是結點的分支數。以組成該樹各結點中最大的度作爲該樹的度,如上圖的樹,其度爲3;樹中度爲零的結點稱爲葉結點或終端結點。樹中度不爲零的結點稱爲分枝結點或非終端結點。除根結點外的分枝結點統稱爲內部結點。
深度:樹的深度——組成該樹各結點的最大層次,如上圖,其深度爲3;
層次:根結點的層次爲1,其他結點的層次等於它的父結點的層次數加1.
請仔細觀察上圖這棵樹,這裏A是根結點,其餘結點均是屬於A的不同層級的子結點。我們由此圖進一步進行想像,人的身體其實也是一棵樹。
如圖B:
上圖詳細表現了人體樹結構的組織結構,左邊是人形的結構,右邊是層級關係展開。它作爲骨骼動畫的基礎理論被廣泛的應用於各類遊戲動畫中。
我們看一下這張圖,首先有一個根骨(脊椎),這個根骨即是樹結構中的根結點。根骨下有三根子骨骼(左胯,右胯,頸背),這三根子骨骼也各自有屬於自已的子骨骼樹結構,同時它們由父骨骼牽引並牽引着子骨骼,與父骨骼和第一層子骨骼保持着固定的距離。
試想一下:
當我們想把這個人移動到一個位置點時,只需要把根骨移動到相應位置,即這個人的所有骨骼都會被這種牽引關係移動到這個世界位置的相對骨骼位置。但如果我們把左胯這根骨骼去掉的話,則在移動根骨後,左胯仍停留在原地,它已經不再屬於當前骨骼樹了,而成了一棵獨立的骨骼樹。
看完這張圖,已經比較接近我們所要講述的內容了,對於骨骼結構的理解將有助於我們掌握遠大於骨骼動畫本身的結構模式,因爲由此理論基礎我們將學會一切基於結點樹結構的系統。
下面我們來用C++的代碼構建這樣一套系統。
首先,我們創建一個基類,稱之爲結點。
- //結點類
- 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;
- }
- ;
對應的實現:
- #include "Node.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;
- }
這樣,一個最基本的結點就建立起來了,我們將可以由它來建立一棵樹,比如下圖這樣一個程序:我們有一個TreeCtrl。初始情況下只有一個Root結點,通過在樹控件上右鍵彈出菜單中進行選項操作來增加或刪除子結點和兄弟結點。當我們創建了一個結點樹後可以調用SaveNodeToXML函數來講結點樹保存下來。
保存的XML文件打開後:
、學到這裏,您已經掌握了一個結點系統的基本設計思想,它將在日後成爲一個強大的武器來幫助您在遊戲開發過程中解決一些相關的設計問題。
2.1.1結點的位置:
上面的結點系統代碼中,只有結點的父子關係,並不能實現父結點移動同時帶動子結點移動。這又是怎麼做到的呢?
這裏有一個關鍵的核心算法:即一個結點的位置,由本結點相對於父結點位置加上父結點的世界位置來取得,而父結點又會通過父結點與其父結點(即爺爺結點)的相對位置加上其父結點(即爺爺結點)的世界位置來取得。這裏有一個層層回溯的思想在裏面。
我們在代碼中加入一個表示空間位置的結構。
- //向量
- struct stVec3
- {
- //三向值
- float m_fX;
- float m_fY;
- float m_fZ;
- stVec3()
- {
- m_fX = 0;
- m_fY = 0;
- m_fZ = 0;
- }
- //構造
- stVec3(float x,float y,float z)
- {
- m_fX = x;
- m_fY = y;
- m_fZ = z;
- }
- //重載賦值操作符
- stVec3& stVec3::operator = (const stVec3& tVec3)
- {
- m_fX = tVec3.m_fX;
- m_fY = tVec3.m_fY;
- m_fZ = tVec3.m_fZ;
- return *this;
- }
- //重載加法操作符
- stVec3 stVec3::operator + (const stVec3& tVec3)
- {
- stVec3 tResultVec;
- tResultVec.m_fX = m_fX + tVec3.m_fX;
- tResultVec.m_fY = m_fY + tVec3.m_fY;
- tResultVec.m_fZ = m_fZ + tVec3.m_fZ;
- return tResultVec;
- }
- //重載加等操作符
- stVec3& stVec3::operator += (const stVec3& tVec3)
- {
- m_fX += tVec3.m_fX;
- m_fY += tVec3.m_fY;
- m_fZ += tVec3.m_fZ;
- return *this;
- }
- //重載減法操作符
- stVec3 stVec3::operator - (const stVec3& tVec3)
- {
- stVec3 tResultVec;
- tResultVec.m_fX = m_fX - tVec3.m_fX;
- tResultVec.m_fY = m_fY - tVec3.m_fY;
- tResultVec.m_fZ = m_fZ - tVec3.m_fZ;
- return tResultVec;
- }
- //重載減等操作符
- stVec3& stVec3::operator -= (const stVec3& tVec3)
- {
- m_fX -= tVec3.m_fX;
- m_fY -= tVec3.m_fY;
- m_fZ -= tVec3.m_fZ;
- return *this;
- }
- }
- ;
- 然後在結點中加入相應接口:
- public:
- //設置當前結點相對於父結點位置
- void SetPos(float x,float y,float z);
- void SetPos_X(float x);
- void SetPos_Y(float y);
- void SetPos_Z(float z);
- //取得當前結點相對於父結點位置
- float GetPos_X();
- float GetPos_Y();
- float GetPos_Z();
- //取得當前結點的世界座標位置
- stVec3 GetWorldPos();
- private:
- //當前結點相對於父結點的位置
- stVec3 m_sPos;
- 對應的實現:
- //設置當前結點相對於父結點位置
- void CNode::SetPos(float x,float y,float z)
- {
- m_sPos.m_fX = x;
- m_sPos.m_fY = y;
- m_sPos.m_fZ = z;
- }
- void CNode::SetPos_X(float x)
- {
- m_sPos.m_fX = x;
- }
- void CNode::SetPos_Y(float y)
- {
- m_sPos.m_fY = y;
- }
- void CNode::SetPos_Z(float z)
- {
- m_sPos.m_fZ = z;
- }
- //取得當前結點相對於父結點位置
- float CNode::GetPos_X()
- {
- return m_sPos.m_fX;
- }
- float CNode::GetPos_Y()
- {
- return m_sPos.m_fY;
- }
- float CNode::GetPos_Z()
- {
- return m_sPos.m_fZ;
- }
- //取得當前結點的世界座標位置
- stVec3 CNode::GetWorldPos()
- {
- stVec3 tResultPos = m_sPos;
- //使用回溯法取得最終的世界位置,這一步是結點系統中父結點固定子結點的關健。
- if(m_pParentNode)
- {
- tResultPos += m_pParentNode->GetWorldPos();
- }
- return tResultPos;
- }
經過這些代碼的建立,我們就可以取得一個受父結點位置固定的子結點的世界位置了。同樣,縮放和旋轉的關係也可以由此建立,在此就不一一贅述了,有興趣的同學可以在本節作用中完成它。
2.2 精靈,層,場景
2.2.1魂鬥羅的場景:
在Cocos2d-x中,大量的物體都是基於結點系統的,這些類均是由最基本的結點類CCNode來派生的。其中最爲重要的是精靈-CCSprite,層-CCLayer,場景- CCScene。
一個遊戲的一個關卡,可以想象爲一棵樹,其中場景是樹幹,層是樹枝,精靈是樹葉。一棵樹只有一個樹幹,樹幹上有多個樹枝,樹枝上又有多個樹葉。從功能性上來講,樹幹的作用是管理樹枝,樹枝的作用是固定其上長出的樹葉,樹葉的作用是吸收陽光…NO,不是這個意思,樹葉的作用是表現力,是觀賞,是用來看的。表現在Cocos2d-x的遊戲中,場景用來管理所有的層,而層則是用來區分具有不同排序分類的精靈集合,並能響應觸屏事件,而精靈就是顯示圖片和動畫的。當遊戲要切換場景時,就像是換一棵樹。作爲一個遊戲設計師,要能夠很好的設計遊戲的這些樹。當然,我們要很清楚的知道如何來種下一棵樹,這很重要!
首先,我們先要確定遊戲都需要哪些場景。作爲樹的根結點,它構成了遊戲的骨架。比如我們小時候玩的FC遊戲-《魂鬥羅》。
我們可以將開始界面和後面每一個關卡都當作是一個場景。那簡單來說這個遊戲是由兩類場景構成的。第一類場景就是開始界面,如下圖:
這個開始界面做爲一個場景是簡單了點,但很清晰。遊戲開始時首先要運行的場景就是它。我們以三級樹形結點來表示這個場景。
在這個三級樹形結點圖示中,“開始界面”即爲場景,“界面層”即爲層,再下面的四個結點可以算爲界面層下的精靈,當然,菜單其實也可以分爲幾個精靈構成。
第二類場景就是關卡了。如圖:
這是熟悉的第一關,我們仍以三級樹形結點來表示這個場景。
在這裏,“第一關”即爲場景,爲了區分具有不同排序分類的精靈集合。我將遊戲中的層由遠及近觀看,由先及後繪製,劃分爲“遠景層”,“近景層”,“人物層”,“效果層”,“界面層”等五個層,再將各種精靈分佈到這些層中。
繼續這樣子分析,我們可以得出所有的關卡樹:
在這裏“Root”代表了遊戲程序。它共種有十棵樹。分別爲“開始界面”,“第一關”…“通關界面”,每完成一個關卡,就將進行場景的切換,也就是顯示一棵新樹。
到這裏,精靈,層與場景的結點關係原理已經講解完成。我們現在來看一下Cocos2d-x中是如何具體實現和應用的。
以開始界面爲例,咱們接着上一節中所講的節點類來進行擴展,爲了更好的講述理論,這部分內容完全不涉及任何渲染引擎的使用,我們只使用VS創建一個簡單的WIN32窗口程序,並使用GDI來進行繪製。
我們將創建的工程命名爲ShowNodeTree,編譯運行只有一個空白窗口,它工作的很好。OK,現在我們創建一個工程篩選目錄NodoTree,並將之前創建的Node放入其中,並依次創建好Scene,Layer,Spriet及Director等類。
顧名思義,上面這些文件分別爲:
Director.h/cpp:win32繪製管理類CDirector,繪圖用。
Node.h/cpp:結點基類CNode,用於構建結點樹。
Layer.h/cpp: 層類CLayer。
Scene.h/cpp:場景類CScene。
Sprite.h/cpp:精靈類CSprite。
我們來看一下具體實現:
首先是win32繪製管理類CDirector:
Director.h:
- #pragma once
- #include <windows.h>
- //==================================================================
- //File:Director.h
- //Desc:顯示設備類,用於繪製
- //==================================================================
- class CDirector
- {
- public:
- ~CDirector();
- public:
- //獲取單件實例指針
- static CDirector* GetInstance();
- //設置HDC
- void Init(HWND hWnd);
- //繪製矩形
- void FillRect(int x,int y,int width,int height,COLORREF rgb);
- //繪製圖像
- void DrawBitMap(int x,int y,int width,int height,HBITMAP hBitmap);
- private:
- CDirector(){}
- private:
- //單件實例指針
- static CDirector* m_pThisInst;
- //WINDOWS 窗口句柄
- HWND m_HWnd;
- //WINDOWS GDI 繪圖所用的設備上下文
- HDC m_HDC;
- }
- ;
可以看到,CDirector類是一個單例,我們爲其創建了兩個函數來進行繪製指定色的矩形和繪製位圖的功能。沒錯,足夠用了。
Director.cpp:
- #include "Director.h"
- CDirector* CDirector::m_pThisInst = NULL;
- CDirector* CDirector::GetInstance()
- {
- if(!m_pThisInst)
- {
- m_pThisInst = new CDirector ;
- if(!m_pThisInst)return NULL;
- m_pThisInst->Init(NULL);
- }
- return m_pThisInst;
- }
- CDirector::~CDirector()
- {
- if(m_pThisInst)
- {
- delete m_pThisInst;
- m_pThisInst = NULL;
- }
- }
- void CDirector::Init(HWND hWnd)
- {
- if(hWnd)
- {
- m_HWnd = hWnd ;
- m_HDC = ::GetDC(m_HWnd) ;
- }
- }
- void CDirector::FillRect(int x,int y,int width,int height,COLORREF rgb)
- {
- HBRUSH hBrush = ::CreateSolidBrush(rgb);
- RECT tRect;
- tRect.left = x;
- tRect.top = y;
- tRect.right = x + width;
- tRect.bottom = y + height;
- ::FillRect(m_HDC,&tRect,hBrush);
- ::DeleteObject(hBrush);
- }
- void CDirector::DrawBitMap(int x,int y,int width,int height,HBITMAP hBitmap)
- {
- HDC hTempHDC = CreateCompatibleDC(m_HDC);
- HGDIOBJ hOldObj = SelectObject(hTempHDC,hBitmap);
- BitBlt(m_HDC,x,y,width, height,hTempHDC,0,0,SRCCOPY);
- DeleteDC(hTempHDC);
- }
都是最基本的GDI繪製操作,這樣我們的設備就建立好了。下面我們來創建場景。
Scene.h:
- #pragma once
- #include "Node.h"
- //==================================================================
- //File:Scene.h
- //Desc:場景類,用於管理所有的層
- //==================================================================
- //結點類
- class CScene : public CNode
- {
- public:
- //構造
- CScene(const char* szName);
- };
其對應的CPP:
- #include "Scene.h"
- //構造
- CScene::CScene(const char* szName)
- {
- SetName(szName);
- }
沒什麼可解釋的,就是一個結點類。然後是層:
Layer.h:
- #pragma once
- #include "Node.h"
- //==================================================================
- //File:Layer.h
- //Desc:層類,用於存放精靈
- //==================================================================
- //結點類
- class CLayer : public CNode
- {
- public:
- //構造
- CLayer(const char* szName);
- public:
- //更新
- virtual inline void Update();
- //直接渲染
- virtual inline void Draw();
- public:
- //設置顏色
- void SetColor(COLORREF color);
- //取得顏色
- COLORREF GetColor();
- //設置高
- void SetWidth(int vWidth);
- //取得寬
- int GetWidth();
- //設置高
- void SetHeight(int vHeight);
- //取得高
- int GetHeight();
- private:
- //設置顏色
- COLORREF m_LayerColor;
- //寬度
- int m_nWidth;
- //高度
- int m_nHeight;
- };
可以看到,層有了寬高和顏色的設置,對應的Layer.cpp:
- #include "Layer.h"
- #include "Director.h"
- //構造
- CLayer::CLayer(const char* szName):
- m_nWidth(0),
- m_nHeight(0)
- {
- SetName(szName);
- m_LayerColor = RGB(255,255,255);
- }
- //更新
- void CLayer::Update()
- {
- CNode::Update();
- }
- //直接渲染
- void CLayer::Draw()
- {
- stVec3 tPos = GetWorldPos();
- CDirector::GetInstance()->FillRect(tPos.m_fX,tPos.m_fY,m_nWidth,m_nHeight,m_LayerColor);
- CNode::Draw();
- }
- //設置顏色
- void CLayer::SetColor(COLORREF color)
- {
- m_LayerColor = color;
- }
- //取得顏色
- COLORREF CLayer::GetColor()
- {
- return m_LayerColor ;
- }
- //設置寬
- void CLayer::SetWidth(int vWidth)
- {
- m_nWidth = vWidth;
- }
- //取得寬
- int CLayer::GetWidth()
- {
- return m_nWidth ;
- }
- //設置高
- void CLayer::SetHeight(int vHeight)
- {
- m_nHeight = vHeight;
- }
- //取得高
- int CLayer::GetHeight()
- {
- return m_nHeight ;
- }
層已經可以顯示了,通過取得設備並調用FillRect來顯示一個色塊矩形。最後我們來看一下精靈:
Sprite.h:
- #pragma once
- #include "Node.h"
- //==================================================================
- //File:Sprite.h
- //Desc:精靈類,用於顯示圖片
- //==================================================================
- //結點類
- class CSprite : public CNode
- {
- public:
- //構造
- CSprite(const char* szName);
- public:
- //更新
- virtual inline void Update();
- //直接渲染
- virtual inline void Draw();
- public:
- //設置位圖句柄
- void SetBitmap(HBITMAP vhBitmap);
- //設置位圖句柄
- void SetBitmap(HBITMAP vhBitmap,int vWidth,int vHeight);
- private:
- //所用位圖句柄
- HBITMAP m_hBitmap;
- //位圖寬度
- int m_nBitmapWidth;
- //位圖高度
- int m_nBitmapHeight;
- };
我們爲精靈增加了位圖句柄,以使它可以繪製相應的位圖。
Sprite.cpp:
- #include "Sprite.h"
- #include "Director.h"
- //構造
- CSprite::CSprite(const char* szName):
- m_hBitmap(NULL),
- m_nBitmapWidth(0),
- m_nBitmapHeight(0)
- {
- SetName(szName);
- }
- //更新
- void CSprite::Update()
- {
- CNode::Update();
- }
- //直接渲染
- void CSprite::Draw()
- {
- if(m_hBitmap)
- {
- stVec3 tPos = GetWorldPos();
- CDirector::GetInstance()->DrawBitMap(tPos.m_fX,tPos.m_fY,m_nBitmapWidth,m_nBitmapHeight,m_hBitmap);
- }
- CNode::Draw();
- }
- //設置位圖句柄
- void CSprite::SetBitmap(HBITMAP vhBitmap)
- {
- BITMAP bmp ;
- GetObject(vhBitmap, sizeof(BITMAP), &bmp); //得到一個位圖對象
- m_hBitmap = vhBitmap ;
- m_nBitmapWidth = bmp.bmWidth ;
- m_nBitmapHeight = bmp.bmHeight ;
- }
- //設置位圖句柄
- void CSprite::SetBitmap(HBITMAP vhBitmap,int vWidth,int vHeight)
- {
- m_hBitmap = vhBitmap ;
- m_nBitmapWidth = vWidth ;
- m_nBitmapHeight = vHeight;
- }
OK,就這樣,我們就建立了一套可以進行場景,層,精靈管理和繪製的類。現在我們來具體的實現一下開始界面。我將開始界面分爲
這裏共有一個層和八個精靈。層嘛,就是一純黑背景色塊,八個精靈嘛,就如上圖所示分別用來顯示不同的位圖:
我們現在打開程序的主源文件ShowNodeTree.cpp,在文件頂部加入:
- #include "Sprite.h"
- #include "Layer.h"
- #include "Scene.h"
- #include "Director.h"
- //唯一使用的場景
- CScene* g_pMyScene = NULL;
並在InitInstance函數的尾部加入:
- //初始化設備
- CDirector::GetInstance()->Init(hWnd);
- //增加層
- CLayer* pNewLayer = new CLayer("Layer1");
- pNewLayer->SetPos(100,40,0);
- pNewLayer->SetColor(RGB(0,0,0));
- pNewLayer->SetWidth(497);
- pNewLayer->SetHeight(434);
- //增加精靈
- char szCurrDir[_MAX_PATH];
- ::GetCurrentDirectory(_MAX_PATH,szCurrDir);
- char szImagePathName[_MAX_PATH];
- wsprintf(szImagePathName,"%s\\knm.bmp",szCurrDir);
- HBITMAP hbmp = (HBITMAP)LoadImage(NULL,szImagePathName,IMAGE_BITMAP,0,0,LR_LOADFROMFILE|LR_CREATEDIBSECTION);
- CSprite* pNewSprite = new CSprite("knm");
- pNewSprite->SetBitmap(hbmp);
- pNewSprite->SetPos(130,40,0);
- //將精靈放入到層
- pNewLayer->AddChild(pNewSprite);
- wsprintf(szImagePathName,"%s\\logo.bmp",szCurrDir);
- hbmp = (HBITMAP)LoadImage(NULL,szImagePathName,IMAGE_BITMAP,0,0,LR_LOADFROMFILE|LR_CREATEDIBSECTION);
- CSprite* pNewSprite2 = new CSprite("logo");
- pNewSprite2->SetBitmap(hbmp);
- pNewSprite2->SetPos(90,100,0);
- //將精靈放入到層
- pNewLayer->AddChild(pNewSprite2);
- wsprintf(szImagePathName,"%s\\player.bmp",szCurrDir);
- hbmp = (HBITMAP)LoadImage(NULL,szImagePathName,IMAGE_BITMAP,0,0,LR_LOADFROMFILE|LR_CREATEDIBSECTION);
- CSprite* pNewSprite3 = new CSprite("player");
- pNewSprite3->SetBitmap(hbmp);
- pNewSprite3->SetPos(260,230,0);
- //將精靈放入到層
- pNewLayer->AddChild(pNewSprite3);
- wsprintf(szImagePathName,"%s\\menu_title.bmp",szCurrDir);
- hbmp = (HBITMAP)LoadImage(NULL,szImagePathName,IMAGE_BITMAP,0,0,LR_LOADFROMFILE|LR_CREATEDIBSECTION);
- CSprite* pNewSprite4 = new CSprite("menu_title");
- pNewSprite4->SetBitmap(hbmp);
- pNewSprite4->SetPos(40,270,0);
- //將精靈放入到層
- pNewLayer->AddChild(pNewSprite4);
- wsprintf(szImagePathName,"%s\\menu_1.bmp",szCurrDir);
- hbmp = (HBITMAP)LoadImage(NULL,szImagePathName,IMAGE_BITMAP,0,0,LR_LOADFROMFILE|LR_CREATEDIBSECTION);
- CSprite* pNewSprite5 = new CSprite("menu_1");
- pNewSprite5->SetBitmap(hbmp);
- pNewSprite5->SetPos(100,310,0);
- //將精靈放入到層
- pNewLayer->AddChild(pNewSprite5);
- wsprintf(szImagePathName,"%s\\menu_2.bmp",szCurrDir);
- hbmp = (HBITMAP)LoadImage(NULL,szImagePathName,IMAGE_BITMAP,0,0,LR_LOADFROMFILE|LR_CREATEDIBSECTION);
- CSprite* pNewSprite6 = new CSprite("menu_2");
- pNewSprite6->SetBitmap(hbmp);
- pNewSprite6->SetPos(100,350,0);
- //將精靈放入到層
- pNewLayer->AddChild(pNewSprite6);
- wsprintf(szImagePathName,"%s\\menu_cursor.bmp",szCurrDir);
- hbmp = (HBITMAP)LoadImage(NULL,szImagePathName,IMAGE_BITMAP,0,0,LR_LOADFROMFILE|LR_CREATEDIBSECTION);
- CSprite* pNewSprite7 = new CSprite("menu_cursor");
- pNewSprite7->SetBitmap(hbmp);
- pNewSprite7->SetPos(60,310,0);
- //將精靈放入到層
- pNewLayer->AddChild(pNewSprite7);
- wsprintf(szImagePathName,"%s\\copyright.bmp",szCurrDir);
- hbmp = (HBITMAP)LoadImage(NULL,szImagePathName,IMAGE_BITMAP,0,0,LR_LOADFROMFILE|LR_CREATEDIBSECTION);
- CSprite* pNewSprite8 = new CSprite("copyright");
- pNewSprite8->SetBitmap(hbmp);
- pNewSprite8->SetPos(120,390,0);
- //將精靈放入到層
- pNewLayer->AddChild(pNewSprite8);
- //將層放入場景
- g_pMyScene = new CScene("HDL");
- g_pMyScene->AddChild(pNewLayer);
- //設定每毫秒刷新一幀
- ::SetTimer(hWnd,1,20,NULL);
看,經過上面的代碼之後,我們就創建了相應的層,精靈和場景。最後創建一個定時器來進行屏幕重繪,FPS嘛,就設爲50好了。
我們在窗口消息處理函數中加入:
- case WM_PAINT:
- {
- hdc = BeginPaint(hWnd, &ps);
- // TODO: 在此添加任意繪圖代碼..
- if(g_pMyScene)
- {
- //更新和繪製場景
- g_pMyScene->Update();
- g_pMyScene->Draw();
- }
- EndPaint(hWnd, &ps);
- }
- break;
- case WM_TIMER:
- {
- //讓場景的Layer1層不斷向右移動,到像素時重置。
- if(g_pMyScene)
- {
- CNode* pLayer = g_pMyScene->QueryChild("Layer1");
- stVec3 tPos = pLayer->GetWorldPos();
- tPos.m_fX += 1;
- if(tPos.m_fX > 400)
- {
- tPos.m_fX = 0;
- }
- pLayer->SetPos_X(tPos.m_fX);
- }
- //響應刷新
- ::InvalidateRect(hWnd,NULL,TRUE);
- }
- break;
- case WM_KEYDOWN:
- {
- if(wParam == VK_UP)
- {//按上時選人菜單光標置在第一項前面。
- if(g_pMyScene)
- {
- CSprite* pNewSprite7 = (CSprite*)g_pMyScene->QueryChild("menu_cursor");
- if(pNewSprite7)
- {
- pNewSprite7->SetPos(60,310,0);
- }
- }
- }
- if(wParam == VK_DOWN)
- {//按下時選人菜單光標置在第二項前面。
- if(g_pMyScene)
- {
- CSprite* pNewSprite7 = (CSprite*)g_pMyScene->QueryChild("menu_cursor");
- if(pNewSprite7)
- {
- pNewSprite7->SetPos(60,350,0);
- }
- }
- }
- }
- break;
- case WM_DESTROY:
- //當窗口銷燬時也一併刪除定時器並釋放場景。
- ::KillTimer(hWnd,1);
- if(g_pMyScene)
- {
- //會調用CNode的虛析構函數釋放所有子結點。所以不會造成內存泄漏。
- delete g_pMyScene;
- g_pMyScene = NULL;
- }
- PostQuitMessage(0);
- break;
這樣我們的開始界面就算完成了,編譯運行一下吧:
怎麼樣?不錯吧。一個開始界面層展現在我們面前,所有精靈做爲層的子結點而隨着層保持運動。雖然這種方式還有一些閃屏,但,那並不是重點,關鍵是你徹徹底底的理解了結點系統對於引擎架構的作用和設計思想。好了,喝口水歇一會兒開始進入到Cocos2d-x中去看看。
2.1.2 Cocos2d-x中的精靈,層,場景與結點:
在Cocos2d-x中,結點的基類是CCNode,它的實現遠遠超越了上面結點代碼的複雜度,不過沒關係,隨着後面相關代碼接觸的加深,你可以很明白它的全部接口函義,但現在,你所需要的只是明白它就不過是個結點,它不過是咱們上面結點類的演變,說的通俗點:不要以爲你穿個馬甲哥就認不出你了!
在CCNode中,有一個指針容器成員m_pChildren ,它存放了當前結點下的所有子結點,我們通過addChild來增加子結點到其中。我們並沒有發現所謂的兄弟結點,爲什麼呢?那時因爲兄弟結點被“扁平化”處理了。爲了提升效率,減少遞歸調用的次數,可以將所有子結點的指針都存放在當前結點的容器中,所以子結點的兄弟結點就不必出現了。
有了結點CCNode,我們來看一下精靈CCSprite,它在libcocos2d的sprite_nodes分類下。
打開CCSprite.h:
CCSprite :publicCCNode,public CCTextureProtocol,public CCRGBAProtocol
很明顯,精靈是由結點CCNode派生出來的子類。它的主要功能就是顯示圖形。在其函數中,涉及紋理加載和OpenGL相關的頂點和顏色,紋理尋址的操作。
層CCLayer和場景CCScene是被存放在libcocos2d的layers_scenes_transitions_nodes分類下。
打開CCLayer.h:
CC_DLLCCLayer : public CCNode,public CCTouchDelegate,publicCCAccelerometerDelegate,publicCCKeypadDelegate
可以看到,CCLayer除了由結點CCNode派生外,還增加了用戶輸入事件的響應接口。如CCTouchDelegate是觸屏事件響應接口類,CCAccelerometerDelegate是加速鍵消息事件響應接口類,CCKeypadDelegate是軟鍵盤消息事件響應接口類。
打開CCScene.h:
class CC_DLL CCScene :publicCCNode
好吧,真是簡單明瞭,場景就是專門管理子結點的,或者說就是專門管理層結點的。
現在我們來看一些它們的具體應用。
打開HelloCpp工程。在Classes下我們看到有兩個類:
1 . AppDelegate:由CCApplication派生,即Cocos2d-x的程序類。可以把它當作上面圖示中的”Root”。它的作用就是啓動一個程序,創建主窗口並初始化遊戲引擎並進入消息循環。
2 . HelloWorld:由CCLayer派生,即Cocos2d-x的層。對應上面圖示中“開始界面”場景中的“界面層”。它的作用是顯示背景圖和菜單及退出按鈕等精靈。在這個類裏有一個靜態函數HelloWorld::scene()創建了所用到的場景並創建HelloWorld這個層放入到場景中。
在程序的main函數中創建了AppDelegate類的實例對象並調用run運行。 之後會在AppDelegate的函數applicationDidFinishLaunching(代表程序啓動時的處理)中結尾處調用HelloWorld::scene()創建了場景。
遊戲運行起來是個什麼樣子呢?沒錯,我看跟魂鬥羅的“開始界面”也差不到哪去嘛。當然,只是指組織關係。
嗯,到此,本節的知識算是講述完畢!做爲一個有上進心的程序員,咱們來做些課後題吧?