当前位置: 首页 > news >正文

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】——更新中

相关文章:

C++和OpenGL实现3D游戏编程【连载21】——父物体和子物体模式实现

欢迎来到zhooyu的专栏。 &#x1f525;C和OpenGL实现3D游戏编程【专题总览】 1、本节要实现的内容 上节课我们已经创建了一个基础Object类&#xff0c;以后所有的游戏元素都可以从这个基类中派生出来。同时为了操作方便&#xff0c;我们可以为任意两个Object类&#xff08;及其…...

Mac苹果电脑 怎么用word文档和Excel表格?

以下是详细步骤&#xff0c;帮助你在 MacBook 上安装和使用 Word 和 Excel&#xff1a; 安装 Microsoft Office 你可以通过以下几种方式在 MacBook 上安装 Word 和 Excel&#xff1a; 方法一&#xff1a;应用安装 pan.baidu.com/s/1EO2uefLPoeqboi69gIeZZg?pwdi2xk 方法二…...

使用AI生成金融时间序列数据:解决股市场的数据稀缺问题并提升信噪比

“GENERATIVE MODELS FOR FINANCIAL TIME SERIES DATA: ENHANCING SIGNAL-TO-NOISE RATIO AND ADDRESSING DATA SCARCITY IN A-SHARE MARKET” 论文地址&#xff1a;https://arxiv.org/pdf/2501.00063 摘要 金融领域面临的数据稀缺与低信噪比问题&#xff0c;限制了深度学习在…...

QT信号槽 笔记

信号与槽就是QT中处理计算机外设响应的一种机制 比如敲击键盘、点击鼠标 // 举例&#xff1a; 代码&#xff1a; connect(ls,SIGNAL(sig_chifanla()),ww,SLOT(slot_quchifan())); connect(ls,SIGNAL(sig_chifanla()),zl,SLOT(slot_quchifan()));connect函数&#xff1a;这是…...

【计算机网络】传输层协议TCP与UDP

传输层 传输层位于OSI七层网络模型的第四层&#xff0c;主要负责端到端通信&#xff0c;可靠性保障&#xff08;TCP&#xff09;&#xff0c;流量控制(TCP)&#xff0c;拥塞控制(TCP)&#xff0c;数据分段与分组&#xff0c;多路复用与解复用等&#xff0c;通过TCP与UDP协议实现…...

UE控件学习

ListView&#xff1a; item设置&#xff1a;使能在list设置为Entry类 关闭listview自带的滑动条 【UEUI篇】ListView使用经验总结 UE4 ListView用法总结&#xff08;二&#xff09;Item的选中与数据获取 Grid Panel&#xff1a; 常用作背包&#xff0c;每个格子大小可不相…...

ThinkPHP 8的多对多关联

【图书介绍】《ThinkPHP 8高效构建Web应用》-CSDN博客 《2025新书 ThinkPHP 8高效构建Web应用 编程与应用开发丛书 夏磊 清华大学出版社教材书籍 9787302678236 ThinkPHP 8高效构建Web应用》【摘要 书评 试读】- 京东图书 使用VS Code开发ThinkPHP项目-CSDN博客 编程与应用开…...

Linux内核编程(二十一)USB驱动开发

一、驱动类型 USB 驱动开发主要分为两种&#xff1a;主机侧的驱动程序和设备侧的驱动程序。一般我们编写的都是主机侧的USB驱动程序。 主机侧驱动程序用于控制插入到主机中的 USB 设备&#xff0c;而设备侧驱动程序则负责控制 USB 设备如何与主机通信。由于设备侧驱动程序通常与…...

【Block总结】WTConv,小波变换(Wavelet Transform)来扩展卷积神经网络(CNN)的感受野

论文解读&#xff1a;Wavelet Convolutions for Large Receptive Fields 论文信息 标题: Wavelet Convolutions for Large Receptive Fields作者: Shahaf E. Finder, Roy Amoyal, Eran Treister, Oren Freifeld提交日期: 2024年7月8日arXiv链接: Wavelet Convolutions for La…...

