您现在的位置是:首页 >其他 >C++和OpenGL实现3D游戏编程【连载23】——父物体和子物体的消息处理机制网站首页其他
C++和OpenGL实现3D游戏编程【连载23】——父物体和子物体的消息处理机制
欢迎来到zhooyu的专栏。
主页:【zhooyu】
专栏:【C++和OpenGL实现3D游戏编程】
🌟🌟🌟这里将通过一个OpenGL实现3D游戏编程实例教程,带大家深入学习OpenGL知识。知识无穷而人力有穷,希望能对您有所帮助。
🌟🌟🌟该教程为系列教程,每一步都有详细的教学和实例,推荐大家通过下边的🔥C++和OpenGL实现3D游戏编程【目录】系统性的了解开发过程,了解怎样一步一步从简单入手,借助C++和OpenGL实现强大的3D效果。
🌟🌟🌟同时您可以在QQ群(群号:739903792)中与大家进行沟通交流,共同解决编程过程中的困惑。
代码具体内容详见专题内容链接:
C++和OpenGL实现3D游戏编程【连载23】——父物体和子物体的消息处理机制(附源码)
1、本节要实现的内容
上一节我们了解了父子物体结构模式,方便我们快捷、控制游戏元素。这一节我们去了解怎样通过父子模式去处理系统消息,系统消息是我们和游戏之间互动的纽带,确定了游戏的用户体验。同时我们还将添加一些预制体,比如说立方体、球体、锥体、胶囊体等,方便我们今后的操作。另外我们此前的模型加载的较为单调,而且我们将加入一些游戏的模型元素,这个视频加载了各式各样的树木,构造出一个奇幻的森林雏形。
视频效果如下:
C++和Opengl森林景观
2、消息处理函数
Windows程序中最重要的WndProc消息处理函数。当Windows窗口接收到消息时,系统会调用这个WndProc函数(该函数是我们注册窗口时自定义的函数名,函数名不唯一)。我们需要在WndProc函数中根据消息的类型进行相应的处理。处理消息一般流程是这样的。
//消息处理模块
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hDC;
switch(message)
{
case WM_PAINT:
PAINTSTRUCT PS;
hDC=BeginPaint(hWnd,&PS);
......
ReleaseDC(hWnd,hDC);
return 0;
case WM_LBUTTONDOWN:
......
return 0;
case WM_LBUTTONDOWN:
......
return 0;
}
return DefWindowProc(hWnd,message,wParam,lParam);
}
我们之前的Object之类,已经添加了所有可能使用到的消息处理虚函数,消息处理虚函数如下边这个Object类的OnSolid3DPaint函数、OnLButtonDown函数和OnRButtonUp函数等等,它们与系统的消息相对应。
//基础类
class Object
{
public:
//当前物体的名称
char szName[100];
//当前物体的类型
char szType[100];
......
public:
//创建物体
Object();
......
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);
......
};
具体的对应关系如下:
1、鼠标左键的按下(OnLButtonDown),对应处理WM_LBUTTONDOWN消息。
2、鼠标左键的松开(OnLButtonUp),对应处理WM_LBUTTONUP消息。
3、鼠标左键的双击(OnLButtonDblClk),对应处理WM_LBUTTONDBLCLK消息。
4、鼠标右键的按下(OnRButtonDown),对应处理WM_RBUTTONDOWN消息。
5、鼠标右键的松开(OnRButtonUp),对应处理WM_RBUTTONUP消息。
6、鼠标右键的双击(OnRButtonDblClk),对应处理WM_RBUTTONDBLCLK消息。
7、鼠标移动(OnMouseMove),对应处理WM_MOUSEMOVE消息。
8、定时器(OnTimer),对应处理WM_TIMER消息。
9、键盘按键按下(OnKeyDown),对应处理WM_KEYDOWN消息。
10、键盘按键松开(OnKeyUp),对应处理WM_KEYUP消息。
11、键盘字符(OnChar),对应处理WM_CHAR消息。
12、窗口大小改变(OnSize),对应处理WM_SIZE消息。
13、鼠标轮滚动(OnMouseWheel),对应处理0x020A(通常自定义为WM_MOUSEWHEEL)消息,处理滚动鼠标操作。
14、输入法输入文字(OnImeComposition),对应处理WM_IMECOMPOSITION消息,处理输入法相关消息。
15、窗口获取焦点(OnSetFocus),对应处理WM_SETFOCUS消息,Windows窗口获得焦点时。
16、窗口失去焦点(OnKillFocus),对应处理WM_KILLFOCUS消息,Windows窗口失去焦点时。
具体来看,如果我们需要显示MyMainSence下所有子物体,那么我们就需要在WM_PAINT消息处理函数中遍历所有的子物体,并执行所有子物体的OnSolid3DPaint(hWnd,hDC,tempShader)显示函数。
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;
同样的,我们需要相应其他系统消息事件,我们就要在相应的消息处理分支添加MyMainSence的所有子类消息处理函数。比如这里让所有MyMainSence的子物体响应处理鼠标左键按下消息。
case WM_LBUTTONDOWN:
......
//获取首个子节点物体
Node<Object> *ptTempNode=MyMainSence.ChildNodeList.ptNodeHead;
//循环执行各个子节点物体的消息处理函数
while(ptTempNode!=NULL)
{
if(ptTempNode->ptInstance!=NULL)
{
ptTempNode->ptInstance->OnLButtonDown(hWnd,message,wParam,lParam);
}
//进行下一个节点
ptTempNode=ptTempNode->ptNextNode;
}
......
return 0;
比如这里让所有MyMainSence的子物体响应处理鼠标右键松开的消息。
case WM_RBUTTONUP:
......
//获取首个子节点物体
Node<Object> *ptTempNode=MyMainSence.ChildNodeList.ptNodeHead;
//循环执行各个子节点物体的消息处理函数
while(ptTempNode!=NULL)
{
if(ptTempNode->ptInstance!=NULL)
{
ptTempNode->ptInstance->OnRButtonUp(hWnd,message,wParam,lParam);
}
//进行下一个节点
ptTempNode=ptTempNode->ptNextNode;
}
......
return 0;
大家看到了,我们使用父子关系,已经能够将MyMainSence下的所有子物体通过遍历循环的方式调用消息处理函数,一定程度上已经比较方便了。但是,这样还存在一个问题,我们前边说子物体是可以再有自身的子物体,或者子子子子物体,那么通过上边这样的处理其实只能处理MyMainSence下的直接子物体消息函数,并不能处理子子物体或者子子子子物体的消息处理函数。那怎么来解决这个问题呢?当然有办法。
3、使用递归处理的方法执行消息处理函数
要解决以上问题,我们可以使用递归处理的方法,遍历执行消息处理函数。也就是说,我们通过以下自定义的DispatchPaintMessage和DispatchMessage函数去递归执行相应的消息处理函数。该函数会在执行自身消息处理函数的同时,递归处理当前物体的所有子物体的消息处理函数;那么,有了这个“同时处理自身子物体的消息处理函数”机制,当我们执行MyMainSence的DispatchPaintMessage和DispatchMessage函数时,系统不仅仅会首先执行MyMainSence的消息处理函数,系统还会遍历所有MyMainSence的子物体的消息处理函数,以及MyMainSence子物体的子物体、子子子子物体的所有消息处理函数。这样会极大简化我们编程的代码量,将所有消息处理分配到具体的类定义中,优化我们的编程逻辑,提高代码的可维护性。
void Object::DispatchPaintMessage(HWND hWnd,HDC hDC,Shader *tempShader,int iPaintType)
{
//根据传入消息判断当前物体需要执行的消息处理函数
switch(iPaintType)
{
case 0:OnSolid3DPaint(hWnd,hDC,tempShader);break;
case 1:OnAlpha3DPaint(hWnd,hDC,tempShader);break;
case 2:OnSolid2DPaint(hWnd,hDC,tempShader);break;
}
//获取首个子节点物体
Node<Object> *ptTempNode=ChildNodeList.ptNodeHead;
//循环执行各个子节点物体的消息处理函数
while(ptTempNode!=NULL)
{
if(ptTempNode->ptInstance!=NULL)
{
ptTempNode->ptInstance->DispatchPaintMessage(hWnd,hDC,tempShader,iPaintType);
}
//进行下一个节点
ptTempNode=ptTempNode->ptNextNode;
}
}
void Object::DispatchMessage(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{
//获取首个子节点物体
Node<Object> *ptTempNode=ChildNodeList.ptNodeHead;
//循环执行各个子节点物体的消息处理函数
while(ptTempNode!=NULL)
{
if(ptTempNode->ptInstance!=NULL)
{
ptTempNode->ptInstance->DispatchMessage(hWnd,message,wParam,lParam);
}
//进行下一个节点
ptTempNode=ptTempNode->ptNextNode;
}
//根据传入消息判断当前物体需要执行的消息处理函数
switch(message)
{
case WM_TIMER:OnTimer(hWnd,message,wParam,lParam);break;
case WM_MOUSEMOVE:OnMouseMove(hWnd,message,wParam,lParam);break;
case WM_LBUTTONDOWN:OnLButtonDown(hWnd,message,wParam,lParam);break;
case WM_LBUTTONDBLCLK:OnLButtonDblClk(hWnd,message,wParam,lParam);break;
case WM_LBUTTONUP:OnLButtonUp(hWnd,message,wParam,lParam);break;
case WM_RBUTTONDOWN:OnRButtonDown(hWnd,message,wParam,lParam);break;
case WM_RBUTTONDBLCLK:OnRButtonDblClk(hWnd,message,wParam,lParam);break;
case WM_RBUTTONUP:OnRButtonUp(hWnd,message,wParam,lParam);break;
case WM_MOUSEWHEEL:OnMouseWheel(hWnd,message,wParam,lParam);break;
case WM_KEYDOWN:OnKeyDown(hWnd,message,wParam,lParam);break;
case WM_KEYUP:OnKeyUp(hWnd,message,wParam,lParam);break;
case WM_CHAR:OnChar(hWnd,message,wParam,lParam);break;
case WM_IME_COMPOSITION:OnImeComposition(hWnd,message,wParam,lParam);break;
case WM_SIZE:OnSize(hWnd,message,wParam,lParam);break;
case WM_SETFOCUS:OnSetFocus(hWnd,message,wParam,lParam);break;
case WM_KILLFOCUS:OnKillFocus(hWnd,message,wParam,lParam);break;
}
}
4、简化后的游戏消息处理逻辑
由于采用了递归处理的方法遍历执行消息处理函数后,我们WndProc中的代码就变得非常简单。只用执行MyMainSence的DispatchPaintMessage和DispatchMessage两个消息分发函数即可,因为执行了这个函数,就相当于所有MyMainSence下所有的子物体,以及子物体的子物体,以及子子子子物体都会去相应相应的消息处理函数。
case WM_PAINT:
PAINTSTRUCT PS;
hDC=BeginPaint(hWnd,&PS);
//显示三维世界内容
......
//显示主要场景,不透明部分
MyMainSence.DispatchPaintMessage(hWnd,hDC,tempShader,0);
//显示主要场景,半透明部分
MyMainSence.DispatchPaintMessage(hWnd,hDC,tempShader,1);
......
ReleaseDC(hWnd,hDC);
return 0;
其他消息相应函数。
case WM_LBUTTONDOWN:
//向主场景及其子物体传递系统消息
MyMainSence.DispatchMessage(hWnd,message,wParam,lParam);
return 0;
case WM_RBUTTONDOWN:
//向主场景及其子物体传递系统消息
MyMainSence.DispatchMessage(hWnd,message,wParam,lParam);
return 0;
//其他消息相应函数处理分支
......
这样,我们今后只需要将所有游戏元素通过父子关系挂靠到MyMainSence物体下,我们就可以方便的执行对应子物体、子子物体的消息处理函数,充分利用类的概念将消息函数分配到具体的类中,而无需单独再WndProc中去处理消息函数。
5、准备一些实用的预制体
我们这个添加一些预制体,方便我们后期使用。为了操作方便,我么预先使用Blender模型制作软件制作一些简单的单位长度的模型,比如正方体、球体、胶囊体、椎体,这些在Blender模型软件中可以一键创建,随后导出为Obj模型文件,就可以使用我们的Mesh类进行加载,很容易就成了我们的预制体。我们在自定义具体的预制体类时,除了Mesh加载的模型文件不一样外,其他设置几乎是完全一样的,没有任何变化。
5.1、正方体
我们从GameObject类派生一个Cube正方体类,这个类具有一个Mesh网格用于我们从Obj模型数据文件中加载模型,另一个颜色变量用于保存模型自定义材质颜色。
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);
//启用着色器并进行渲染
tempShader->UseShader();
//显示网格
MainMesh.DrawMesh(hWnd,hDC,tempShader);
}
5.2、球体
由于我们已经通过Blender模型软件创建了正方体、经纬球、胶囊体、圆锥的模型,并导出为OBJ模型数据文件,我们可以直接生成自定义类,并通过Mesh类加载相应的模型文件即可。球体类和正方体类除了加载的模型文件不一样,其他基本没有变化。
class Cube:public GameObject
{
public:
//生成一个网格
Mesh MainMesh;
//颜色
glm::vec4 Color;
public:
Sphere();
void OnSolid3DPaint(HWND hWnd,HDC hDC,Shader *tempShader);
};
Sphere::Sphere()
{
//当前物体的名称
SetName("Sphere");
SetType("Sphere");
MainMesh.LoadMeshFromObjFile("Model\Sphere\Sphere.obj");
}
void Sphere::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);
//启用着色器并进行渲染
tempShader->UseShader();
//显示网格
MainMesh.DrawMesh(hWnd,hDC,tempShader);
}
5.3、胶囊体
添加胶囊体的预制体。
代码同上,进行改变加载模型的文件路径,显示效果见上图。
5.4、椎体
添加椎体的预制体。
代码同上,进行改变加载模型的文件路径,显示效果见上图。
5.5、法线问题优化
这里的球体、圆锥,出现了点小问题。例如,我在Blender中添加的经纬球模型,其显示效果放大后是这样的,球面存在明显的方格,这主要是由于Blender产生的经纬球顶点法线问题。圆被经纬线切分成的每个小四边形面的四个顶点法线都垂直于这个小四边形面,这样就导致后期片段着色器光照时,线性差值的法线不能很好的在不同面中进行平稳过渡。因此,我们球体上每个点的法线应该是球心到该顶点所在方向向量在进行单位化后的结果。
在进行法线修正后,球体的显示恢复了正常,显示效果如下。其实,我么也可以直接在Blender软件中直接修改法线,通过平滑着色和平直着色功能进行法线切换。
这部分内容我们将在下节法线可视化内容中详细了解。
5.6、各种预制体整体效果
添加完以上预制体后,我们就可以在后期演示一些基本功能时方便的使用它们。下图是所有自定义预制体的显示效果。
6、添加游戏坐标轴
在拥有了预制体后,我们可以使用椎体显示坐标轴的箭头,这样我们就可以显示较为真实的坐标轴。
class CoordinateAxis:public GameObject
{
public:
//设置坐标轴长度
float LineLength;
//设置线条的宽度
float LineWidth;
//生成三维坐标线段网格
Mesh LineMesh[3];
//坐标轴箭头
Mesh ArrowMesh;
public:
CoordinateAxis();
void OnSolid3DPaint(HWND hWnd,HDC hDC,Shader *tempShader);
void OnSolid2DPaint(HWND hWnd,HDC hDC,Shader *tempShader);
};
CoordinateAxis MyCoordinateAxis;
CoordinateAxis::CoordinateAxis()
{
//当前物体的名称
SetName("CoordinateAxis");
SetType("CoordinateAxis");
LineLength=5.0f;
LineWidth=2.0f;
//初始化坐标轴
if(true)
{
//顶点信息
std::vector<Vertex> vectices;
vectices.push_back(Vertex(glm::vec3(-LineLength,0.0f,0.0f),glm::vec3(0.0f,0.0f,0.0f),glm::vec2(0.0f,0.0f),glm::vec4(1.0f,0.0f,0.0f,1.0f)));
vectices.push_back(Vertex(glm::vec3(LineLength,0.0f,0.0f),glm::vec3(0.0f,0.0f,0.0f),glm::vec2(0.0f,0.0f),glm::vec4(1.0f,0.0f,0.0f,1.0f)));
//顶点索引信息
int tempIndices[]={0,1};
//初始化顶点索引编号
std::vector<GLuint> indices(tempIndices,tempIndices+sizeof(tempIndices));
//纹理信息
std::vector<Texture> textures;
//加载Mesh
LineMesh[0].LoadMesh(vectices,indices,textures);
}
//初始化坐标轴
if(true)
{
//顶点信息
std::vector<Vertex> vectices;
vectices.push_back(Vertex(glm::vec3(0.0f,-LineLength,0.0f),glm::vec3(0.0f,0.0f,0.0f),glm::vec2(0.0f,0.0f)));
vectices.push_back(Vertex(glm::vec3(0.0f,LineLength,0.0f),glm::vec3(0.0f,0.0f,0.0f),glm::vec2(0.0f,0.0f)));
//顶点索引信息
int tempIndices[]={0,1};
//初始化顶点索引编号
std::vector<GLuint> indices(tempIndices,tempIndices+sizeof(tempIndices));
//纹理信息
std::vector<Texture> textures;
//加载Mesh
LineMesh[1].LoadMesh(vectices,indices,textures);
}
//初始化坐标轴
if(true)
{
//顶点信息
std::vector<Vertex> vectices;
vectices.push_back(Vertex(glm::vec3(0.0f,0.0f,-LineLength),glm::vec3(0.0f,0.0f,0.0f),glm::vec2(0.0f,0.0f)));
vectices.push_back(Vertex(glm::vec3(0.0f,0.0f,LineLength),glm::vec3(0.0f,0.0f,0.0f),glm::vec2(0.0f,0.0f)));
//顶点索引信息
int tempIndices[]={0,1};
//初始化顶点索引编号
std::vector<GLuint> indices(tempIndices,tempIndices+sizeof(tempIndices));
//纹理信息
std::vector<Texture> textures;
//加载Mesh
LineMesh[2].LoadMesh(vectices,indices,textures);
}
//加载坐标轴箭头
ArrowMesh.LoadMeshFromObjFile("Model\Cone\Cone.obj");
}
void CoordinateAxis::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);
//设置线的宽度
glLineWidth(LineWidth);
//光照效果
light[0].tagEnable=0;
light[1].tagEnable=0;
//显示X轴直线
if(true)
{
//材质效果
material.tagEnable=1;
material.diffuse=glm::vec4(1.0f,0.0f,0.0f,1.0f);
//启用着色器并进行渲染
tempShader->UseShader();
//显示网格
LineMesh[0].DrawMesh(hWnd,hDC,tempShader,GL_LINES);
}
//显示Y轴直线
if(true)
{
//材质效果
material.tagEnable=1;
material.diffuse=glm::vec4(0.0f,1.0f,0.0f,1.0f);
//启用着色器并进行渲染
tempShader->UseShader();
//显示网格
LineMesh[1].DrawMesh(hWnd,hDC,tempShader,GL_LINES);
}
//显示Z轴直线
if(true)
{
//材质效果
material.tagEnable=1;
material.diffuse=glm::vec4(0.0f,0.0f,1.0f,1.0f);
//启用着色器并进行渲染
tempShader->UseShader();
//显示网格
LineMesh[2].DrawMesh(hWnd,hDC,tempShader,GL_LINES);
}
//光照效果
light[0].tagEnable=1;
//显示箭头X轴
if(true)
{
//保存模型矩阵入栈
PushModelMatrix(ModelMatrix);
//设置材质颜色
material.diffuse=glm::vec4(1.0f,0.0f,0.0f,1.0f);
material.specular=glm::vec4(0.0f,0.0f,0.0f,1.0f);
ModelMatrix=glm::translate(ModelMatrix,glm::vec3(LineLength,0.0f,0.0f));
ModelMatrix=glm::rotate(ModelMatrix,-PI/2,glm::vec3(0.0f,0.0f,1.0f));
ModelMatrix=glm::scale(ModelMatrix,glm::vec3(0.3f,0.3f,0.3f));
tempShader->UseShader();
ArrowMesh.DrawMesh(hWnd,hDC,tempShader,GL_TRIANGLES);
//恢复模型矩阵出栈
ModelMatrix=PopModelMatrix();
}
//显示箭头Y轴
if(true)
{
//保存模型矩阵入栈
PushModelMatrix(ModelMatrix);
//设置材质颜色
material.diffuse=glm::vec4(0.0f,1.0f,0.0f,1.0f);
material.specular=glm::vec4(0.0f,0.0f,0.0f,1.0f);
ModelMatrix=glm::translate(ModelMatrix,glm::vec3(0.0f,LineLength,0.0f));
ModelMatrix=glm::scale(ModelMatrix,glm::vec3(0.3f,0.3f,0.3f));
tempShader->UseShader();
ArrowMesh.DrawMesh(hWnd,hDC,tempShader,GL_TRIANGLES);
//恢复模型矩阵出栈
ModelMatrix=PopModelMatrix();
}
//显示箭头Z轴
if(true)
{
//保存模型矩阵入栈
PushModelMatrix(ModelMatrix);
//设置材质颜色
material.diffuse=glm::vec4(0.0f,0.0f,1.0f,1.0f);
material.specular=glm::vec4(0.0f,0.0f,0.0f,1.0f);
ModelMatrix=glm::translate(ModelMatrix,glm::vec3(0.0f,0.0f,LineLength));
ModelMatrix=glm::rotate(ModelMatrix,PI/2,glm::vec3(1.0f,0.0f,0.0f));
ModelMatrix=glm::scale(ModelMatrix,glm::vec3(0.3f,0.3f,0.3f));
tempShader->UseShader();
ArrowMesh.DrawMesh(hWnd,hDC,tempShader,GL_TRIANGLES);
//恢复模型矩阵出栈
ModelMatrix=PopModelMatrix();
}
}
我们这里为了能对模型矩阵操作方便,采用了类似glPushMatrix和glPopMatrix的操作,单与系统函数不同的是,我们这里的操作矩阵只包括模型矩阵,不包括视图矩阵。这里我们采用STL的自带stack栈来操作,但要注意在出栈时要对栈进行empty()是否为空的判断,否则有可能导致内存操作失败和程序崩溃。
//用栈保存模型矩阵
std::stack<glm::mat4> ModelMatrixStack;
//用栈保存模型矩阵,类似于glPushMatrix功能
glm::mat4 PushModelMatrix(glm::mat4 tempModelMatrix)
{
ModelMatrixStack.push(tempModelMatrix);
return tempModelMatrix;
}
//用栈读取模型矩阵,类似于glPopMatrix功能
glm::mat4 PopModelMatrix()
{
//初始化
glm::mat4 tempModelMatrix=glm::mat4(1.0f);
//不为空的情况下弹出并返回矩阵
if(ModelMatrixStack.empty()==false)
{
tempModelMatrix=ModelMatrixStack.top();
ModelMatrixStack.pop();
}
return tempModelMatrix;
}
7、添加游戏帧显示控件
游戏帧数是指游戏运行时每秒所运行的帧数(简称FPS,Frames Per Second)和视频一样,FPS越大,在屏幕上的视频就越来越平滑,直到一个临界点(大约是100FPS),超过这个临界点,再高的FPS都只是一个令人惊奇的数值,400FPS和100FPS在人的视觉中几乎没有差别。一般游戏都是40左右fps就可以称之为流畅了。比如策略类(三国志什么的)游戏帧5fps也是可以接受的,但赛车类5fps根本玩不下去。
//统计游戏显示画面的刷新帧数
class GameFrame:public GameObject
{
public:
//存储不同帧率下的延迟调整时间
float DeltaTime;
private:
//存储帧数计算的运行次数
DWORD iFrameRateCounter;
//存储帧数计算的开始时间
DWORD iFrameStartTime;
//存储当前帧数并显示
DWORD iFrameRateValue;
public:
GameFrame();
void OnSolid2DPaint(HWND hWnd,HDC hDC,Shader *tempShader);
};
GameFrame MyGameFrame;
这里加入一个DeltaTime变量。DeltaTime是一个非常重要的变量,它表示从上一帧到当前帧所经过的时间(以秒为单位)。这个值通常用于确保游戏逻辑的更新与帧率无关,从而实现平滑且一致的游戏体验。简单用公式来说,游戏帧数*DeltaTime=1秒。具体来说,DeltaTime 的值取决于当前帧的渲染速度。如果帧率高(即每秒渲染的帧数多),那么DeltaTime 的值就会较小;如果帧率低,那么DeltaTime 的值就会较大。但无论帧率如何变化,DeltaTime 都会自动调整,以确保游戏逻辑按照预期的速度执行。
使用 DeltaTime的一个常见场景是在物体的移动中。例如,如果你想让一个物体每秒移动1个单位,这里1.0f表示物体应该沿着前方移动1个单位,而乘以DeltaTime (即1.0f*DeltaTime)则确保了无论帧率如何,物体都会以每秒1个单位的速度移动。如果帧率很高,DeltaTime会很小,所以物体的移动量会很小,但1秒内移动的次数多;如果帧率很低,DeltaTime 会很大,但1秒内移动的次数少,物体的总移动量仍然会接近1个单位,从而保持速度的一致性。
GameFrame::GameFrame()
{
SetName("GameFrame");
SetType("GameFrame");
DeltaTime=0;
iFrameStartTime=0;
iFrameRateValue=0;
iFrameRateCounter=0;
}
void GameFrame::OnSolid2DPaint(HWND hWnd,HDC hDC,Shader *tempShader)
{
//当时间超过一秒后进行帧率计算并重置
if(timeGetTime()-iFrameStartTime>1000)
{
//获取当前统计的帧率
iFrameStartTime=timeGetTime();
iFrameRateValue=iFrameRateCounter;
//重置帧数计数器
iFrameRateCounter=0;
}
//累计一秒内运行的次数
iFrameRateCounter++;
//设置延迟调整时间
if(iFrameRateValue!=0){DeltaTime=1.0f/iFrameRateValue;}
//显示帧率和延迟调整时间
if(true)
{
//获取窗口大小
RECT tempClientRect;
GetClientRect(hWnd,&tempClientRect);
//设置文本颜色
glColor3f(1,1,0);
//显示帧数
char szTemp[1024]="";
sprintf(szTemp,"%0.3f %d",DeltaTime,iFrameRateValue);
//获得文字宽度和高度,调整字符串显示对齐方式
SIZE fontSize;
GetTextExtentPoint(hDC,szTemp,strlen(szTemp),&fontSize);
glWindowPos2f(tempClientRect.right-10-fontSize.cx,tempClientRect.bottom-20);
drawString(hDC,szTemp);
}
}
我们要密切关注程序运行过程中帧数的变化,如果在游戏开发过程中帧数出现了大幅的波动,或者持续降低,那说明我们的资源加载或者是运行逻辑出了问题,就需要特别注意,排查问题的所在。
8、场景完善:奇幻森林雏形
我们此前的模型加载的较为单调,而且我们将加入一些游戏的模型元素,这个视频加载了各式各样的树木,构造出一个奇幻的森林雏形。我们主场景的类可以扩展一下:
class MainSence:public Object
{
public:
MainSence();
void OnSolid2DPaint(HWND hWnd,HDC hDC,Shader *tempShader);
void OnRButtonDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
void OnLButtonDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);
};
MainSence MyMainSence;
这样,当我们点击鼠标左键将随机添加预制体,预制体包括正方体、球体、胶囊体、椎体,位置被设置在半径为10的圆形范围内;当我们点击鼠标右键则随机添加树木模型,树木的的位置被设置在半径为15的圆形范围内,树木的缩放大小为0.5倍。这个随着树木的增加,我们魔幻森林的雏形就形成了。
MainSence::MainSence()
{
SetName("MainSence");
SetType("MainSence");
}
void MainSence::OnSolid2DPaint(HWND hWnd,HDC hDC,Shader *tempShader)
{
//显示节点调试信息
if(true)
{
//设置文字颜色
glColor3f(1.0f,0.0f,0.0f);
//获取窗口大小
RECT tempClientRect;
GetClientRect(hWnd,&tempClientRect);
float y=tempClientRect.bottom-20;
//显示调试信息
//y=ShowNodeStatistic(hWnd,hDC,0,y);
//y=ShowObjectStatistic(hWnd,hDC,0,y);
y=MyMainSence.ShowAllChildNodeList(hWnd,hDC,0,y);
}
}
void MainSence::OnRButtonDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{
if(true)
{
//新动态创建一个物体,并把该物体添加到主场景的子物体列表中
MeshObject *ptTempGameObject=new MeshObject();
//创建成功后设置一些属性,如位置信息
if(ptTempGameObject!=NULL)
{
//加载不同的模型
switch(rand()%6)
//switch(5)
{
case 0:ptTempGameObject->MainMesh.LoadMeshFromObjFile("Model\CoconutTree\CoconutTree.obj","Model\CoconutTree\CoconutTree.png","Model\CoconutTree\CoconutTree.png");break;
case 1:ptTempGameObject->MainMesh.LoadMeshFromObjFile("Model\Main_Pallete_Tree02\Main_Pallete_Tree02.obj","Model\Main_Pallete_Tree02\Main_Pallete.png","!Model\Main_Pallete_Tree02\Main_Pallete.png");break;
case 2:ptTempGameObject->MainMesh.LoadMeshFromObjFile("Model\FantasyTree\FantasyTree.obj","Model\FantasyTree\FantasyTree.png","Model\FantasyTree\FantasyTree.png");break;
case 3:ptTempGameObject->MainMesh.LoadMeshFromObjFile("Model\FantasyTree\FantasyTree2.obj","Model\FantasyTree\FantasyTree.png","Model\FantasyTree\FantasyTree.png");break;
case 4:ptTempGameObject->MainMesh.LoadMeshFromObjFile("Model\FantasyTree\FantasyTree3.obj","Model\FantasyTree\FantasyTree.png","Model\FantasyTree\FantasyTree.png");break;
case 5:ptTempGameObject->MainMesh.LoadMeshFromObjFile("Model\Tree5\Tree5.obj","Model\Tree5\Tree5.jpg","Model\Tree5\Tree5.jpg");break;
}
//标记为动态创建,后期删除子物体时可自动删除释放内存
ptTempGameObject->SetDynamicCreateStatus();
//添加为子物体
MyMainSence.AddChild(ptTempGameObject);
//设置新创建物体的位移、旋转、缩放信息
ptTempGameObject->RelativeTransform.Position=glm::vec3(15.0f-(rand()%20)*1.5f,0.0f,15.0f-(rand()%20)*1.5f);
ptTempGameObject->RelativeTransform.Rotate=glm::vec3(0.0f,PI*(rand()%360)/180.0f,0.0f);
ptTempGameObject->RelativeTransform.Scale=glm::vec3(0.5f,0.5f,0.5f);
}
}
}
void MainSence::OnLButtonDown(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{
if(true)
{
//新动态创建一个物体,并把该物体添加到主场景的子物体列表中
GameObject *ptTempObject=NULL;
//动态创建一些预制体
switch(rand()%4)
{
case 0:ptTempObject=new Sphere();break;
case 1:ptTempObject=new Capsule();break;
case 2:ptTempObject=new Cube();break;
case 3:ptTempObject=new Cone();break;
}
//创建成功后设置一些属性,如位置信息
if(ptTempObject!=NULL)
{
//标记为动态创建,后期删除子物体时可自动删除释放内存
ptTempObject->SetDynamicCreateStatus();
//添加为子物体
MyMainSence.AddChild(ptTempObject);
//设置新创建物体的位移、旋转、缩放信息
ptTempObject->RelativeTransform.Position=glm::vec3(10.0f-(rand()%20)*1.0f,10.0f-(rand()%20)*1.0f,10.0f-(rand()%20)*1.0f);
ptTempObject->RelativeTransform.Rotate=glm::vec3(PI*(rand()%360)/180.0f,PI*(rand()%360)/180.0f,PI*(rand()%360)/180.0f);
}
}
}
当我们点击鼠标🖱右键,就会在主场景中添加一个树木模型,由于我们在添加物体的同时,也随机设置了物体的位置和旋转角度,因此树木模型出现在不同的位置,模型多了以后便形成了一个森林的雏形。同时,我们的森林在平行光的照射下也产生了光照效果,光照面较亮,背光面较暗些。
我们再来看下自定义预制体和树木模型同框的效果。