第21章 视差背景与着色器
# 第21章 视差背景与着色器
这是我们为游戏进行开发的最后一章。在本章结束时,游戏将具备所有功能,可完整游玩。为完成《奔跑》(Run)这款游戏,我们将进行以下工作:
- 了解OpenGL、着色器(shader)和图形库着色语言(Graphics Library Shading Language,GLSL)。
- 通过实现滚动背景和着色器来完善
CameraGraphics
类。 - 使用他人的代码为游戏编写一个着色器。
- 运行完成后的游戏。
本章完整代码可在Run7
文件夹中找到。让我们从了解OpenGL、着色器和GLSL开始吧。
# 了解OpenGL、着色器和GLSL
开放图形库(Open Graphics Library,OpenGL)是一个用于处理2D和3D图形的编程库。OpenGL可在所有主流桌面操作系统上运行,还有一个适用于移动设备的版本,名为OpenGL ES。
OpenGL最初于1992年发布。在二十多年的时间里,它不断被优化和改进。此外,显卡制造商在设计硬件时,也会使其能与OpenGL良好配合。提及这些并非要给你上一堂历史课,而是想说明,试图改进OpenGL并在桌面2D(和3D)游戏中使用它是不明智的,尤其是当我们希望游戏能在多种操作系统上运行,而不只是在Windows系统(这显然是个常见选择)上运行时。实际上,我们已经在使用OpenGL了,因为SFML库使用了OpenGL。
着色器是在GPU(图形处理单元)上运行的程序。我们将在接下来的部分深入了解它们。
# 可编程管线与着色器
通过OpenGL,我们可以访问所谓的可编程管线。可编程管线使我们能够使用RenderWindow
实例的draw
函数,在每一帧中将着色器程序发送出去进行绘制。在调用draw
函数之后,我们还可以编写在GPU上运行的代码,这些代码能独立地处理每个像素。这是一项非常强大的功能。
在GPU上运行的这些额外代码被称为着色器程序。我们可以编写代码,在顶点着色器(vertex shader)中处理图形的几何形状(位置)。我们也可以编写代码,在片段着色器(fragment shader)中单独处理每个像素的外观。此外,还有其他类型的着色器,如计算着色器(compute shader)和几何着色器(geometry shader),在本次讨论中我们暂不涉及。
虽然我们不会深入探究着色器,但会使用GLSL(GL着色语言)来研究一些相对简单的着色器代码。在这种场景下,GLSL是你需要使用的语言。在《奔跑》项目中,我们将使用他人编写的相当复杂的GLSL着色器代码,以实现令人惊叹的效果。
在OpenGL中,所有图形元素都是点、线或三角形。此外,我们可以为这些基本几何图形添加颜色和纹理,并将这些元素组合起来,制作出我们在现代游戏中看到的复杂图形。这些统称为图元(primitives)。我们可以通过SFML的图元、VertexArray
,以及Sprite
和Shape
类来访问OpenGL的图元。
除了图元,OpenGL还使用矩阵(matrices)。矩阵是一种用于进行算术运算的方法和结构。这些算术运算可以从非常简单的高中水平计算,比如移动(平移)一个坐标,到相当复杂的运算,例如执行更高级的数学运算,将我们的游戏世界坐标转换为GPU可以使用的OpenGL屏幕坐标。幸运的是,这些复杂的运算由SFML在幕后为我们处理。SFML也允许我们直接操作OpenGL。
如果你想进一步了解OpenGL,可以从这里开始:http://learnopengl.com/#!Introduction (opens new window)。如果你想在使用SFML的同时直接使用OpenGL,可以阅读这篇文章来了解更多信息:https://www.sfml-dev.org/tutorials/2.5/window-opengl.php (opens new window)。
一个游戏可以有多个着色器。我们可以为不同的游戏对象附加不同的着色器,以创建所需的效果。在这个游戏中,我们只会使用一个顶点着色器,并在每一帧中通过单独的绘制调用来将其应用到游戏背景上。在SFML中,你将着色器附加到绘制调用上,它会影响该绘制调用中的所有内容。
不过,当你了解如何将着色器附加到绘制调用后,就会明白添加更多着色器是很容易的事。
我们将按以下步骤进行:
- 首先,我们需要在GPU上执行的着色器代码。我们将在“为游戏编写着色器”这部分获取。
- 然后,我们需要使用SFML C++代码编译这些代码。Visual Studio不会为我们编译着色器。
- 最后,我们需要在游戏的
draw
函数中,将着色器附加到相应的绘制函数调用上。
GLSL是一种语言,它有自己的类型和可以声明及使用的变量。此外,我们可以从C++代码中与着色器程序的变量进行交互。正如我们将看到的,GLSL的一些语法与C++相似。
# 编写一个假设的片段着色器
在本节中,我们将研究一些简单的假设代码。我们不会将这些代码添加到《奔跑》项目中。下面是一个相对简单的着色器fragShader.frag
的代码:
// attributes from vertShader .vert
varying vec4 vColor;
varying vec2 vTexCoord;
// uniforms
uniform sampler2D uTexture;
uniform float uTime;
void main()
{
float coef = sin(gl_FragCoord.y * 0.1 + 1 * uTime);
vTexCoord.y += coef * 0.03;
gl_FragColor = vColor * texture2D(uTexture, vTexCoord);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
前四行(不包括注释)是片段着色器将使用的变量,但它们不是普通变量。我们首先看到的类型是varying
。varying
用于在两个着色器中都有效的变量。接下来,是uniform
变量。这些变量可以直接从我们的C++代码中进行操作。我们很快会看到如何在更复杂的着色器中实现这一点。
除了varying
和uniform
类型,每个变量还有一个更常规的类型,用于定义实际的数据,如下所示:
vec4
是一个包含四个值的向量。vec2
是一个包含两个值的向量。sampler2d
将保存一个纹理。float
与C++中的float
数据类型类似。
main
函数中的代码会被执行。如果仔细查看main
函数中的代码,我们会看到每个变量都在被使用。这段代码具体的功能超出了本书的范围。不过,总的来说,纹理坐标(vTexCoord
)和像素/片段的颜色(glFragColor
)通过一些数学函数和运算进行了处理。请记住,这是针对绘制函数涉及的每个像素执行的,而绘制函数在游戏的每一帧都会被调用。此外,要注意uTime
在每一帧中传入的值都不同。其结果是使图形呈现出波纹效果。
# 编写一个假设的顶点着色器
在本节中,我们将看到一些假设的顶点着色器简单代码。我们不会在《奔跑》项目中使用这些代码。下面是假设的vertShader.vert
文件中的代码。你不需要编写这段代码:
//varying "out" variables to be used in the fragment shader
varying vec4 vColor;
varying vec2 vTexCoord;
void main()
{
vColor = gl_Color;
vTexCoord = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
2
3
4
5
6
7
8
9
10
首先,注意两个以v
开头的varying
变量。这些正是我们在片段着色器中处理过的变量。在main
函数中,代码处理每个顶点的位置。代码的工作原理超出了本书的范围,但幕后涉及一些相当深入的数学运算。如果你对此感兴趣,进一步探索GLSL会非常有趣。
在下一节中,我们将了解如何准备和加载一个真正的着色器程序,以及如何向每一帧传递值(varying
变量)。我们将使用一个比刚刚看到的假设着色器复杂得多的着色器程序。
# 完成CameraGraphics
类的编写
在本节中,我们将回顾、修改并为CameraGraphics
类添加内容。首先,我们要在CameraGraphics.h
文件中添加一些与背景和着色器相关的变量。在CameraGraphics.h
文件私有部分的末尾,添加以下变量:
//For the shaders and parallax background
Shader m_Shader;
bool m_ShowShader = false;
bool m_BackgrounsAreFlipped = false;
Clock m_ShaderClock;
Vector2f m_PlayersPreviousPosition;
Texture m_BackgroundTexture;
Sprite m_BackgroundSprite;
Sprite m_BackgroundSprite2;
2
3
4
5
6
7
8
9
10
在上述代码中,我们有一个名为m_Shader
的SFML Shader
(着色器),以及一个名为m_ShowShader
的布尔变量,我们可以用它来跟踪何时显示着色器。在这个游戏中,我们将在显示着色器10秒和显示视差背景10秒之间切换。
布尔变量m_BackgroundsAreFlipped
将用于判断代表背景的纹理是否水平翻转。我们这样做是为了将一个背景图像与它的多个实例无缝连接起来,以创建平滑的滚动效果。
Clock
类型的m_ShaderClock
很有趣,它将作为着色器中一个varying
变量的输入值。
Vector2f
类型的m_PlayersPreviousPosition
变量可以让我们知道玩家在上一次更新前的位置。当我们在CameraGraphics.cpp
文件中添加更多代码时,会看到它的用处。
Texture
实例m_BackgroundTexture
是用于背景图像的单独纹理。它与保存其他所有内容的纹理图集完全分开。
名为m_BackgroundSprite
的Sprite
(精灵)用于显示背景图像。Sprite
m_BackgroundSprite2
则用于显示背景的翻转副本。
现在,在CameraGraphics.cpp
文件中的CameraGraphics
构造函数里,在右花括号即将结束之前,添加以下新代码:
// Initialize the background sprites
m_BackgroundTexture.loadFromFile(
"graphics/backgroundTexture.png");
m_BackgroundSprite.setTexture(m_BackgroundTexture);
m_BackgroundSprite2.setTexture(m_BackgroundTexture);
m_BackgroundSprite.setPosition(0, -200);
// Initialize the shader
m_Shader.loadFromFile(
"shaders/glslsandbox109644", sf::Shader::Fragment);
if (!m_Shader.isAvailable())
{
std::cout << "The shader is not available\n";
}
m_Shader.setUniform(
"resolution", sf::Vector2f(2500, 2500));
m_ShaderClock.restart();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上述代码加载背景纹理,并将其附加到我们的两个新Sprite
上。第一个背景Sprite
被定位在玩家身后,以填充屏幕。
接下来,我们使用loadFromFile
函数加载着色器,通过调用isAvailable
函数检查着色器是否可用,并使用setUniform
函数设置着色器代码中统一变量(uniform variable)的值。resolution
这个值对应着色器代码中声明的一个统一变量;Vector2f
被赋给它,并在着色器代码中使用。
最后,我们调用m_ShaderClock
的restart
函数,使其开始计时。
最后,在draw
函数中,在下面这行函数调用之后:
m_Window->setView(m_View);
在下面这段代码之前:
// Draw the time UI but only in the main camera
if (!m_IsMiniMap)
{
2
3
添加绘制相关的代码。我保留了上面两行代码并突出显示了它们。这两行之间所有常规格式的代码都是新代码。一次性添加所有代码,以避免混淆众多的if
、else
语句和花括号,然后我们再将其分解,逐块分析:
m_Window->setView(m_View);
/// Background stuff
Vector2f movement;
movement.x = m_Position->left - m_PlayersPreviousPosition.x;
movement.y = m_Position->top -
m_PlayersPreviousPosition.y;
if (m_BackgrounsAreFlipped)
{
m_BackgroundSprite2.setPosition(
m_BackgroundSprite2.getPosition().x + movement.x / 6,
m_BackgroundSprite2.getPosition().y + movement.y / 6);
m_BackgroundSprite.setPosition(
m_BackgroundSprite2.getPosition().x
+ m_BackgroundSprite2.getTextureRect().getSize().x, m_BackgroundSprite2.getPosition().y);
if (m_Position->left >
m_BackgroundSprite.getPosition().x +
(m_BackgroundSprite.getTextureRect().getSize().x / 2))
{
m_BackgrounsAreFlipped = !m_BackgrounsAreFlipped;
m_BackgroundSprite2.setPosition(
m_BackgroundSprite.getPosition());
}
}
else
{
//cout << mBackgrounsAreFlipped << endl;
m_BackgroundSprite.setPosition(
m_BackgroundSprite.getPosition().x - movement.x /
6, m_BackgroundSprite.getPosition().y + movement.y / 6);
m_BackgroundSprite2.setPosition(
m_BackgroundSprite.getPosition().x +
m_BackgroundSprite.getTextureRect().getSize().x, m_BackgroundSprite.getPosition().y);
if (m_Position->left >
m_BackgroundSprite2.getPosition().x +
(m_BackgroundSprite2.getTextureRect().getSize().x / 2))
{
m_BackgrounsAreFlipped = !m_BackgrounsAreFlipped;
m_BackgroundSprite.setPosition(
m_BackgroundSprite2.getPosition());
}
}
m_PlayersPreviousPosition.x = m_Position->left;
m_PlayersPreviousPosition.y = m_Position->top;
// Set the others parameters who //need to be updated every frame
m_Shader.setUniform("time",
m_ShaderClock.getElapsedTime().asSeconds());
sf::Vector2i mousePos =
m_Window->mapCoordsToPixel(m_Position->getPosition());
m_Shader.setUniform("mouse",
sf::Vector2f(mousePos.x, mousePos.y + 1000));
if (m_ShaderClock.getElapsedTime().asSeconds() > 10)
{
m_ShaderClock.restart();
m_ShowShader = !m_ShowShader;
}
if (!m_ShowShader)
{
m_Window->draw(m_BackgroundSprite, &m_Shader);
m_Window->draw(m_BackgroundSprite2, &m_Shader);
}
else// Show the parallax background
{
m_Window->draw(m_BackgroundSprite);
m_Window->draw(m_BackgroundSprite2);
}
// Draw the time UI but only in the main camera
if (!m_IsMiniMap)
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
接下来,我们将在后续部分分解这段冗长的代码,以便更好地理解它。
# 拆分新的绘制代码
在拆分前面的代码时,首先我们看到的是这段:
// 背景相关内容
Vector2f movement;
movement.x = m_Position->left - m_PlayersPreviousPosition.x;
movement.y = m_Position->top - m_PlayersPreviousPosition.y;
2
3
4
上述代码声明了一个名为movement
的Vector2f
(二维向量),并使用上一帧中玩家的最后位置来设置x
和y
的值。
接下来,我们有这段代码:
if (m_BackgrounsAreFlipped)
{
m_BackgroundSprite2.setPosition(
m_BackgroundSprite2.getPosition().x + movement.x / 6,
m_BackgroundSprite2.getPosition().y + movement.y / 6);
m_BackgroundSprite.setPosition(
m_BackgroundSprite2.getPosition().x
+ m_BackgroundSprite2.getTextureRect().getSize().x, m_BackgroundSprite2.getPosition().y);
if (m_Position->left >
m_BackgroundSprite.getPosition().x +
(m_BackgroundSprite.getTextureRect().getSize().x / 2))
{
m_BackgrounsAreFlipped = !m_BackgrounsAreFlipped;
m_BackgroundSprite2.setPosition(
m_BackgroundSprite.getPosition());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上述代码在m_BackgroundsAreFlipped
为true
时执行。我们会看到,下一个代码块在m_BackgroundsAreFlipped
为false
时执行。在上面的代码块中,m_BackgroundSprite2
的位置设置在m_BackgroundSprite
之前。这里,m_BackgroundSprite2
的位置是根据玩家从上一帧到当前帧的位置变化来设置的,但要除以6。数字6是一个 “神奇” 的数字,这样设置效果看起来不错。如果你增大数字6,背景滚动会变慢;如果你减小数字6,背景滚动会变快。mBackgroundSprite
的位置是相对于m_BackgroundSprite2
的右边缘来设置的。最后,在上述代码中,有一个if
语句,当相机的位置超过m_BackgroundSprite
的左边缘加上纹理大小的一半时,该语句会执行。
这意味着相机聚焦在m_BackgroundSprite
的中心,而mBackgroundSprite2
完全不在 “镜头” 中。这正是切换哪个背景先绘制的最佳时机。然后,随着相机向右移动,看起来城市就像是无穷无尽的。在if
语句中,m_BackgroundsAreFlipped
布尔值会取反,背景的位置也会互换。
接下来,是我们刚刚讨论的代码的另一部分:
else
{
//cout << mBackgrounsAreFlipped << endl;
m_BackgroundSprite.setPosition(
m_BackgroundSprite.getPosition().x - movement.x /
6, m_BackgroundSprite.getPosition().y + movement.y / 6);
m_BackgroundSprite2.setPosition(
m_BackgroundSprite.getPosition().x +
m_BackgroundSprite.getTextureRect().getSize().x,
m_BackgroundSprite.getPosition().y);
if (m_Position->left >
m_BackgroundSprite2.getPosition().x +
(m_BackgroundSprite2.getTextureRect().getSize().x / 2))
{
m_BackgrounsAreFlipped = !m_BackgrounsAreFlipped;
m_BackgroundSprite.setPosition(
m_BackgroundSprite2.getPosition());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
上述代码完善了背景切换的逻辑,在相机聚焦到第二个背景之前,先绘制第一个背景,然后再进行切换。
接下来,我们有这段代码:
m_PlayersPreviousPosition.x = m_Position->left;
m_PlayersPreviousPosition.y = m_Position->top;
// 设置其他需要每帧更新的参数
m_Shader.setUniform("time",
m_ShaderClock.getElapsedTime().asSeconds());
sf::Vector2i mousePos =
m_Window->mapCoordsToPixel(m_Position->getPosition());
m_Shader.setUniform("mouse",
sf::Vector2f(mousePos.x, mousePos.y + 1000));
2
3
4
5
6
7
8
9
10
11
12
在上述代码中,首先,我们看到将玩家的位置保存到m_PlayersPreviousPosition
中。记住,这就是我们在绘制函数开始时确定背景移动的方式。接下来,我们在Shader
(着色器)实例上调用setUniform
函数,并传入要更改的统一变量名称以及从Clock
(时钟)实例获取的当前时间(以秒为单位)。然后,我们获取鼠标的像素坐标,并将其传入以设置着色器中的mouse
统一变量。
接下来是这段代码:
if (m_ShaderClock.getElapsedTime().asSeconds() > 10)
{
m_ShaderClock.restart();
m_ShowShader = !m_ShowShader;
}
2
3
4
5
在上述代码中,我们检查自上次重置时钟以来是否已经过了10秒,如果是,那么我们将时钟重置为零,并反转m_ShowShader
的值,以便在显示着色器效果和显示视差背景之间切换。
最后,对于游戏中最大的代码块:
if (!m_ShowShader)
{
m_Window->draw(m_BackgroundSprite, &m_Shader);
m_Window->draw(m_BackgroundSprite2, &m_Shader);
}
else// 显示视差背景
{
m_Window->draw(m_BackgroundSprite);
m_Window->draw(m_BackgroundSprite2);
}
2
3
4
5
6
7
8
9
10
在上述代码中,如果不显示着色器,我们将使用和不使用着色器来绘制背景。
# 为游戏编写着色器
唯一剩下的任务是,着色器代码试图加载的文件是空的。着色器代码是公开可用的,但代码不是我编写的,而且我可能不被允许分发它。访问https://glslsandbox.com/e#109644.0并点击 “显示代码”。将将近400行代码全部复制并粘贴到shaders/glsldandbox109644
文件中。一定要给发布这段代码的有才华的着色器程序员留下友好的评论或感谢语。保存文件,我们就准备就绪了。着色器代码超出了本书的范围。
在下一节中,我们将全面领略着色器的魅力。
# 运行完成的游戏
运行游戏,欣赏新的背景和火焰滚动的乡村效果着色器,它每十秒切换一次。
图21.1:着色器
哇!你可能会认同,着色器和着色器程序的功能相当令人惊叹。
存活十秒后,画面将切换到滚动的背景,如下图所示。
图21.2:背景
就是这样。我们的游戏完成了。
# 总结
当你第一次翻开这本厚厚的书时,封底可能看起来还很遥远。但我希望这并没有那么难。
关键是你现在已经走到了这里,希望你对如何使用C++ 构建游戏有了很好的理解。
本节的目的是祝贺你取得的出色成就,但同时也要指出,这一页可能不应该是你旅程的终点。如果你和我一样,每当实现一个新的游戏功能时都会感到兴奋,那么你可能想要学习更多。
# 进一步阅读
你可能会惊讶地发现,即便已经写了几百页内容,我们对C++ 的探索也只是浅尝辄止。即使是我们已经涵盖的主题,也可以更深入地探讨,而且还有许多——有些相当重要——的主题我们甚至都没有提及。考虑到这一点,让我们来看看接下来可以做些什么。
如果你绝对需要一个正规的资格证书,那么唯一的途径就是接受正规教育。当然,这既昂贵又耗时,而且我也帮不上更多的忙。
另一方面,如果你想在工作中学习,比如在开始着手开发一款最终会发布的游戏时学习,那么接下来的内容是关于你接下来可能想做的事情的讨论。
在每个项目中,我们面临的最棘手的决定可能就是如何构建代码结构。在我看来,关于如何构建C++ 游戏代码结构,绝对最佳的信息来源是http://gameprogrammingpatterns.com/。其中一些讨论涉及到本书未涵盖的概念,但大部分内容都是完全可以理解的。如果你理解类、封装、纯虚函数和单例模式,那就深入研究这个网站吧。
着色器在游戏开发中的重要性比我们在这个简短介绍中看到的要大得多。如果你想成为着色器方面的专家,我推荐阅读亚马逊上的《Anton’s OpenGL 4 Tutorials Kindle Edition》。由于某种原因,这本书在搜索结果中似乎有点被埋没,所以可能需要在亚马逊搜索框中输入完整的书名。它比大多数其他关于这个主题的书籍更便宜,内容也更全面。同样重要的是要知道,SFML(Simple and Fast Multimedia Library,简单快速多媒体库)处理着色器的方式与在纯OpenGL中使用着色器的方式不同。阅读一些关于OpenGL的内容(见下文)是个不错的主意,或者如果你坚持使用SFML(这是一个完全可行的策略),可以在这里阅读SFML是如何抽象着色器的:https://www.sfml-dev.org/tutorials/2.6/graphics-shader.php。
关于OpenGL,有大量的教科书可供选择。如果你喜欢视频,我推荐在Udemy上的《Computer Graphics with Modern OpenGL and C++》课程。如果你想要一本教科书,可以试试《OpenGL Programming Guide》或《Learn OpenGL: Learn modern OpenGL graphics programming in a step-by-step fashion》。
当然,你可能想要拓展一下,做一些完全不同的事情,特别是如果你想制作一款最前沿的3D游戏。在这种情况下,你应该研究一下像虚幻引擎(Unreal Engine)这样的引擎,它也使用C++; 对于2D(可能带有一些3D元素)游戏,可以试试Godot引擎。
在本书中我已经多次提到SFML的网站。如果你还没有访问过,请看一下:http://www.sfml-dev.org/。
当你遇到不理解(或者甚至从未听说过)的C++ 主题时,最简洁、最有条理的C++ 教程可以在http://www.cplusplus.com/doc/tutorial/找到。另一个选择是ChatGPT。ChatGPT很适合用来询问诸如 “解释这段代码” 或者 “我怎样才能让这段代码更好 / 更快?” 之类的问题。
除此之外,还有四本关于SFML的书籍你可能会想了解一下。它们都是不错的书,但适用人群差异很大。请注意,这些书有点过时了,但我认为仍然有用。以下是按从最适合初学者到最具技术性的顺序列出的书籍:
- 《SFML Blueprints》,作者Maxime Barbier:https://www.packtpub.com/game-development/sfml-blueprints
- 《SFML Game Development By Example》,作者Raimondas Pupius:https://www.packtpub.com/game-development/sfml-game-development-example
- 《SFML Game Development》,作者Jan Haller、Henrik Vogelius Hansson和Artur Moreira:https://www.packtpub.com/game-development/sfml-game-development
你可能还想考虑为你的游戏添加逼真的2D物理效果。SFML与Box2d物理引擎配合得非常好。这个网址是官方网站:http://box2d.org/。下面这个网址可能会带你找到使用C++ 搭配它的最佳指南:http://www.iforce2d.net/。
如果你觉得自己在C++ 游戏编程领域起步晚了,别担心。25年前我也觉得自己起步晚了,但如今用C++ 制作的游戏比以往任何时候都多。如果你想走在最前沿,可以考虑研究区块链。区块链技术支持Web3游戏,这是一种新型游戏,它利用区块链技术为玩家创造更沉浸式、更有回报的游戏体验。在Web3游戏中,玩家拥有自己的游戏内资产,这些资产可以在其他游戏中交易或使用。这创造了一个更开放、更引人入胜的游戏生态系统。想象一下,有人玩《精灵宝可梦》游戏,在数字钱包应用中赢得了一张数字精灵宝可梦卡片。然后他们可以与其他人在网上或学校操场上交易这张可能稀有甚至独一无二的卡片。这就是区块链游戏的前景。不幸的是,到目前为止,大多数尝试都只做出了平庸的游戏,甚至还出现了金融诈骗。我敢打赌,无论谁能把这个做好,都会引发人们对这一游戏类型的极大兴趣。我的观点是,你在C++ 游戏编程领域并不晚。一切才刚刚开始。
最重要的是,非常感谢你阅读这本书,继续制作游戏吧!