礼物:《红孩儿引擎内功心法修练与Cocos2d-x》之结点系统(场景,层,精灵)
[Cocos2d-x相关教程来源于红孩儿的游戏编程之路CSDN博客地址:http://blog.csdn.net/honghaier
礼物:《红孩儿引擎内功心法修练与Cocos2d-x》之结点系统(场景,层,精灵)
本节的学习目标:
(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();//设置HDCvoid 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()创建了场景。
游戏运行起来是个什么样子呢?没错,我看跟魂斗罗的“开始界面”也差不到哪去嘛。当然,只是指组织关系。
嗯,到此,本节的知识算是讲述完毕!做为一个有上进心的程序员,咱们来做些课后题吧?
2.3 课后题目
1.在2.1.1结中为结点增加缩放,旋转(当然,如果没有好的数学知识就算了,也可以用一些第三方数学库)的处理,使取得一个子结点在世界坐标系中的大小时会受到其父结点的影响。
2. 将《魂斗罗》之外你玩过的游戏选几个做一个树型分析。
礼物:《红孩儿引擎内功心法修练与Cocos2d-x》之结点系统(场景,层,精灵)相关推荐
- python 游戏引擎 cocos2d_2.2 完成一个Cocos2d游戏程序代码
本视频基于**Python 3.6版本 Python语言之所以受欢迎,很大的原因是有很多可以使用的库,Python社区也有很多游戏开发库,其中较为优秀有:Cocos2d.Pyglet和Pygame,还 ...
- 千里眼的修练方法--末法时代即将结束
现今社会,各公司领导层涌现出了一大批修士,为了帮助大家更好的修行,我来给大家简单介绍一下各种修行方法.各位一定要勤加练习,早日飞升. 1,千里眼 相传古时候有一位叫甘蝇的射箭神手,他有一个弟子叫飞卫, ...
- 职场十大自我修练工具
要想带领你的团队打胜仗,要想在激烈人才的竞争中胜出,你必须有一身好武功.你是在管理上也好,在专业领域上也好,或者在整合资源上也好,你修成好武功就必须掌握自我修练的工具. 自我修练的工具太多了,我们选出 ...
- docker修练之windows与linux下实践记录
docker修练之windows与linux下实践记录 mysql 持久化 mkdir -p /var/own/mysqldata/ docker run --name mmsql -v /var/o ...
- 程序员修练之道-笔记
这是我阅读程序员修练之道时做的一些笔记,那些是我在阅读的过程中,有著不得不把它写下来的冲动,这本书我还在阅读中,它总共有8个章节,我希望可以一天完整一个章节,最迟两星期之内需要把它读完. 2016年1 ...
- 深圳嵌入式培训 修练软硬件之功 高薪一路相伴
修练软硬件之功 高薪一路相伴 ...
- 虚幻引擎学习之路:渲染模块之光照系统
原文链接:https://blog.uwa4d.com/archives/Study_unreal4_Rendering_1.html 写在前面 "UWA什么时候可以支持Unreal引擎?& ...
- 【iOS-cocos2d游戏引擎开发之一】搭建cocos2d游戏引擎环境,创建第一个HelloWorld!...
最近几天仔细了解了iOS游戏开发引擎,常用的cocos2d,Unity引擎,那么Unity是非免费的,而cocos2d则是免费开源的: 最后促使我选择cocos2d的原因有两点: 1.最重要的原因是它 ...
- 重构修练笔记 Refactoring Xiu Lian Notes (1) - 练气期
笔记纯属虚构,如有雷同,纯属巧合 万物皆有定数,普通人入门练气期一味求快,只满足身体自身需求,对于后面更高层级没有远见,导致后面越修炼越难,极其不便和难受,从而有人直接崩盘而道陨神消.需要一种新的方法 ...
- 修练8年C++面向对象程序设计之体会
面向对象程序设计语言很多,如Smalltalk.Ada.Eiffel.Object Pascal.Visual Basic.C++等等.C++语言最讨人喜欢,因为它兼容C 语言,并且具备C 语言的性能 ...
最新文章
- 9个让PyTorch模型训练提速的技巧!
- IBM首家发布了公有云中的裸机Kubernetes
- framework之Activity启动流程(基于Android11源码)
- 解决git本地仓库与远程仓库关联出现 failed to push some refs to git的问题
- jquery正则表达式验证:手机号码
- Dockerfile构建MySQL
- 大学学好高数的爆炸性意义!
- 搭建个人博客,还有比这更快的?
- python基础知识-Python学习--最完整的基础知识大全
- PAT (Basic Level) Practice1019 数字黑洞
- ubuntu执行configure配置代码出现unable to guess system type报错
- SQL SERVER 和EXCEL的数据导入导出
- Android内存泄漏分析心得
- textarea字数实时统计方案
- 手把手教你申请计算机软件著作权(1)——填写软著申请表
- 记一道面试算法题: 某学校需要一个能给学领前儿童自动出三个数加减法的算术题的程序(此处省略一万个字)
- HDU 5442 (串的最大表示+KMP)
- 在配置SSH免密登录时报错:/usr/bin/ssh-copy-id: ERROR: failed to open ID file '/root/.pub': 没有那个文件或目录
- python :脚本运行出现语法错误:IndentationError:unexpected indent(缩进问题)
- 【WebRTC】回声抵消(aec、aecm)算法简介
热门文章
- 【通信原理】第四章 -- 信道
- excel快速删除奇偶数行
- linux7怎么关闭更新,如何让centos7关闭yum自动更新系统
- 室内定位导航-室内地图导航-高精度地图应用场景
- hbuilderx为什么打不开_windows系统,HBuilderX无法启动、点击无反应、或启动报错的解决方案...
- php codeigniter3,codeigniter
- 第二期-Linux内核发展史(1)
- 计算机中rom和ram分别指什么,RAM和ROM分别是什么意思
- 3.Orangepi PC2 使用busybox制作文件系统
- 母亲节任务最后一个料理的源码