从0开始搭建FPS游戏引擎 (附源代码)【OpenGL 您所在的位置:网站首页 xboxone老版 从0开始搭建FPS游戏引擎 (附源代码)【OpenGL

从0开始搭建FPS游戏引擎 (附源代码)【OpenGL

2023-03-02 21:02| 来源: 网络整理| 查看: 265

目录 项目简介目标准备GLADGLFWglmstb_imageassimp 窗口 int main()基础配置窗口回调 着色器 Shader着色器简介从文本文件中读取着色器编译着色器 纹理 Texture加载纹理配置并绑定纹理 模型 Mesh加载模型绘制模型 RendererGame摄像机 Camera用户参数 ResourceManager角色属性 Character开始组装GameObject!动态加载参数射击碰撞大功告成!未实现的功能

项目简介

大家好,我是冤种五月。

由于《初级软件实作》课程的需要,加上自己对渲染感兴趣,前段时间学了OpenGL,所以想着用OpenGL设计一个软件。老师给了我一个选题,是FPS游戏引擎。

写引擎的过程中我疯狂踩坑。

首先要谢谢learnOpenGL-cn,没有它就没有我前期的快速学习;谢谢它,更新了前面6章的代码,没更新第7章的代码;更谢谢我自己,学习不认真,以至于我两个版本的代码混用而不自知,浪费了一个月时间自闭。

所以,建议学习OpenGL的你,前期参考learnOpenGL-cn,第7章开始参考learnOpenGL(英文原版)。冷知识,learnOpenGL不需要科学上网,但是科学上网才可以看到中文译版和英文原版评论区(评论区里有很多前辈已经帮我们踩坑了)。

于是我决定写下这篇博客,跟大家分享我开发过程中的心得。

博客主要是分享思路大纲,如果大家对具体的知识感兴趣,可以点开链接过去看。 请添加图片描述在这里插入图片描述

目标

加载玩家自定义的FPS的战斗场景,包括总体空间(一个包裹着整个游戏环境的长方体,其大小应由游戏配置文件来定义)、里面的各种障碍物/躲避物(立方体、或长方体即可);

加载玩家自定义的各种敌人:包括其位置和特性(角色形象可自定);

加载玩家自定义属性值的血包、弹药包。

允许玩家射击、拾取

参数由用户自定义

准备

在开始造房子之前,我们需要先准备好建材。

GLAD

glad是用来访问OpenGL规范接口的第三方库,实现代码跨平台。配置教程看这里

需要理解的是,OpenGL中许多缓冲区的分配、对象的开辟,都是用一个无符号整型数id来进行引用和值传递。

glad库是glew库的升级版。老版的learnOpenGL代码就是用glew库写的,新版用的是glad。

GLFW

全称Graphics Library Framework,主要是用来创建并管理窗口和OpenGL上下文的图形库框架。

配置教程看这里;想查常用函数看看这里

glm

全称OpenGL Mathematics,是基于GLSL规范的图形软件的数学库。

摄像机类的矩阵变换、着色器mvp矩阵的赋值,都离不开它。

因为glm库的实现都写在头文件中,所以不需要编译成库。下载链接在这里。

stb_image

