0. 概述

参考资料:

设想目标和实现目标

目标 实现状态
基于 OpenGL 的方块渲染 DONE
第一人称视角控制 DONE
射线检测 DONE
人物与世界交互 DONE
仿 MC 世界控制(放置方块/挖掘方块) DONE
不同特性的方块 只做了不同皮肤
人物渲染 orz
物理系统 orz
世界随机生成算法 orz

总的来说,从类的设计到具体实现都很不专业,还有很大的改进空间
github 项目地址 | MineCraft_Yishiyu_Edition

效果展示:
(只实现了石头方块和草方块,左键挖掘右键放置)

1. 第三方库

1.1 OpenGL

[OpenGL 是什么 | 陈嘉栋 | 知乎]
OpenGL 是 Khronos Group 开发维护的一个规范,它主要为我们定义了用来操作图形和图片的一系列函数的 API,需要注意的是 OpenGL 本身并非 API.GPU 的硬件开发商则需要提供满足 OpenGL 规范的实现,这些实现通常被称为“驱动”,它们负责将 OpenGL 定义的 API 命令翻译为 GPU 指令.

简而言之 OpenGL 是一个绘图引擎,同为绘图引擎的还有微软开发的 DirectX 系列,Qt 引擎等
OpenGL 的优势在于其跨平台,但是其已经逐渐过时(Vulkan 是其指定接班人).不过关于 OpenGL 的教程和资料比较多,可以绘图引擎的入门之选

然后是 opengl 和 opencv 之间的关系

  • opengl:连接程序和显卡驱动的库(知道数据,渲染出图像)
  • opencv:图形处理库(已知图像,从中提取信息)

安装方法:安装支持 OpenGL 的显卡驱动即可

1.2 vcpkg

vcpkg 是 windows 上的 c++包管理系统(类似于 python 中的 pip,linux 上的 apt-get(?))
VisualStudio 可以很方便地使用 vcpkg 安装的包

安装方法vcpkg 安装:

1
2
3
4
5
6
7
8
9
10
11
# 克隆项目
git clone https://github.com/Microsoft/vcpkg.git

# build
.\vcpkg\bootstrap-vcpkg.bat

# 添加对VS的支持
vcpkg integrate install

# 使用vcpkg安装包
vcpkg install [packages to install]

后面的包安装步骤中的

1.3 GLAD

GLAD:[链接 | LearnOpenGL-CN]

首先 opengl 并不是一个库,而是一个 API 接口标准,不同的显卡厂商实现其具体函数 GLAD 是一个负责找到并初始化 opengl 具体实现的库

1.4 SFML

SFML(Simple and Fast Multimedia Library):[SFML | 官方网站]

一个轻量级多媒体库,支持窗口控制,音频控制,系统控制,图形渲染,网络通信

2. 渲染系统

2.1 整体结构

完善了渲染系统之后,针对每个模型(Model)及其对应的纹理(TextureAtlas)进行渲染(为了实现不同的方块)

整体结构:

  • RenerMaster:游戏渲染管理器,每帧由 Application 实例调用一次其 finishRender 函数
    • drawQuad:绘制单独的一个四边形(由其成员变量 QuadRenderer 实例完成)
    • drawCube:绘制一个方块(由成员变量 CubeRenderer 实例实现)
    • drawSection:渲染一个游戏区块(区域)(由成员变量 SectionRenderer 实例完成)
  • QuadRenderer/CubeRenderer/SectionRenderer:四边形/方块/区块渲染器
    • add:添加一个被渲染的四边形/方块/区块模型
    • render:执行一次渲染

2.2 OpenGL 渲染

以最方块渲染类 CubeRenderer 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//
// Created by Yishiyu on 2020/11/3.
//

#ifndef MINECRAFT_YISHIYU_EDITION_CUBERENDERER_H
#define MINECRAFT_YISHIYU_EDITION_CUBERENDERER_H

#include <vector>
#include "src/Model/Model.h"
#include "../Maths/glm.h"
#include "../Shaders/BasicShader.h"
#include "../Texture/BasicTexture.h"
#include "../Texture/TextureAtlas.h"

class Camera;

// 方块渲染类
// 持有一个方块模型,一个基础着色器,一个基础贴图
// 1. 可以添加模型的渲染位置(世界坐标)
// 2. 可以根据一个相机渲染出画面
class CubeRenderer {
public:
CubeRenderer();

void add(const glm::vec3 &position);

void render(const Camera &camera);

private:
std::vector<glm::vec3> m_quads;

Model m_cubeModel;
BasicShader m_shader;
BasicTexture m_basicTexture;

TextureAtlas m_atlasTest;
};


#endif //MINECRAFT_YISHIYU_EDITION_CUBERENDERER_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//
// Created by Yishiyu on 2020/11/3.
//

#include "CubeRenderer.h"

#include <iostream>

#include "../Camera.h"
#include "../Maths/Matrix.h"

