C++ 目前还没有一个标准的 C++ 依赖包管理器,传统上都是手动去下载源码编译(经典的例如 make),或者直接下载预编译好的库文件(例如没有开源的)和头文件。之后在项目里配置对应的头文件路径和库路径。这个过程非常繁琐,而且枯燥。对于 Windows 上的用户,其实可以考虑使用 NuGet 安装所需要的依赖。NuGet 会自动下载依赖并配置好,省去很多繁琐的过程。

什么是 NuGet

简单来讲,它是一个包管理的平台。类似,Python 的 Pip、JavaScript 的 npm。

创建一个项目

  • Visual Studio 2022
  • 平台工具集:v143

NuGet

在 【工具】 中可以打开 NuGet。

NuGet 管理器

引入 SDL2 库

搜索 SDL 并添加到项目中。

NuGet 管理器

现在 SDL 库就引入到项目中了,就是这么简单。可以直接引用头文件使用 SDL 库。接下来,尝试使用一下 SDL。

Game 基础框架

实现一个基础的游戏框架:

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
// Game.h
#pragma once
#include <SDL.h>

// Game class
class Game
{
public:
Game();
// 初始化游戏
bool Initialize();
// 运行游戏循环直到游戏结束
void RunLoop();
// 关闭游戏
void Shutdown();
private:
// 处理进程输入
void ProcessInput();
// 更新游戏
void UpdateGame();
// 生成输出
void GenerateOutput();

// 通过 SDL 创建窗体
SDL_Window* mWindow;

// 继续运行
bool mIsRunning;
};

对应的具体实现:

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
// Game.cpp
#include "Game.h"

Game::Game()
:mWindow(nullptr)
, mIsRunning(true)
{
}

bool Game::Initialize()
{
int sdlResult = SDL_Init(SDL_INIT_VIDEO);
if (sdlResult != 0)
{
SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
return false;
}

mWindow = SDL_CreateWindow(
"OpenGLNu",
100,
100,
1024,
768,
SDL_WINDOW_OPENGL
);

if (!mWindow)
{
SDL_Log("Failed to create window: %s", SDL_GetError());
return false;
}

return true;
}

void Game::RunLoop()
{
while (mIsRunning)
{
ProcessInput();
UpdateGame();
GenerateOutput();
}
}

void Game::Shutdown()
{
SDL_DestroyWindow(mWindow);
SDL_Quit();
}

void Game::ProcessInput()
{
SDL_Event event;

// 有 event 在队列就一直循环
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
mIsRunning = false;
break;
default:
break;
}
}

// 获取键盘的状态
const Uint8* state = SDL_GetKeyboardState(nullptr);
// 如果按了 Esc,结束循环
if (state[SDL_SCANCODE_ESCAPE])
{
mIsRunning = false;
}
}

void Game::UpdateGame()
{
}

void Game::GenerateOutput()
{
}

创建一个 Main.cpp 调用 Game

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

#include "Game.h"

int main(int argc, char* argv[])
{
Game game;
if (game.Initialize())
{
game.RunLoop();
}
game.Shutdown();
return 0;
}

可以直接编译运行。

SDL

OpenGL

上面的代码中,SDL_WINDOW_OPENGL 属性指定了使用 OpenGL,下一步就是要创建一个 OpenGL 的上下文(OpenGL 被设计成一个状态机)。上下文可以看成是 OpenGL 的 World(世界),包含了色彩缓冲区、加载的图像或者模型等等。

1
2
// Game.h
SDL_GLContext mContext;

Game::Initialize() 设置 OpenGL 参数,并初始化上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Game.cpp

/// 设置 OpenGL 参数
// core OpenGL
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
// 指定版本 4.2
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
// RGBA
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
// 双缓冲
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
// 强制使用硬件加速
SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1);

mContext = SDL_GL_CreateContext(mWindow);

OpenGL 支持向后兼容,需要手动查询所需要的扩展,这个过程也很枯燥。为了简化这个过程,通常会使用 OpenGL Extension Wrangler Library(GLEW)。GLEW 可以自动初始化当前 OpenGL 上下文所支持的所有扩展函数。

在 NuGet 中搜索 GLEW 并安装到项目:

GLEW

可以看到有两个下载比较多的 GLEW,从发布日期上看,glew.v140 会比较新,但名称绑定了 v140。这里还是选择了 GLEW

安装完成之后,引入 GLEW 的头文件:

1
2
// Game.h
#include <GL/glew.h>

要初始化 GLEW,要在创建上下文之后,添加下面的代码:

1
2
3
4
5
6
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
SDL_Log("Failed to initialize GLEW.");
return false;
}

如果在 v143 下尝试编译,会发现链接失败了,因为依赖包指定了版本

glew 绑定了版本

其实 C++ API 还是相对比较稳定的,我觉得绑定平台反而看起来不是一个很好设计。

glew 链接

直接复制修改

复制了 v110,修改文件夹名为 v143。修改 glew.targets 文件,将 v110 替换为 v143。当然,这种方法比较简单粗暴,不是特别提倡。

NuGet 中,动态运行库 .dll 是单独列出来的,同样拷贝 v110 文件夹作为 v143,修改 glew.redist.targets,将 v110 替换成为 v143

这样就可以运行了。

然而,对于 Debug 版本,由于目前我用的是 V143,并没有 v110 的工具集,会发现运行的时候少了一个 msvcr110d.dll。这个问题比较好解决,直接去 下载 一个对应的 dll,拷贝 Debug 程序的目录下就可以了(不推荐拷到 System 下面)。

这样,无论是 Debug 版本还是 Release 都可以正常运行了。

OpenGL 的依赖

OpenGL 还需要使用到系统自带的 OpenGL32.lib,否则下面代码会出现链接错误。

1
2
3
4
5
6
7
8
9
10
11
12
void Game::GenerateOutput()
{
// 灰色
glClearColor(0.86f, 0.86f, 0.86f, 1.0f);
// 清除颜色缓冲区
glClear(GL_COLOR_BUFFER_BIT);

// TODO: 绘制场景

// 交换缓冲区
SDL_GL_SwapWindow(mWindow);
}

由于是系统自带,只需要在项目设置中的附加依赖项直接加入 OpenGL32.lib

OpenGL32.lib 依赖项

最终

灰色背景的效果

这就意味着 OpenGL 环境搭建好并正常工作了。

小结

NuGet 的确省去了很多繁琐的过程,但很明显,很多包的更新赶不上 Visual Studio 的工具集,而 NuGet 大概是为了可重入才绑定了对应的工具集,导致有时候无法找到对应版本的库。NuGet 显然并不完美,无法做到全自动,但相对于传统的方式,还是大大简化了。不过,这种方式对于源码版本的控制显然会更难,推崇源码构建的开发者自然不会选择用这种方式。自主可控和方便快捷之间永远是需要权衡的。对于初学者,显然使用 NuGet 搭建环境还是比较方便的。