C++和OpenGL实现3D游戏编程【连载21】——父物体和子物体模式实现
欢迎来到zhooyu的专栏。
🔥C++和OpenGL实现3D游戏编程【专题总览】
1、本节要实现的内容
上节课我们已经创建了一个基础Object类,以后所有的游戏元素都可以从这个基类中派生出来。同时为了操作方便,我们可以为任意两个Object类(及其派生类)的实例对象添加一种父子关系,后期通过父物体与子物体关系,能够轻松的实现游戏物体的操控,方便我们在游戏中对所有游戏元素的组织和管理。再比如后期加父子关系后,就可以同步控制父子物体在三维空间的位置、旋转和缩放操作。
2、父子物体的概念
上节课介绍了Object类,我们知道所有可以由Object类派生出来,这是类之间的继承与被继承的关系(指的是类)。同时,我们还可以将具体类的实例附加一种父子关系概念(指的是具体的实例)。父子关系,就是假设我们有两个Object实例B和实例A,这两个实例对应的类都是Object类,那么我们就可以指定A和B的父子关系,那么我们可以指定A是父物体B是子物体,也可以指定B是父体A是子物体。也就是指定一个物体添加为另一个物体的子物体,此时我们称两个实例建立了父子关系。Object是我们游戏里边绝大部分类的根类。
3、父子物体模式的优点
父物体通过一个链表,可以保存他所有的子物体,但每一个子物体只能有一个父物体,这样就构造出了一个简单的树状结构。我们可以通过链表的方式,物体添加、删除、遍历的它的物体。
3.1、组成一个树状结构管理模式
像许多游戏里头一样,有些场景它是有一个MainSence根节点。其他所有的游戏元素都是连接到这个游戏MainSence的子节点上去,当然也可以是子节点的子节点,以此类推。就是说后期我们如果想给游戏中添加任何元素,就只用给这个这个MainSence实例添加子节点就可以了。我们游戏中Object对象之间可以以对象树的形式组织起来的。子对象就会加入到父对象的一个成员变量叫Child(孩子)的List(列表)中,我们程序中每个Object类都有一个ChildNodeList列表,用于存储当前物体的所有子类实例,同时每个子类实例都有一个Object类型的ptParent指针,用于存储唯一的父物体指针。当然如果当前物体不存在父物体(实例)时,这个父指针也可以为空。当父对象析构的时候,这个列表ChildNodeList中的所有子对象也会被析构。(注意,这里是说父对象和子对象,不要理解成父类和子类)。我们今后所有创建的Object类或继承类的实例也可以也继承了这种对象树关系。一个孩子可以添加成为父组件的一个子组件。
3.2、方便的内存管理模式
我们向某个Object对象实例中添加了Object对象实例(建立父子关系),当我们删除父子关系时,我们的DelChild函数或DelAllChild默认情况下会将子对象实例析构,同时我们子物体在析构过程中,子物体的子物体以及,子子子物体均会一同被析构,确保内存能及时释放,避免内存溢出问题。这个结果也是我们开发人员所期望的。比如,如果有一个窗口和窗口中的一个按钮,我们建立父子关系后,窗口可以添加按钮为子物体。后期当我们用窗口的DelChild函数删除和按钮的父子关系时,其所在的窗口会自动将该按钮从其子对象列表(ChildNodeList)中删除,并析构这个按键释放内存,按钮在屏幕上消失。当这个窗口析构的时候,窗口ChildNodeList列表里边其他所有的子物体也会自动释放内存。引入对象树的概念,在一定程度上解决了内存问题。
3.3、父物体可以控制子物体的移动旋转及缩放
因为我们设置了负物体与子物体的从属关系。我们可以方便的控制他们之间的Transform操作。后期我们添加相应功能后,当父体移动时,子物体也会随之移动。但是反过来子物体移动时副物体不需要移动。这种模式在旋转和缩放中同样适用。这个场景其实比较常用。比如说我们比如说我们去超市推了一个购物车,装上一些商品后,购物车和商品就构成了简单的父子关系。购物车当做父物体,车里的商品当成是子物体。那么我们推着购物车移动时,所有的子物体商品会随着我们的购物车父物体移动或旋转。但是,如果你仅仅摆动购物车里的商品时,购物车是不会随之移动的。日常生活中遍及着各种这样的父子关系模式,比如汽车拉货物,电梯和电梯里乘坐的人的关系,书包和书包中的书的关系。
4、链表的实现
要实现对象树概念,我们就必须有一个链表来实现功能。当然,我们可以直接使用STL中的List容器,功能基本一致。但为了我们后期操作的灵活性和代码清晰度,我们自己准备了一个链表,我们这里实现的是不带头双向不循环列表。
4.1、存储实例的指针
我们这个ChildNodeList链表中存储的是类实例的指针,也就是说这个链表只负责保存父子关系。具体的实物创建需要单独创建,这个对象实物可以是静态定义的,也可以是动态创建的。不管是动态创建还是静态定义的,我们都可以将它的指针通过AddChild函数添加到父物体的子链表中,让他们之间形成父子从属关系。
4.2、使用了双向链表模板
为了操作方便,我们使用了不带头双向不循环列表。但同时还有一个问题,由于我们Object拥有一个子链表,链表中的保存的指针又是Object类型的,因此我们必须使用C++模板template来定义Node和NodeList类,并创建这个ChildNodeList链表,否则会出现前后创建逻辑顺序错误。采用双向链方式表主要是为了查询、操作方便。
4.3、方便后期拓展
这里我们知道STL有现成的链表,但是我们没有使用,给自己写一个链表,主要是为了能够更加自主的去操控链表。大家如果为了方便,也可以使用STL有现成的链表,不过它是一个“带头双向循环链表”。同时我们还在可以在后期扩展链表的使用方法,方便我们的操作。
//用于统计所有创建的物体个数,动态创建节点个数
int st_NodeCreateNum=0;
//用于统计所有创建的物体个数,动态删除节点个数
int st_NodeDeleteNum=0;
//用于统计所有创建的物体个数,显示调试信息
float ShowNodeStatistic(HWND hWnd,HDC hDC,float x,float y,float h=20);
//重置统计数据
void ResetNodeStatistic();
//自定义节点(模板)
template<typename Type>
struct Node
{
public:
//存储链表的下一指针
Node *ptNextNode;
//存储链表的上一指针
Node *ptPrevNode;
//存储链表的当前物体指针
Type *ptInstance;
//初始化结构体
Node()
{
ptNextNode=NULL;
ptPrevNode=NULL;
ptInstance=NULL;
}
};
//自定义双向链表(模板)
template<typename Type>
class NodeList
{
public:
//指针链表首部
Node<Type> *ptNodeHead;
//指针链表尾部
Node<Type> *ptNodeTail;
//记录链表的子节点个数
int iNodeAmount;
public:
//构造函数
NodeList();
//从链表尾部添加一个保存特定实例指针的子节点
Type *AddNode(Type *ptTempInstance);
//从链表尾部删除一个保存特定实例指针的子节点
Type *DelNode();
//从链表中删除特定实例指针的一个子节点
Type *DelNode(Type *ptTempInstance);
//非递归显示显示所有子节点概要信息
void ShowNodeList(HWND hWnd,HDC hDC,float x,float y,float h=20);
};
float ShowNodeStatistic(HWND hWnd,HDC hDC,float x,float y,float h)
{
int line=0;
//显示坐标信息文字
glColor3f(1,0,0);
//显示子物体链表
char szTemp[1024]="";
//显示统计信息
glWindowPos2f(x,y-h*line++);
sprintf(szTemp,"st_NodeCreateNum:%d",st_NodeCreateNum);
drawString(hDC,szTemp);
//显示统计信息
glWindowPos2f(x,y-h*line++);
sprintf(szTemp,"st_NodeDeleteNum:%d",st_NodeDeleteNum);
drawString(hDC,szTemp);
//返回显示后的纵坐标
return y-h*line;
}
//重置统计数据
void ResetNodeStatistic()
{
//用于统计所有创建的物体个数,动态创建节点个数
st_NodeCreateNum=0;
//用于统计所有创建的物体个数,动态删除节点个数
st_NodeDeleteNum=0;
}
//构造函数
template<typename Type>
NodeList<Type>::NodeList()
{
ptNodeHead=NULL;
ptNodeTail=NULL;
iNodeAmount=0;
}
//从链表头部添加一个保存特定实例指针的子节点
template<typename Type>
Type *NodeList<Type>::AddNode(Type *ptTempInstance)
{
//待添加实例指针不能为空
if(ptTempInstance!=NULL)
{
//动态创建链表项
Node<Type> *ptTempNode=new Node<Type>;
//保存实例指针到节点中
ptTempNode->ptInstance=ptTempInstance;
//将动态创建的链表节点添加到链表中
if(ptNodeHead==NULL)
{
//插入链表节点,如果链表为空则直接添加
ptNodeHead=ptTempNode;
ptNodeTail=ptTempNode;
}
else
{
//如果链表不为空,则将动态创建的节点保存到链表的尾部
ptTempNode->ptPrevNode=ptNodeTail;
//将子类保存到新创建的链表项指针中
ptNodeTail->ptNextNode=ptTempNode;
//更新链表尾部指针
ptNodeTail=ptTempNode;
}
//统计信息
st_NodeCreateNum++;
//统计子节点个数
iNodeAmount++;
}
//返回节点内容的实例指针
return ptTempInstance;
}
//从链表尾部删除一个保存特定实例指针的子节点
template<typename Type>
Type *NodeList<Type>::DelNode()
{
//链表节点不能为空
if(ptNodeTail!=NULL)
{
//从尾部删除动态节点
Node<Type> *ptTempNode=ptNodeTail;
//保存待删除节点内容的实例指针
Type *ptTempInstance=ptNodeTail->ptInstance;
//从尾部删除节点
if(ptNodeTail->ptPrevNode==NULL)
{
//重置尾部节点指针
ptNodeHead=NULL;
ptNodeTail=NULL;
}
else
{
//删除尾部子节点指针
ptNodeTail->ptPrevNode->ptNextNode=NULL;
//设置新的尾部节点指针
ptNodeTail=ptNodeTail->ptPrevNode;
}
//从尾部删除动态节点
delete ptTempNode;
//统计信息
st_NodeDeleteNum++;
//统计子节点个数
iNodeAmount--;
//返回待删除节点内容的实例指针
return ptTempInstance;
}
return NULL;
}
//从链表中删除特定实例指针的一个子节点
template<typename Type>
Type *NodeList<Type>::DelNode(Type *ptTempInstance)
{
//待删除实例指针不能为空
if(ptTempInstance!=NULL)
{
//链表不能为空
if(ptNodeHead!=NULL)
{
//头部节点作为循环的开始节点
Node<Type> *ptTempNode=ptNodeHead;
//遍历链表项,删除指定内容的节点
while(ptTempNode!=NULL)
{
if(ptTempNode->ptInstance==ptTempInstance)
{
//当前待删除节点在头部的情况
if(ptTempNode==ptNodeHead)
{
//判断是否只有一个节点
if(ptTempNode->ptNextNode==NULL)
{
//如果只有一项待删除,则置空头部和尾部指针
ptNodeHead=NULL;
ptNodeTail=NULL;
}
else
{
//如果有两项及以上节点,将头部指针指向下一个节点
ptNodeHead=ptTempNode->ptNextNode;
//将当前头节点的上一项指针重置为空
ptNodeHead->ptPrevNode=NULL;
}
}
else
{
//当前待删除节点不在头部的情况
if(ptTempNode==ptNodeTail)
{
//当前待删除节点在尾部的情况,尾部指针指向待删除节点的前一个节点
ptNodeTail=ptTempNode->ptPrevNode;
//当前待删除节点在尾部的情况,将末尾的节点下一项指针置空删除掉
ptNodeTail->ptNextNode=NULL;
}
else
{
//当前待删除节点在中部的情况,删除位于中间节点
ptTempNode->ptPrevNode->ptNextNode=ptTempNode->ptNextNode;
//当前待删除节点在中部的情况,删除位于中间节点
ptTempNode->ptNextNode->ptPrevNode=ptTempNode->ptPrevNode;
}
}
//删除动态创建的链表项
delete ptTempNode;
//统计信息
st_NodeDeleteNum++;
iNodeAmount--;
//返回待删除节点内容的实例指针
return ptTempInstance;
}
//指针移动到下一个节点进行循环
ptTempNode=ptTempNode->ptNextNode;
}
}
}
return NULL;
}
//显示所有子节点概要信息
template<typename Type>
void NodeList<Type>::ShowNodeList(HWND hWnd,HDC hDC,float x,float y,float h)
{
int line=0;
char szTemp[1024]="";
sprintf(szTemp,"Show All Node In List[amount:%d]",iNodeAmount);
glWindowPos2f(x,y-h*line++);
drawString(hDC,szTemp);
//显示头部和尾部指针信息
sprintf(szTemp,"[head:%d][tail:%d]",ptNodeHead,ptNodeTail);
glWindowPos2f(x,y-h*line++);
drawString(hDC,szTemp);
//标记首次开始循环的指针
Node<Type> *ptTempNode=ptNodeHead;
//记录序号
int index=0;
//遍历所有子物体进行判断
while(ptTempNode!=NULL)
{
//定位显示位置
glWindowPos2f(x,y-h*line++);
//显示子物体信息
sprintf(szTemp,"[%d][prev:%d][curr:%d][next:%d][inst:%d];",++index
,ptTempNode->ptPrevNode
,ptTempNode
,ptTempNode->ptNextNode
,ptTempNode->ptInstance);
drawString(hDC,szTemp);
ptTempNode=ptTempNode->ptNextNode;
}
}
5、在Object中添加对子列表的操作
我们刚才完成了链表模板,它的功能只是一个工具,应该只包括一些基本的链表操作。具体的添加子物体和删除子物体等操作不应该在列表本身实现,这些操作应该是抽象的Object类中来实现的。因此我们在Object类中添加了对子类的各种常用基础操作。同时,为了确保防范内存溢出,我们使用了一些统计变量来记录new和delete的使用情况。
//用于统计所有创建的物体个数,基类为Object实例的总创建和总销毁数量
int st_ObjectCreateNum=0;
int st_ObjectDeleteNum=0;
//用于统计所有创建的物体个数,(静态创建)基类为Object实例的总创建和总销毁数量
int st_ObjectStaticCreateNum=0;
int st_ObjectStaticDeleteNum=0;
//用于统计所有创建的物体个数,(动态创建)基类为Object实例的总创建和总销毁数量
int st_ObjectDynamicCreateNum=0;
int st_ObjectDynamicDeleteNum=0;
//用于统计所有创建的物体个数,显示调试信息
float ShowObjectStatistic(HWND hWnd,HDC hDC,float x,float y,float h=20);
//重置统计数据
void ResetObjectStatistic();
//基础类
class Object
{
public:
//当前物体的名称
char szName[100];
//当前物体的类型
char szType[100];
//标记该物体是否为动态创建
bool tagDynamicCreate;
public:
//创建物体
Object();
//标记为动态创建状态
void SetDynamicCreateStatus();
//析构函数添加为虚函数,才能确保所有继承类注销时调用
virtual ~Object();
//设置物体的名称
void SetName(char *szTempName);
//设置物体的名称
void SetType(char *szTempType);
public:
//当前物体的父物体指针
Object *ptParent;
//当前物体的子物体链表,用于存储所有子物体指针
NodeList<Object> ChildNodeList;
//添加某个子物体
Object *AddChild(Object *ptTempObject);
//删除某个子物体,并自动是否动态创建的子物体
Object *DelChild(Object *ptTempObject);
//删除所有子物体,并自动是否动态创建的子物体
void DelAllChild();
//非递归显示显示所有子节点内容,仅显示当前物体的子物体
void ShowChildNodeList(HWND hWnd,HDC hDC,float x,float y,float h=20);
//递归显示所有子节点信息,返回显示所有子节点后的光标位置,参数iChildLevel表示统计层级,参数iChildIndex子物体的统计编号
float ShowAllChildNodeList(HWND hWnd,HDC hDC,float x,float y,int iChildLevel=0,int iChildIndex=0,float h=20);
public:
//用于除构造函数以外的操作
virtual void Initialize();
//用于除析构函数以外的操作
virtual void UnInitialize();
public:
virtual void OnSolid3DPaint(HWND hWnd,HDC hDC,Shader &tempShader);
virtual void OnAlpha3DPaint(HWND hWnd,HDC hDC,Shader &tempShader);
virtual void OnSolid2DPaint(HWND hWnd,HDC hDC,Shader &tempShader);
virtual void OnTimer(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnMouseMove(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnLButtonDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnLButtonDblClk(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnLButtonUp(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnRButtonDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnRButtonDblClk(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnRButtonUp(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnMouseWheel(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnKeyDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnKeyUp(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnChar(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnImeComposition(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnSize(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnSetFocus(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
virtual void OnKillFocus(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
};
具体Object类的详细操作如下:
float ShowObjectStatistic(HWND hWnd,HDC hDC,float x,float y,float h)
{
int line=0;
//显示坐标信息文字
glColor3f(1,0,0);
//显示子物体链表
char szTemp[1024]="";
//显示统计信息
glWindowPos2f(x,y-h*line++);
sprintf(szTemp,"st_ObjectCreateNum:%d",st_ObjectCreateNum);
drawString(hDC,szTemp);
//显示统计信息
glWindowPos2f(x,y-h*line++);
sprintf(szTemp,"st_ObjectDeleteNum:%d",st_ObjectDeleteNum);
drawString(hDC,szTemp);
//显示统计信息
glWindowPos2f(x,y-h*line++);
sprintf(szTemp,"st_ObjectStaticCreateNum:%d",st_ObjectStaticCreateNum);
drawString(hDC,szTemp);
//显示统计信息
glWindowPos2f(x,y-h*line++);
sprintf(szTemp,"st_ObjectStaticDeleteNum:%d",st_ObjectStaticDeleteNum);
drawString(hDC,szTemp);
//显示统计信息
glWindowPos2f(x,y-h*line++);
sprintf(szTemp,"st_ObjectDynamicCreateNum:%d",st_ObjectDynamicCreateNum);
drawString(hDC,szTemp);
//显示统计信息
glWindowPos2f(x,y-h*line++);
sprintf(szTemp,"st_ObjectDynamicDeleteNum:%d",st_ObjectDynamicDeleteNum);
drawString(hDC,szTemp);
//返回显示后的纵坐标
return y-h*line;
}
//重置统计数据
void ResetObjectStatistic()
{
//用于统计所有创建的物体个数,基类为Object实例的总创建和总销毁数量
st_ObjectCreateNum=0;
st_ObjectDeleteNum=0;
//用于统计所有创建的物体个数,(静态创建)基类为Object实例的总创建和总销毁数量
st_ObjectStaticCreateNum=0;
st_ObjectStaticDeleteNum=0;
//用于统计所有创建的物体个数,(动态创建)基类为Object实例的总创建和总销毁数量
st_ObjectDynamicCreateNum=0;
st_ObjectDynamicDeleteNum=0;
}
Object::Object()
{
//当前物体的名称
SetName("Object");
//当前物体的类型
SetType("Object");
//当前物体的父物体指针
ptParent=NULL;
//统计信息
st_ObjectCreateNum++;
//标记是否动态创建
tagDynamicCreate=false;
//是否动态创建统计信息
tagDynamicCreate==false?st_ObjectStaticCreateNum++:st_ObjectDynamicCreateNum++;
}
//标记为动态创建状态
void Object::SetDynamicCreateStatus()
{
tagDynamicCreate=true;
//动态创建统计信息
st_ObjectStaticCreateNum--;
//动态创建统计信息
st_ObjectDynamicCreateNum++;
}
Object::~Object()
{
//递归删除所有的子节点及子物体
DelAllChild();
//统计信息
st_ObjectDeleteNum++;
//统计信息
tagDynamicCreate==false?st_ObjectStaticDeleteNum++:st_ObjectDynamicDeleteNum++;
}
//设置物体的名称
void Object::SetName(char *szTempName)
{
strcpy(szName,szTempName);
}
//设置物体的名称
void Object::SetType(char *szTempType)
{
strcpy(szType,szTempType);
}
//添加某个子物体
Object *Object::AddChild(Object *ptTempObject)
{
if(ptTempObject!=NULL)
{
//插入前先删除旧的子指针,防止出现重复添加两次的情况
ChildNodeList.DelNode(ptTempObject);
//在链表尾部添加一个节点,并保存指定的实例指针
ChildNodeList.AddNode(ptTempObject);
//在子物体中保存父物体的指针
ptTempObject->ptParent=this;
}
//返回待添加的实例指针
return ptTempObject;
}
//删除某个子物体
Object *Object::DelChild(Object *ptTempObject)
{
if(ptTempObject!=NULL)
{
//删除链表中保存指定实例指针的子节点
ChildNodeList.DelNode(ptTempObject);
//在子物体中保存父物体的指针
ptTempObject->ptParent=NULL;
//释放动态创建的实例
if(ptTempObject->tagDynamicCreate==true)
{
delete ptTempObject;
}
}
//返回待删除的实例指针
return ptTempObject;
}
//删除所有子物体
void Object::DelAllChild()
{
//遍历链表项,删除指定内容的节点
while(ChildNodeList.ptNodeTail!=NULL)
{
//删除链表中的尾部的子节点,并返回子节点中保存的实例指针
Object *ptTempObject=ChildNodeList.DelNode();
//已删除节点返回的实例不能为空
if(ptTempObject!=NULL)
{
//在子物体中保存父物体的指针
ptTempObject->ptParent=NULL;
//释放动态创建的实例
if(ptTempObject->tagDynamicCreate==true)
{
delete ptTempObject;
}
}
}
}
//非递归显示显示所有子节点内容
void Object::ShowChildNodeList(HWND hWnd,HDC hDC,float x,float y,float h)
{
//设置显示文字颜色
glColor3f(1,0,0);
//显示当前物体信息
char szTemp[1024]="";
glWindowPos2f(x,y);
sprintf(szTemp,"%s[%d]",szName,this);
drawString(hDC,szTemp);
sprintf(szTemp,"ptParent:%s[%d]",(ptParent==NULL?"NULL":ptParent->szName),ptParent);
drawString(hDC,szTemp);
//调试信息,非递归显示链表调试信息
ChildNodeList.ShowNodeList(hWnd,hDC,x,y-h);
}
//递归显示所有子节点信息,返回显示所有子节点后的光标位置
float Object::ShowAllChildNodeList(HWND hWnd,HDC hDC,float x,float y,int iChildLevel,int iChildIndex,float h)
{
//设置显示文字颜色
glColor3f(1,0,0);
//显示当前物体信息
char szTemp[1024]="";
glWindowPos2f(x+iChildLevel*20,y);
sprintf(szTemp,"[%d][%d]->%s[%d][%s]",iChildLevel,iChildIndex,szName,ChildNodeList.iNodeAmount,tagDynamicCreate?"D":"S");
drawString(hDC,szTemp);
//设置首个子节点计数
iChildIndex=0;
//获取首个节点
Node<Object> *ptTempNode=ChildNodeList.ptNodeHead;
//循环显示各个子节点信息
while(ptTempNode!=NULL)
{
if(ptTempNode->ptInstance!=NULL)
{
y=ptTempNode->ptInstance->ShowAllChildNodeList(hWnd,hDC,x,y-20,1+iChildLevel,iChildIndex++,h);
}
//进行下一个节点
ptTempNode=ptTempNode->ptNextNode;
}
//返回光标的位置
return y;
}
void Object::Initialize()
{}
void Object::UnInitialize()
{}
void Object::OnSolid3DPaint(HWND hWnd,HDC hDC,Shader &tempShader)
{}
void Object::OnAlpha3DPaint(HWND hWnd,HDC hDC,Shader &tempShader)
{}
void Object::OnSolid2DPaint(HWND hWnd,HDC hDC,Shader &tempShader)
{}
void Object::OnTimer(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnMouseMove(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnLButtonDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnLButtonDblClk(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnLButtonUp(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnRButtonDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnRButtonDblClk(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnRButtonUp(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnMouseWheel(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnKeyDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnKeyUp(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnChar(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnImeComposition(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnSize(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnSetFocus(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
void Object::OnKillFocus(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{}
6、父子物体的操作及展示
在添加完以上的操作功能后,我们就可以方便的去创建父子物体的关系。同时为了方便查看,我们实现了一个递归函数的目录树显示功能,来用树形目录的形式展示父子物体的关系。我们可以通过任何一个父物体的ShowAllChildNodeList函数来展示实例树及其内容。
具体的树形目录显示如下,具体来说一个静态创建的MainSence为根节点,之后所有的游戏元素都会以子物体的方式添加到这个主要场景节点上。就是说我们主要场景中包括了一个坐标轴子物体,一个用于显示游戏帧的子物体,一个用于显示木箱的子物体(当然这个木箱这个子物体还可以拥有它的子物体),和一个全局的平行光子物体。将这些子物体添加到MainSence场景中后,我们就可以方便的统一在屏幕中显示到我们场景中所有的游戏元素。
有没有发现这个有点像unity里的结构展示窗口。
6.1、子物体的添加
所有Object类(以及Object类的派生类)均具有AddChild方法,可以使用AddChild函数添加两个物体的父子关系。
//自定义一个父物体和一个子物体
Object ParentObject;
//自定义一个子物体
Object ChildObject1;
ParentObject.AddChild(&ChildObject1);
ChildObject1.SetName("ChildObject1");
//创建动态实例,并添加到父物体子链表中
Object *ptObject=new Object();
ParentObject.AddChild(ptObject);
ptObject->SetDynamicCreateStatus();
ptObject->SetName("ChildObject2");
6.2、指定子物体的删除
在已经建立父子关系后,父物体可以通过DelChild函数和DelAllChild函数删除子物体。
//删除静态创建的子物体实例
ParentObject.DelChild(&ChildObject1);
//删除动态创建的子物体实例
ParentObject.DelChild(ptObject);
//删除所有子物体实例
ParentObject.DelAllChild();
6.3、所有子物体的遍历显示
我们可以通过链表的循环,遍历各个子物体,并执行相应子物体的消息处理函数。
//获取首个子节点物体
Node<Object> *ptTempNode=ParentObject.ChildNodeList.ptNodeHead;
//循环执行各个子节点物体的消息处理函数
while(ptTempNode!=NULL)
{
if(ptTempNode->ptInstance!=NULL)
{
ptTempNode->ptInstance->OnSolid3DPaint(hWnd,hDC,tempShader);
}
//进行下一个节点
ptTempNode=ptTempNode->ptNextNode;
}
7、用户互动添加父子物体例子(跳跃的立方体)
根据我们以上添加的父子结构功能,来实现一个小小的互动实例。我们这里有一个主场景类,初期这个类仅有一些必要的游戏元素(游戏帧、平行光),没有任何方块子物体,当用户点击鼠标右键时,通过消息处理函数自动添加一个动态生成的立方体,并将它添加到我们的主场景物体中,并开始不停的跳跃。因为我们已经在显示函数中添加了主场景(MainSence)内所有子物体的显示函数,主场景的所有子物体也会自动显示到我们的屏幕上。通过这种模式,我们极大的增加了我们的编程代码的便捷性。
7.1、添加Transform结构体
在做这个例子前,我们需要做一些准备工作。所有物体应该具有位置、旋转和缩放信息,所有我们需要一个Transform结构体,用于保存物体的位置、旋转和缩放信息。
//负责记录位置、旋转和缩放信息
struct Transform
{
//物体的位置、角度和缩放比例
glm::vec3 Position;
glm::vec3 Rotate;
glm::vec3 Scale;
//初始化
Transform()
{
Position=glm::vec3(0.0f,0.0f,0.0f);
Rotate=glm::vec3(0.0f,0.0f,0.0f);
Scale=glm::vec3(1.0f,1.0f,1.0f);
}
};
有点类似于Unity界面操作控件中设置的要素。
7.2、添加GemeObject类
我们需要创建一个GemeObject类,它具有游戏所需具备的一些必要特性,比如说拥有Transform结构体,用于保存物体的位置、旋转和缩放信息。这个类时抽象出来的,主要是为了管理方便。
//游戏的基本类,具有游戏所需具备的一些必要特性
class GameObject:public Object
{
public:
//物体的位置,相对位置、相对角度和相对缩放比例
Transform RelativeTransform;
//物体的位置,考虑所有父物体后的绝对位置、绝对角度和绝对缩放比例
Transform AbsoluteTransform;
public:
GameObject();
};
GameObject::GameObject()
{}
7.3、添加Cube立方体类
我们先创建一个立方体类,这个立方体只是显示一个预制的立方体,我们将在这个基本的立方体类基础上进一步生产“跳跃的立方体”。这里关于光照和材质的设置可以参照C++和OpenGL实现3D游戏编程【连载19】——着色器光照初步(平行光和光照贴图)中关于光照和材质的介绍。
//创建一个立方体预制体
class Cube:public GameObject
{
public:
//生成一个网格
Mesh MainMesh;
//颜色
glm::vec4 Color;
public:
Cube();
void OnSolid3DPaint(HWND hWnd,HDC hDC,Shader *tempShader);
};
Cube::Cube()
{
//当前物体的名称
SetName("Cube");
SetType("Cube");
//加载网格和纹理
MainMesh.LoadMeshFromObjFile("Model\\Cube\\Cube.obj");
}
void Cube::OnSolid3DPaint(HWND hWnd,HDC hDC,Shader *tempShader)
{
//重置并设置模型矩阵
ModelMatrix=glm::mat4(1.0f);
//根据位置、旋转和缩放信息设置模型矩阵
ModelMatrix=glm::translate(ModelMatrix,RelativeTransform.Position);
ModelMatrix=glm::rotate(ModelMatrix,RelativeTransform.Rotate.x,glm::vec3(1.0f,0.0f,0.0f));
ModelMatrix=glm::rotate(ModelMatrix,RelativeTransform.Rotate.y,glm::vec3(0.0f,1.0f,0.0f));
ModelMatrix=glm::rotate(ModelMatrix,RelativeTransform.Rotate.z,glm::vec3(0.0f,0.0f,1.0f));
ModelMatrix=glm::scale(ModelMatrix,RelativeTransform.Scale);
//启用光照
light[0].tagEnable=1;
light[1].tagEnable=0;
//设置材质,启用颜色
material.tagEnable=1;
material.diffuse=Color;
material.specular=Color;
//启用着色器并进行渲染
tempShader->UseShader();
//显示网格
MainMesh.DrawMesh(hWnd,hDC,tempShader);
}
7.4、添加JumpingCube类
我们将在基础的立方体基础上,添加立方体的跳跃功能,这是我们最终需要的“跳跃的立方体”。
//创建一个跳跃立方体类
class JumpingCube:public Cube
{
public:
//记录调整时间
float JumpStepTime;
public:
JumpingCube();
void OnSolid3DPaint(HWND hWnd,HDC hDC,Shader *tempShader);
};
JumpingCube::JumpingCube()
{
//当前物体的名称
SetName("JumpingCube");
SetType("JumpingCube");
//跳跃调整时间
JumpStepTime=0;
//随机颜色
switch(rand()%7)
{
case 0:Color=glm::vec4(1.0f,1.0f,1.0f,1.0f);break;
case 1:Color=glm::vec4(1.0f,0.0f,0.0f,1.0f);break;
case 2:Color=glm::vec4(0.0f,1.0f,0.0f,1.0f);break;
case 3:Color=glm::vec4(0.0f,0.0f,1.0f,1.0f);break;
case 4:Color=glm::vec4(1.0f,1.0f,0.0f,1.0f);break;
case 5:Color=glm::vec4(1.0f,0.0f,1.0f,1.0f);break;
case 6:Color=glm::vec4(0.0f,1.0f,1.0f,1.0f);break;
}
}
void JumpingCube::OnSolid3DPaint(HWND hWnd,HDC hDC,Shader *tempShader)
{
//调整时间
JumpStepTime+=0.05;
//重置并设置模型矩阵
ModelMatrix=glm::mat4(1.0f);
//根据位置、旋转和缩放信息设置模型矩阵
ModelMatrix=glm::translate(ModelMatrix,RelativeTransform.Position+glm::vec3(0.0f,Lerp(0.0f,3.0f,sin(JumpStepTime)),0.0f));
//ModelMatrix=glm::rotate(ModelMatrix,RelativeTransform.Rotate.x,glm::vec3(1.0f,0.0f,0.0f));
ModelMatrix=glm::rotate(ModelMatrix,RelativeTransform.Rotate.y+Lerp(0.0f,2*PI,JumpStepTime*0.2),glm::vec3(0.0f,1.0f,0.0f));
//ModelMatrix=glm::rotate(ModelMatrix,RelativeTransform.Rotate.z,glm::vec3(0.0f,0.0f,1.0f));
ModelMatrix=glm::scale(ModelMatrix,RelativeTransform.Scale+glm::vec3(Lerp(0.0f,0.5f,cos(JumpStepTime)/2+0.5),Lerp(0.0f,0.5f,cos(JumpStepTime)/2+0.5),Lerp(0.0f,0.5f,cos(JumpStepTime)/2+0.5)));
//启用光照
light[0].tagEnable=1;
light[1].tagEnable=0;
//设置材质,启用颜色
material.tagEnable=1;
material.diffuse=Color;
material.specular=Color;
//启用着色器并进行渲染
tempShader->UseShader();
//显示网格
MainMesh.DrawMesh(hWnd,hDC,tempShader);
}
7.5、鼠标右键添加跳跃的立方体
我们需要通过鼠标右键添加一些用户的互动操作,当用户点击鼠标右键时,会在随机位置产生于一个跳动的立方体,颜色随机,位置随机,立方体可以不断地跳动并进行旋转。同时添加跳动的立方体为主场景MainSence下的子物体,MainSence是一个GameObject类,他没有实际意义,仅仅作为一个父物体,将所有场景内游戏元素“挂”成该物体的子物体,即可实现统一管理。
case WM_RBUTTONDOWN:
if(true)
{
//新动态创建一个物体,并把该物体添加到主场景的子物体列表中
JumpingCube *ptTempObject=new JumpingCube();
if(ptTempObject!=NULL)
{
//标记为动态创建,后期删除子物体时可自动删除释放内存
ptTempObject->SetDynamicCreateStatus();
//添加为子物体
MyMainSence.AddChild(ptTempObject);
//设置新创建物体的位移、旋转、缩放信息
ptTempObject->RelativeTransform.Position=glm::vec3(10.0f-(rand()%20)*1.0f,0.0f,3.0f-(rand()%20)*0.3f);
ptTempObject->RelativeTransform.Rotate=glm::vec3(PI*(rand()%360)/360.0f,PI*(rand()%360)/360.0f,PI*(rand()%360)/360.0f);
ptTempObject->RelativeTransform.Scale=glm::vec3(0.5f,0.5f,0.5f);
}
}
return 0;
同时我们需要显示我们物体,可以循环显示MainSence下的所有子物体。
case WM_PAINT:
PAINTSTRUCT PS;
hDC=BeginPaint(hWnd,&PS);
//显示三维世界内容
......
//获取首个子节点物体
Node<Object> *ptTempNode=MyMainSence.ChildNodeList.ptNodeHead;
//循环执行各个子节点物体的消息处理函数
while(ptTempNode!=NULL)
{
if(ptTempNode->ptInstance!=NULL)
{
ptTempNode->ptInstance->OnSolid3DPaint(hWnd,hDC,tempShader);
}
//进行下一个节点
ptTempNode=ptTempNode->ptNextNode;
}
......
ReleaseDC(hWnd,hDC);
return 0;
7.6、显示效果
最终的显示效果如下:
跳跃的方块
8、总结
C++作为一个下接操作系统硬件底层,上接用户逻辑的编程语言,为了适应各种环境,不为你不需要的东西付代价,C++是并没有提供原生内存管理GC的。STL库的那些智能指针更多只是在C++的语言层面上再提供一些小辅助。在最开始设计游戏引擎的时候,你不光要考虑该引擎所面对的用户群体和针对的游戏重点,更要开始考虑你所能利用到的都有什么内存管理方式。
【上一节】:🔥C++和OpenGL实现3D游戏编程【连载20】——游戏基类Object的构建
【下一节】:🔥[C++和OpenGL实现3D游戏编程【连载22】——更新中