用于加载纹理的库。教程在[这里](https://learnopengl-cn.github.io/01 Getting started/06 Textures/#stb_imageh)。

这个库只有一个头文件stb_image.h,用的时候直接包含进去就行了,此时只能用加载和销毁函数。如果考虑到其它更高级的图片加载函数和配置,就需要在包含头文件之前加一行定义#define STB_IMAGE_IMPLEMENTATION。

打开VS,观察一下头文件会发现,如果不加这个定义,stb_image.h源码中#ifdef STB_IMAGE_IMPLEMENTATION下面的代码都是灰色的。我不懂,网上的说法是这样加一行之后,预处理器会修改头文件,让其只包含相关的函数定义源码,相当于把.h文件变成.cpp文件。

老版的learnOpenGL的纹理加载方法是用SOIL库写的,但是这个库比较老了,而且有些mac电脑用不了。

assimp

全称Asset Importer Lib,用于加载和处理各种3D模型。配置和使用方法在这里。

注意要把assimp-vc143-mtd.dll放到与工程生成的.exe文件相同的目录下

窗口 int main()

在准备部分有提到,我们会用glad库配置OpenGL的接口,用glfw配置渲染窗口,有印象嘛?

这是每个OpenGL都离不开的步骤,是最基础的。

所以我的小引擎第一件事就是先把窗口创建出来。 请添加图片描述

基础配置

思路是:先导库(glad,glfw,iostream)-> 初始化glfw渲染窗口 -> 初始化glad函数上下文 -> 配置OpenGL -> 空出位置配置其它素材 -> 编写渲染循环:响应用户输入,更新数据,渲染 -> 回收工作

窗口回调

考虑到要实现glfw窗口回调。也就是我希望,当我按下按键Esc时,glfw配置的窗口能知道并响应我们的行为,或者当我们拖拽窗口边界时,窗口大小能重新调整。因此我需要增加响应函数。

// 声明函数 void framebuffer_size_callback(GLFWwindow* window, int width, int height); // 配置窗口(帧缓冲)大小的函数 void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode); // 响应用户键盘输入的函数 ... // glfw: 窗口回调函数 glfwSetKeyCallback(window, key_callback); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); ... // 实现函数 void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) { // 当用户按下Esc键时,关闭程序 if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // 确保视口匹配新的窗口尺寸 // 注意宽度和高度将明显大于视网膜显示器上指定的高度 glViewport(0, 0, width, height); } 着色器 Shader 着色器简介

可以这么简单粗暴地理解:着色器就是一个电子画家,当你教会他画画的套路(着色器代码)之后,他指哪(图元)画哪(渲染)。

在这一节中,我只是测试了让着色器把图元填充成蓝色。

使用着色器有三件事要做:第一件事,从读取着色器代码,第二件事,配置参数、编译代码,第三件事,激活代码。接下来我一一说明。

请添加图片描述

从文本文件中读取着色器

根据单一职责原则,最好设计一个类专门负责读取代码,一个类专门负责编译代码。

又考虑到将来我们还要加载纹理和模型,还要管理他们,因此我先编写了一个ResourceManager类。它的基本架构是,设置一个map字典,用名字查找对应的着色器。增加Load函数实现加载文件,Get函数获取着色器对象,Clear函数完成回收工作。Manager类一般是单例类,构造函数私有,成员函数全为静态,函数返回值一般是对应资源的引用。

编译着色器

剩下的事就交给Shdaer类完成啦!

以顶点着色器为例(其实片元着色器也是必须的,只是便于理解我先不写进去),编译的上下文如下:

unsigned int sVertex; sVertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(sVertex, 1, &vertexSource, NULL);// 链接代码 glCompileShader(sVertex); // 编译着色器 checkCompileErrors(sVertex, "VERTEX"); // 检查顶点着色器的编译错误 ··· // 绑定着色器程序到id this->ID = glCreateProgram(); // 开辟缓存空间(也可以理解为新建一个OpenGL的着色器对象),返回空间的id glAttachShader(this->ID, sVertex); // 根据id,为着色器绑定顶点着色器部分 ··· glLinkProgram(this->ID); // 链接程序 checkCompileErrors(this->ID, "PROGRAM"); // 检查整个着色器的编译错误 // 删除着色器(因为它们现在已经被链接到我们的程序了,不再需要了) glDeleteShader(sVertex);

激活着色器的上下文非常简单:

glUseProgram(this->ID);

每次渲染模型之前,都要激活着色器。

s.Use(); //激活着色器 glBindVertexArray(quadVAO); //绑定图元 glDrawArrays(GL_TRIANGLES, 0, 6); //画它 glBindVertexArray(0); //解绑 纹理 Texture

请添加图片描述

参考教程在这里。

加载纹理

在ResourceManager类中,开始用上了stb_image库,只用了两个函数,无敌:

// 加载图像数据(是char数组) int width, height, nrChannels; unsigned char* data = stbi_load(file, &width, &height, &nrChannels, 0); // 交给纹理对象,生成纹理 texture.Generate(width, height, data); // 释放图像数据 stbi_image_free(data);

由于图片的y轴原点在左上角,OpenGL的纹理原点在左下角,所以加载纹理之前要先配置一下:

stbi_set_flip_vertically_on_load(true);//翻转图片y轴

用这个函数要记得先定义宏:

#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" 配置并绑定纹理

这部分是OpenGL上下文的事了,包含glad库即可。

新增Texture2D类,绑定纹理glBindTexture(GL_TEXTURE_2D, this->ID);,使用glTexParameteri(···)配合各种枚举配置纹理参数。为了减少空间的开销,在配置好纹理之后,不渲染时,程序会解绑纹理glBindTexture(GL_TEXTURE_2D, 0);。

踩坑:做测试的时候,因为一行代码太长,命名上下不一致都没发现,浪费了半小时时间debug

测试:

s.Use(); //激活着色器 glActiveTexture(GL_TEXTURE0); // 激活纹理缓冲 t.Bind(); // 绑定问题 glBindVertexArray(quadVAO); //绑定图元 glDrawArrays(GL_TRIANGLES, 0, 6); //画它 glBindVertexArray(0); //解绑 模型 Mesh

请添加图片描述

assimp库闪亮登场!依旧是沿用ResourceManager类加载文件,Mesh类处理OpenGL上下文的架构。参考教程在这里。

加载模型

使用assimp库自带的导入器

Assimp::Importer importer; const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);