CubeRenderer::CubeRenderer()
: m_atlasTest("DefaultPack") {
// 加载纹理集
m_basicTexture.loadFromFile("test");

// 模型顶点坐标(模型坐标系)(原点为0的右手坐标系,直面z轴正方向)
std::vector<GLfloat> vertexCoords
{
//Back
1, 0, 0,
0, 0, 0,
0, 1, 0,
1, 1, 0,

//Front
0, 0, 1,
1, 0, 1,
1, 1, 1,
0, 1, 1,

//Right
1, 0, 1,
1, 0, 0,
1, 1, 0,
1, 1, 1,

//Left
0, 0, 0,
0, 0, 1,
0, 1, 1,
0, 1, 0,

//Top
0, 1, 1,
1, 1, 1,
1, 1, 0,
0, 1, 0,

//Bottom
0, 0, 0,
1, 0, 0,
1, 0, 1,
0, 0, 1.
};

// 根据方块的信息获取纹理图集中的坐标(先仅加载草方块)
auto top = m_atlasTest.getTexture({0, 0});
auto side = m_atlasTest.getTexture({1, 0});
auto bottom = m_atlasTest.getTexture({2, 0});

// 依次添加四周纹理,顶部纹理,底部纹理
std::vector<GLfloat> texCoords;
texCoords.insert(texCoords.end(), side.begin(), side.end());
texCoords.insert(texCoords.end(), side.begin(), side.end());
texCoords.insert(texCoords.end(), side.begin(), side.end());
texCoords.insert(texCoords.end(), side.begin(), side.end());
texCoords.insert(texCoords.end(), top.begin(), top.end());
texCoords.insert(texCoords.end(), bottom.begin(), bottom.end());

// 每三个数字是一个三角形,两个三角形是一个方块的一个面,一共六个面
std::vector<GLuint> indices
{
0, 1, 2,
2, 3, 0,

4, 5, 6,
6, 7, 4,

8, 9, 10,
10, 11, 8,

12, 13, 14,
14, 15, 12,

16, 17, 18,
18, 19, 16,

20, 21, 22,
22, 23, 20
};

m_cubeModel.addData({vertexCoords, texCoords, indices});
}

void CubeRenderer::add(const glm::vec3 &position) {
m_quads.push_back(position);
}

void CubeRenderer::render(const Camera &camera) {
// 对于不可视的面进行优化(自动进行)
glEnable(GL_CULL_FACE);

// 激活shader
m_shader.useProgram();
// 将当前CubeRenderer的模型数据导入GPU(顶点数据)
// bind Vertex Array Object
m_cubeModel.bindVAO();
// 导入纹理信息
m_atlasTest.bindTexture();

// 加载当前相机的投影视口变换矩阵
m_shader.loadProjectionViewMatrix(camera.getProjectionViewMatrix());

// 在每个不同位置上渲染
for (auto &quad : m_quads) {
// 加载模型位置/旋转信息,
// 由于MC中方块永远同时垂直于三个坐标轴,所以旋转部分直接设为0,0,0
m_shader.loadModelMatrix(makeModelMatrix({quad, {0, 0, 0}}));

// m_cubeModel.getIndicesCount()会返回被渲染cube在顶点集/纹理集中的下标信息
glDrawElements(GL_TRIANGLES, m_cubeModel.getIndicesCount(), GL_UNSIGNED_INT, nullptr);
}

// 清除所有需要渲染的位置信息(为下一帧做准备)
m_quads.clear();
}

3. 第一人称视角控制

  • Entity:
    • position:实体位置
    • rotation:实体旋转
  • Player:Entity 的子类
    • m_velocity:玩家的速度
    • handleInput:处理一帧的控制输入(鼠标/键盘)
    • update:更新玩家状态(根据速度计算新位置/旋转)
  • Cameara:Entity 的子类
    • hookEntity:绑定一个其他 Entity 对象
    • update:根据绑定的 Entity 对象更新自身状态(位置/角度)
    • m_projectionMatrix:投影变换矩阵
    • m_viewMatrix:视图变换矩阵
    • m_projViewMatrix:投影视图变换矩阵(由前两个矩阵计算得到)

4. 世界管理

  • IChunk:区块管理接口
    • setBlock:设置方块
    • getBlock:获取方块类型
  • Section:小区块(实现了 IChunk 接口)(实现中大小为 16x16x16 个 block)
  • Chunk:大区块(实现了 IChunk 接口)(实现中每个 Chunk 包含不同高度的 3 个 Section)
  • World:世界(实现了 IChunk 接口)(管理游戏中所有的 Chunk)

5. 射线检测

  • Ray:射线类
    • m_rayStart:起始位置
    • m_rayEnd:终止位置(初始为起始位置,后不断沿着射线方向扩充)
    • m_direction:射线方向
    • getLength:获取射线长度
    • step:射线朝 m_direction 方向前进一段(来检测物体)
    • getEnd:获取当前射线终止位置
  • World->getBlock:检测目标位置方块

射线检测核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// StatePlaying.cpp 41
// 当前视线的6格方块
for (Ray ray(m_player.position, m_player.rotation);
ray.getLength() < 6;
ray.step(0.1)){
int x = ray.getEnd().x;
int y = ray.getEnd().y;
int z = ray.getEnd().z;

auto block = m_world.getBlock(x, y, z);

// 不处理空气方块
if (block != 0){
// 冷却时间0.2s
if (timer.getElapsedTime().asSeconds() > 0.2){

// 处理事件
switch (events){
case Events::MOUSE_LEFT_DOWN:
timer.restart();
m_world.editBlock(x, y, z, 0);
break;
case Events::MOUSE_RIGHT_DOWN:
timer.restart();
m_world.editBlock(lastPosition.x,
lastPosition.y,
lastPosition.z, 1);
break;
default:
break;
}
break;
}
}
// 记录最后一个空气方块
lastPosition = ray.getEnd();
}

6. 游戏管理

总体来说 Application 是一个状态栈,不同游戏状态都继承自状态基类
通过不同游戏状态实现不同的游戏模式

  • StateBase:状态基类
    • handleEvent
    • handleInput
    • update
    • render
  • StatePlaying:StateBase 的子类