一.什么是结点
在介绍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; }
@H_301_0@二.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的渲染流程