按教程所说,一个模型所导出的scene对象,背后是一个树结构,从根结点出发,遍历每个子节点,就可以得到网格Mesh。所有网格Mesh组成一个模型Model,可以说typedef vector Model。我们将来在Render类渲染的时候,也是一个一个网格渲染的。

绘制模型

创建一个Mesh对象同样需要准备顶点数组和索引数组,这将是构造函数必备的参数。接着,构造函数会以此初始化其它gl上下文,并得到一个顶点数组对象的引用VAO。绘制模型时,同样仅需三句代码:

glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, static_cast(indices.size()), GL_UNSIGNED_INT, 0); glBindVertexArray(0); Renderer

完成了基础的渲染,是时候把这些代码从main函数封装起来了!

首先是渲染部分,我要定义一个Renderer类,把着色器、纹理贴图、模型三种资源集合起来,在有需要的时候就调用它的成员函数Draw(),调动这些资源完整地绘制出图像。

另外我还会定义一个负责配置Shader参数的函数void setPbrShaderParameters(glm::vec3 albedo, float metallic, float roughness, float ao);。可惜的是,本项目中的PBR着色器是不正确的。

Game

一个游戏不仅仅涉及渲染,它还要接收用户的设备输入,要计算众多数值。这些事情都塞在main函数里是不合适的,毕竟main函数已经承担了配置窗口等脱不开身的工作。

所以,我还需要定义一个Game游戏类,它的成员属性包含了游戏中所需的各种参数和状态变量,成员函数包含了初始化函数void Init();,处理输入的函数void ProcessInput(float dt);,更新参数函数void Update(float dt);,渲染函数void Render();。

在这一阶段我的任务仅需复制粘贴,确保搬运到类中的渲染代码能正常执行即可。而之后的每个阶段,就多多少少都涉及对Game类的改动。

摄像机 Camera

摄像机类Camera的创建涉及MVP矩阵变换,教程在这里。

在作业中,我用到的摄像机是经过小改的FPS摄像机,禁止上下移动。我需要在game类中定义一个camera,初始化它。在game类的ProcessInput函数中处理摄像机的移动,在Update函数中获取视口矩阵camera.GetViewMatrix()并以此赋值给着色器。

现在我可以通过WASD键和鼠标来移动视角啦!

请添加图片描述

用户参数 ResourceManager

随着代码量的增多,可调的参数也越来越多,例如模型的变换。那么现在可以开始考虑嵌入自定义参数了。

我在ResourceManager类中新增了参数结构体Parameter,并添加了从文件中读取参数的函数static Parameter LoadParameter();。

角色属性 Character

简单起见,我为角色定义了两个属性:生命值hp和攻击力atk。另外还定义了一个bool值isDeath。

到了这一步类越来越多了,可能读者会犯迷糊。大家可以联想一下unity的GameObject是怎么设计的:一个3D游戏的GameObject一般包含Transform组件,Mesh Filter组件,Mesh Render组件,CScript自定义脚本组件。前三者被我规范成了我的Renderer类,最后一者就是这个Character类。

我还为角色类编写了改变hp和atk的函数void modifyHp(float value)和void modifyAtk(float value)。并且,当hpposition.x, 0.0, o->position.y) - p;,将方向向量与摄像机的前向向量进行点乘,若结果大于0,说明两者朝向相同,否则朝向相反。当大于0.5时,不能朝前走,小于-0.5时不能朝后走。左右同理。

其实这个算法不是完全正确的,只能实现阻挡,不能实现被迫挡着导致斜着走。

大功告成!

请添加图片描述

前期配置环境我走了很多弯路,消耗了太多的时间,后期也没有完善的力气了,就这样吧quq

代码已开源在GitHub,欢迎有作业需要的同学把它clone下来并改进~

有任何疑问或建议,欢迎在评论区留意告诉我XD

接受一切批评指正

未实现的功能 敌人AIPBR渲染用户错误配置时的参数的异常报错更多拓展玩法……


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有