深入探究分布式日志系统 Graylog:架构、部署与优化

文章目录 一、Graylog简介二、Graylog原理架构三、日志系统对比四、Graylog部署传统部署MongoDB部署OS或者ES部署Garylog部署容器化部署 五、配置详情六、优化网络和 REST APIMongoDB 七、升级八、监控九、常见问题及处理 一、Graylog简介 Graylog是一个简单易用、功能较全面的…...

构建高可用和高防御力的云服务架构第五部分:PolarDB(55)

引言 云计算与数据库服务 云计算作为一种革命性的技术&#xff0c;已经深刻改变了信息技术行业的面貌。它通过提供按需分配的计算资源&#xff0c;使得数据存储、处理和分析变得更加灵活和高效。在云计算的众多服务中&#xff0c;数据库服务扮演着核心角色。数据库服务不仅负…...

【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活

&#x1f4ac; 欢迎讨论&#xff1a;如对文章内容有疑问或见解&#xff0c;欢迎在评论区留言&#xff0c;我需要您的帮助&#xff01; &#x1f44d; 点赞、收藏与分享&#xff1a;如果这篇文章对您有所帮助&#xff0c;请不吝点赞、收藏或分享&#xff0c;谢谢您的支持&#x…...

HTTP / 2

序言 在之前的文章中我们介绍过了 HTTP/1.1 协议&#xff0c;现在再来认识一下迭代版本 2。了解比起 1.1 版本&#xff0c;后面的版本改进在哪里&#xff0c;特点在哪里&#xff1f;话不多说&#xff0c;开始吧⭐️&#xff01; 一、 HTTP / 1.1 存在的问题 很多时候新的版本的…...

【深度学习】利用Java DL4J 训练金融投资组合模型

🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程,高并发设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s…...

跨域cookie携带问题总结

背景 我们知道很多场景&#xff0c;都需要前端请求带上cookie&#xff0c;例如用户鉴权、登陆校验等。而有些场景下&#xff0c;我们会发现请求不会带上cookie&#xff0c;这是为什么呢&#xff1f; 概念 cookie是种在域名下的信息。只有请求同域且同站的请求&#xff0c;才…...

Pytorch使用教程(12)-如何进行并行训练?

在使用GPU训练大模型时&#xff0c;往往会面临单卡显存不足的情况。这时&#xff0c;通过多卡并行的形式来扩大显存是一个有效的解决方案。PyTorch主要提供了两个类来实现多卡并行&#xff1a;数据并行torch.nn.DataParallel&#xff08;DP&#xff09;和模型并行torch.nn.Dist…...

指针之旅:从基础到进阶的全面讲解

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 本文目录 引言正文&#xff08;1&#xff09;内置数…...

FPGA与ASIC:深度解析与职业选择

IC&#xff08;集成电路&#xff09;行业涵盖广泛&#xff0c;涉及数字、模拟等不同研究方向&#xff0c;以及设计、制造、封测等不同产业环节。其中&#xff0c;FPGA&#xff08;现场可编程门阵列&#xff09;和ASIC&#xff08;专用集成电路&#xff09;是两种重要的芯片类型…...

PostgreSQL 中进行数据导入和导出

在数据库管理中&#xff0c;数据的导入和导出是非常常见的操作。特别是在 PostgreSQL 中&#xff0c;提供了多种工具和方法来实现数据的有效管理。无论是备份数据&#xff0c;还是将数据迁移到其他数据库&#xff0c;或是进行数据分析&#xff0c;掌握数据导入和导出的技巧都是…...

SDL2基本的绘制流程与步骤

