本节的学习目标:
(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 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 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,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,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,HBITMAP hBitmap) { HDC hTempHDC = CreateCompatibleDC(m_HDC); HGdioBJ hOldobj = SelectObject(hTempHDC,hBitmap); BitBlt(m_HDC,x,y,width,height,hTempHDC,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:
层已经可以显示了,通过取得设备并调用FillRect来显示一个色块矩形。最后我们来看一下精灵:
Sprite.h:
我们为精灵增加了位图句柄,以使它可以绘制相应的位图。
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,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 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)); 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,LR_LOADFROMFILE|LR_CREATEDIBSECTION); CSprite* pNewSprite = new CSprite("knm"); pNewSprite->SetBitmap(hbmp); pNewSprite->SetPos(130,0); //将精灵放入到层 pNewLayer->AddChild(pNewSprite); wsprintf(szImagePathName,"%s\\logo.bmp",szCurrDir); hbmp = (HBITMAP)LoadImage(NULL,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",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",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",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",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",LR_LOADFROMFILE|LR_CREATEDIBSECTION); CSprite* pNewSprite7 = new CSprite("menu_cursor"); pNewSprite7->SetBitmap(hbmp); pNewSprite7->SetPos(60,0); //将精灵放入到层 pNewLayer->AddChild(pNewSprite7); wsprintf(szImagePathName,"%s\\copyright.bmp",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,0); } } } if(wParam == VK_DOWN) {//按下时选人菜单光标置在第二项前面。 if(g_pMyScene) { CSprite* pNewSprite7 = (CSprite*)g_pMyScene->QueryChild("menu_cursor"); if(pNewSprite7) { pNewSprite7->SetPos(60,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()创建了场景。
游戏运行起来是个什么样子呢?没错,我看跟魂斗罗的“开始界面”也差不到哪去嘛。当然,只是指组织关系。
嗯,到此,本节的知识算是讲述完毕!做为一个有上进心的程序员,咱们来做些课后题吧?
2.3 课后题目
1.在2.1.1结中为结点增加缩放,旋转(当然,如果没有好的数学知识就算了,也可以用一些第三方数学库)的处理,使取得一个子结点在世界坐标系中的大小时会受到其父结点的影响。
2. 将《魂斗罗》之外你玩过的游戏选几个做一个树型分析。
原文地址:http://blog.csdn.net/honghaier/article/details/8760124