面向对象-简单游戏项目

抽象化的代码架构

面向对象可是C++区别于C的核心优势,最核心的好处是能提高代码复用。LearnOpenGL在每个单元后面都有一个类。

一个基于OpenGL的C++游戏项目,最开始肯定是把所有的代码全放在一个main.cpp里。到后面知道了要将一些重复出现的代码分离,定义成一个函数。在了解了面向对象之后,终于不走弯路,把所有函数都写在类里面。

最开始写的OpenGL项目是main+函数+部分类的形式。类化的代码多为导入的第三方库和高度封装这些库的类,main函数里面要搞定帧循环。而面向对象的终极目标,应该是main里相当简洁,只有一个类的初始化和结束,就像下面:

1
2
3
4
5
6
7
8
#include <application.h>

int main(){
    Application app;
    app.run();
    app.exit();
    return 0;
}

main.cpp 里没有帧循环,程序全放在 application 类里。

很容易写的类

OpenGL项目,特别是游戏项目,一些类在早期就能写出来。

camera.h

摄像机的类当然相当简单:

  1. 记录 自身的世界坐标系位置Pos,上方向向量 Front,右方向向量以及欧拉角。

  2. 设置一些函数可以 接收参数来 改变 这些值。

  3. 设置一个函数,最终 返回 我们最想要的观察矩阵。

这个类的目标是很明确的,最终是返回一个矩阵。有了这个目标,设置 privatemethods 就很自然了。

InputController.h

这个类是与输入设备交互的类,要处理玩家的输入。如果有做过后端项目的话,就知道java web里有一个 Interceptor 类可以拦截所有的 Http 请求,由请求的内容跳转不同的逻辑。

仿照这个java web的思路, InputController类应当处理所有的输入,并且在帧循环一开始就拦截,有且仅有一个拦截器,这样才不会乱套。

controller控制谁呢?我觉得控制摄像机也行,控制玩家也可以。不用切换人称视角的话,直接控制摄像机更好些。

window.h

这个类的功能就很单一了:初始化glfw、glad,保存window指针。我们的项目里还记录了window的大小,以及设置调整窗口的函数。

渲染逻辑

在渲染画面的相关代码上,我们也走过不少弯路。一开始所有的VAO/VBO,shader全写在main里,导致main函数相当臃肿。后面还有各种模型model,网格mesh,动画animation,必须使用面向对象的思想。

渲染画面部分的类要写好,就必须对渲染的流程十分了解。

1.画面是一帧一帧生成的,因此有了帧循环。

2.渲染初期要指定shaderPragram,而shaderPragram要提前编译好,编译时至少要指定vertexShader、framShader,于是就有了 shader.h 类。

3.模型的加载需要处理多个mesh,加载模型文件遍历读取节点,构造mesh,于是有了 model.h ,用来导入模型。这个model对象是一次加载,可多次复用的。

4.一个场景包含了多个模型对象,这些模型对象保存了自身的属性(大小,位置,朝向,其他状态),游戏逻辑可以直接增删改查这些对象。于是有了 scene.h,它保存了一帧里要绘制的模型对象,自身不渲染画面。

5.scene对象需要在帧循环里渲染出来。遍历scene里所有的模型对象,分别查看对象是否要绘制,绘制时用的 model对象,用哪个 shader 对象,传入shader相关参数。这个过程就可以写成 Renderer 类。Render类只接收scene等对象,只管渲染画面,不改变其他对象。

6.既然 scene类存的是模型对象,那么需要一个类来保存这个模型。model 类确切来说是模型加载类,只是加载模型的数据,这个数据是复用的。那么一个实例化的人物,他当然对应一个 model对象,同时还有位置,朝向,血量等,这些可以封装进一个类,Object.h。这个类应当记录用哪个 model,对应的shader,大小、位置、朝向,可见性等。这样,scenne里保存的就不是model对象,而是object。游戏逻辑里影响object对象,scene里保存object的引用,并在renderer里渲染出来。

游戏逻辑

这部分的代码结构,我确实想了好久,一直犹豫不决。因为我不知道从哪开始,输入什么,改变什么,最后输出什么。

之前的类都比较简单,采用的是 自底向上 的思路。加载着色器程序麻烦,有了 shader;加载模型困难,有了model,shader包含在内;model则被object包含…不断地从底层向上抽象。

换成游戏逻辑的部分,情况就很不同,不知道从哪下手,总觉得每个地方入手都不够底层,害怕会有历史遗留问题。后来我想通了,那不妨转换思路,改成 自顶向下 ,从上层一步步往下,只写抽象的伪代码,不断的分解需求,直到不能再分解为止。

这个过程我就不断地进行状态模拟。

Licensed under CC BY-NC-SA 4.0
最后更新于 2026年1月4日 00:14
使用 Hugo 构建
主题 StackJimmy 设计