SDL2(Simple DirectMedia Layer 2)是一个跨平台的多媒体库,它为游戏开发和图形应用提供了一个简单的接口,允许程序直接访问音频、键盘、鼠标、硬件加速的渲染等功能。在 SDL2 中,屏幕绘制的流程通常涉及到窗口的创建、渲染目标的设置、图像的绘制、事件的处理等几个步骤。…...

人工智能--大型语言模型的存储

好的&#xff0c;我现在需要回答用户关于GGUF文件和safetensors文件后缀的差别的问题。首先&#xff0c;我得先确认这两个文件格式的具体应用场景和它们各自的优缺点。用户可能是在处理大模型时遇到了这两种文件格式&#xff0c;想了解它们的区别以便正确使用。 首先&#xff…...

第四讲:类和对象(下)

1. 再探构造函数 • 之前我们实现构造函数时&#xff0c;初始化成员变量主要使⽤函数体内赋值&#xff0c;构造函数初始化还有⼀种⽅ 式&#xff0c;就是初始化列表&#xff0c;初始化列表的使⽤⽅式是以⼀个冒号开始&#xff0c;接着是⼀个以逗号分隔的数据成 员列表&#xff…...

山东大学《数据可视化》期末复习宝典

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a;&#x1f3c0;山东大学期末速通专用_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1…...

Linux系统编程-DAY10(TCP操作)

一、网络模型 1、服务器/客户端模型 &#xff08;1&#xff09;C/S&#xff1a;client server &#xff08;2&#xff09;B/S&#xff1a;browser server &#xff08;3&#xff09;P2P&#xff1a;peer to peer 2、C/S与B/S区别 &#xff08;1&#xff09;客户端不同&#…...

自动化办公集成工具:一站式解决文档处理难题

1. 项目概述 在当今信息化时代,办公自动化已成为提升工作效率的关键。本文将详细介绍一款基于Python和PyQt5开发的「自动化办公集成工具」,该工具集成了多种常用的办公文档处理功能,包括批量格式转换、文本智能替换、表格数据清洗等,旨在为用户提供一站式的办公自动化解决方…...

Git 常见操作

目录 1.git stash 2.合并多个commit 3. git commit -amend (后悔药) 4.版本回退 5.merge和rebase 6.cherry pick 7.分支 8.alias 1.git stash git-stash操作_git stash 怎么增加更改内容-CSDN博客 2.合并多个commit 通过git bash工具交互式操作。 1.查询commit的c…...

Android设备推送traceroute命令进行网络诊断

文章目录 工作原理下载traceroute for android推送到安卓设备执行traceroutetraceroute www.baidu.com Traceroute&#xff08;追踪路由&#xff09; 是一个用于网络诊断的工具&#xff0c;主要用于追踪数据包从源主机到目标主机所经过的路由路径&#xff0c;以及每一跳&#x…...

LabVIEW音频测试分析

LabVIEW通过读取指定WAV 文件&#xff0c;实现对音频信号的播放、多维度测量分析功能&#xff0c;为音频设备研发、声学研究及质量检测提供专业工具支持。 主要功能 文件读取与播放&#xff1a;支持持续读取示例数据文件夹内的 WAV 文件&#xff0c;可实时播放音频以监听被测信…...

详细介绍uni-app中Composition API和Options API的使用方法

uni-app 中 Composition API 和 Options API 的使用方法详解 一、Options API&#xff08;Vue 2.x 传统方式&#xff09; 1. 基本结构 Options API 通过配置对象的不同选项&#xff08;如 data、methods、computed 等&#xff09;组织代码&#xff1a; <template><…...

《深入理解 Nacos 集群与 Raft 协议》系列四:日志复制机制:Raft 如何确保提交可靠且幂等

《深入理解 Nacos 集群与 Raft 协议》系列 大家好&#xff0c;我是G探险者&#xff01; 在前几篇中我们介绍了选主与日志对比机制&#xff0c;它们保证了“谁能成为 Leader”以及“Leader 的日志是否可靠”。 而当 Leader 已选定&#xff0c;系统需要把客户端的写请求写入所…...