编程最棒的部分是创造其他人可以享受的东西。这可能是我们都想至少尝试制作游戏的原因,但学习一个完整的引擎可能会让人望而生畏。

有一些选择可以让开发图形应用程序更容易上手,特别是对于 C 和 C++ 来说。有 OpenGL 和 SDL,它们会抽象出硬件,但给我们更多自由来编写应用程序代码。“游戏开发入门课程”中通常会用到这些。然而,这很快就会变得让人不知所措,因为从头构建这些库本身就是一个挑战。

SDL2 或 Simple DirectMedia Layer 2.0,是一个旨在提供对音频、键盘、鼠标、操纵杆和图形硬件的低级访问的库。它是跨平台和移动友好的,因此有很多选择和机会可以更深入地了解使用 C++ 开发游戏的不同方面。

本教程将引导您完成设置一个基本应用程序的过程,包括键盘控制、图像和文本。这是制作贪吃蛇或吃豆人风格游戏的坚实起点。对于更高级的游戏,我将为您提供一些优秀的参考,以便您在设置完成后继续学习!

制作我们的第一个游戏

SDL2 非常容易上手。基本步骤是初始化、创建渲染循环和清理。我们将在本博客中构建的示例可以在 GitHub conan-examples2 仓库中找到。

您可以通过克隆示例并自行运行安装命令来按照本节操作!

git clone https://github.com/conan-io/examples2.git
cd examples2/examples/libraries/sdl2/introduction

1:创建窗口和渲染器

#include <stdio.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_timer.h>

int main(int argc, char *argv[])
{
    // returns zero on success else non-zero
    if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
        printf("error initializing SDL: %s\n", SDL_GetError());
        return 1;
    }
    SDL_Window* win = SDL_CreateWindow("GAME", // creates a window
                                    SDL_WINDOWPOS_CENTERED,
                                    SDL_WINDOWPOS_CENTERED,
                                    1000, 1000, 0);

    // triggers the program that controls
    // your graphics hardware and sets flags
    Uint32 render_flags = SDL_RENDERER_ACCELERATED;

    // creates a renderer to render our images
    SDL_Renderer* rend = SDL_CreateRenderer(win, -1, render_flags);

    ///
    /// Section 2: SDL image loader
    ///

    ///
    /// Section 4: SDL ttf and rendering text
    ///

    ///
    /// Section 3: Game Loop and Basic Controls
    ///

    // We add a delay in order to see that our window
    // has successfully popped up.
    SDL_Delay(3000);

    ///
    /// Section 5: Freeing resources
    ///

    // We destroy our window. We are passing in the pointer
    // that points to the memory allocated by the 
    // 'SDL_CreateWindow' function. Remember, this is
    // a 'C-style' API, we don't have destructors.
    SDL_DestroyWindow(win);
    
    // We safely uninitialize SDL2, that is, we are
    // taking down the subsystems here before we exit
    // our program.
    SDL_Quit();
}

这是仅打开窗口所需的最低限度。

screenshot of a basic windows opened by SDL using WSL2 on Windows for the GUI

注意:在 macOS 上,应用程序和窗口的存在方式有所不同,您会在 Dock 中看到它,但很可能不会弹出。

我们先不提窗口关闭时出现的段错误。稍后在确保清理时我们会解决它!

2:使用 SDL_Image 加载图像

确保为该库添加头文件包含 #include SDL2/SDL_image.h>

    ///
    /// Section 2: SDL image loader
    ///

    // creates a surface to load an image into the main memory
    SDL_Surface* surface;

    // please provide a path for your image
    surface = IMG_Load("conan-logo.png");

    // loads image to our graphics hardware memory.
    SDL_Texture* tex = SDL_CreateTextureFromSurface(rend, surface);

    // clears main-memory
    SDL_FreeSurface(surface);

    // let us control our image position
    // so that we can move it with our keyboard.
    SDL_Rect dest;

    // connects our texture with dest to control position
    SDL_QueryTexture(tex, NULL, NULL, &dest.w, &dest.h);

    // adjust height and width of our image box.
    dest.w /= 6;
    dest.h /= 6;

    // sets initial x-position of object
    dest.x = (1000 - dest.w) / 2;

    // sets initial y-position of object
    dest.y = (1000 - dest.h) / 2;

    ///
    /// Section 4: SDL ttf and rendering text
    ///

    ///
    /// Section 3: Game Loop and Basic Controls
    ///     Note: The rest of this snippet will be removed

    while (1)
    {
        // clears the screen
        SDL_RenderClear(rend);
        SDL_RenderCopy(rend, tex, NULL, &dest);

        // triggers the double buffers
        // for multiple rendering
        SDL_RenderPresent(rend);
    }

    ///
    /// Section 3: Game Loop and Basic Controls
    ///     Note: The code above will be removed

    ///
    /// Section 5: Freeing resources
    ///

查看代码,有一些需要注意的地方。图像从磁盘加载,然后转换为纹理。我们要显示的所有内容都需要是纹理。使用纹理,我们可以查询填充的内容并反向工作以将图像居中在窗口上。

basic game window with a Conan 2.0 logo cube center

成功!我们在窗口中渲染了 Conan 2.0 徽标。

现在,尝试关闭窗口。哦,它没有关闭?!这是因为我们写了一个无限循环 while(1),它将永远运行。您需要打开任务管理器并杀死应用程序。任何游戏都需要有一个控制循环,它在每一帧开始时运行,等待用户输入,显示图形,并允许您优雅地关闭。

3:游戏循环和基本控制

现在,我们需要替换一些弹出窗口并显示图像的临时代码。该代码仅限于渲染一次,但现在我们希望能够在每次用户输入时更新和重新渲染图像。

    ///
    /// Section 4: SDL ttf and rendering text
    ///

    ///
    /// Section 3: Game Loop and Basic Controls
    ///

    // controls animation loop
    int close = 0;

    // speed of box
    int speed = 300;

    // animation loop
    while (!close) {
        SDL_Event event;

        // Events management
        while (SDL_PollEvent(&event)) {
            switch (event.type) {

            case SDL_QUIT:
                // handling of close button
                close = 1;
                break;

            case SDL_KEYDOWN:
                // keyboard API for key pressed
                switch (event.key.keysym.scancode) {
                case SDL_SCANCODE_ESCAPE:
                    close = 1;
                    break;
                case SDL_SCANCODE_W:
                case SDL_SCANCODE_UP:
                    dest.y -= speed / 30;
                    break;
                case SDL_SCANCODE_A:
                case SDL_SCANCODE_LEFT:
                    dest.x -= speed / 30;
                    break;
                case SDL_SCANCODE_S:
                case SDL_SCANCODE_DOWN:
                    dest.y += speed / 30;
                    break;
                case SDL_SCANCODE_D:
                case SDL_SCANCODE_RIGHT:
                    dest.x += speed / 30;
                    break;
                default:
                    break;
                }
            }
        }

        // right boundary
        if (dest.x + dest.w > 1000)
            dest.x = 1000 - dest.w;

        // left boundary
        if (dest.x < 0)
            dest.x = 0;

        // bottom boundary
        if (dest.y + dest.h > 1000)
            dest.y = 1000 - dest.h;

        // upper boundary
        if (dest.y < 0)
            dest.y = 0;

        // clears the screen
        SDL_RenderClear(rend);
        SDL_RenderCopy(rend, tex, NULL, &dest);

       ///
       /// Section 4: SDL ttf and rendering text
       ///

        // triggers the double buffers
        // for multiple rendering
        SDL_RenderPresent(rend);

        // calculates to 60 fps
        SDL_Delay(1000 / 60);
    }

    ///
    /// Section 3: Game Loop and Basic Controls
    ///

    ///
    /// Section 5: Freeing resources
    ///

我们应该有了游戏的雏形!

尝试移动方块并观察它四处移动!

4:添加 SDL_ttf 和字体以在游戏中显示文本

让我们将头文件 #include <SDL2/SDL_ttf.h> 添加到主代码文件的顶部。

文本实际上很复杂!每个字母都是一个字形,每个字形都需要光栅化为纹理,以便我们可以将其添加到渲染循环中。我们将为此创建一个辅助函数。

SDL_ttf 负责很多工作,并为我们提供了 RenderTextCreateTexture,如果您还记得,这正是我们最终用来显示方块的方式。

您可以在 main 函数上方添加以下辅助函数

void render_text(
    SDL_Renderer *renderer,
    int x,
    int y,
    const char *text,
    TTF_Font *font,
    SDL_Rect *rect,
    SDL_Color *color
) {
    SDL_Surface *surface;
    SDL_Texture *texture;

    surface = TTF_RenderText_Solid(font, text, *color);
    texture = SDL_CreateTextureFromSurface(renderer, surface);
    rect->x = x;
    rect->y = y;
    rect->w = surface->w;
    rect->h = surface->h;
    SDL_FreeSurface(surface);
    SDL_RenderCopy(renderer, texture, NULL, rect);
    SDL_DestroyTexture(texture);
}

接下来我们需要加载要使用的字体,对于此演示,我选择了 Roboto,这将在游戏循环之外进行,并且只需要调用一次。

    ///
    /// Section 4: SDL ttf and rendering text
    ///

    // Init TTF
    TTF_Init();
    TTF_Font *font = TTF_OpenFont("Roboto-Regular.ttf", 24);
    if (font == NULL) {
        printf("error initializing TTF: %s\n", TTF_GetError());
        return 1;
    }

    ///
    /// Section 3: Game Loop and Basic Controls
    ///

现在在游戏循环中调用它

        ///
        /// Section 4: SDL ttf and rendering text
        ///

        // create a rectangle to update with the size of the rendered text
        SDL_Rect text_rect;

        // The color for the text we will be displaying
        SDL_Color white = {255, 255, 255, 0};

        // so we can have nice text, two lines one above the next
        render_text(rend, 10, 10, "Hello World!", font, &text_rect, &white);
        render_text(rend, 10, text_rect.y + text_rect.h, "Conan demo by JFrog", font, &text_rect, &white);

        // triggers the double buffers
        // for multiple rendering

这里要注意的是,我们编写的辅助函数在循环的每次迭代中重新创建相同的纹理。这不会影响我们的示例,但如果您的游戏变得复杂,这可能会非常低效。

simple game windows from SDL with a cube that moves and text render from a texture using SDL_ttf and TTF

5:清理和释放资源

在使用像 SDL 这样的 C 风格 API 时,最关键的步骤之一是内存管理,许多这些结构是在栈上分配的,我们需要释放它们。

    ///
    /// Section 5: Freeing resources
    ///

    // close font handle
    TTF_CloseFont(font);

    // close TTF
    TTF_Quit();

    // destroy texture
    SDL_DestroyTexture(tex);

    // destroy renderer
    SDL_DestroyRenderer(rend);

安装 SDL2 并设置您的项目

第一步是设置 CMake 来构建我们的项目。按照 SDL CMake 指南,我们会注意到有几个 CMake 目标,具体取决于我们使用库的静态或共享选项。

    cmake_minimum_required(VERSION 3.15)
    project(sdl-example CXX)

    find_package(SDL2 REQUIRED CONFIG)
    find_package(SDL2_image REQUIRED CONFIG)
    find_package(SDL2_ttf REQUIRED CONFIG)

    add_executable(sdl-example src/main.c)


    # Main SDL library for init
    target_link_libraries(${PROJECT_NAME} PRIVATE SDL2::SDL2main SDL2::SDL2-static)


    # SDL image to make a surface (aka what we'll render)
    target_link_libraries(${PROJECT_NAME} PRIVATE SDL2_image::SDL2_image-static)


    # SDL ttf so we can display hello world!
    target_link_libraries(${PROJECT_NAME} PRIVATE SDL2_ttf::SDL2_ttf)

现在,我们可以使用 Conan(一个 C 和 C++ 包管理器)来安装库。它不仅会安装 SDL2,还会安装所有必要的传递依赖项。Conan 从默认的 ConanCenter 远程获取这些包,这是开源 Conan 包的官方存储库。

使用 Conan 的原因是,虽然可以本地构建 SDL,但它有很多依赖项。例如 FreeType、libjpeg、libwebp 和 libdeflate 等等。不同的平台和不同的选项也需要不同的依赖项,这是 Windows MSVC 19.3 的 HTML 图形视图,使用下面的 conanfile.txt,您可以使用 conan graph info . --format=html > graph.html 生成任何项目的图形,看看它是什么样子!

conan graph info HTML formatted view of the SDL2 dependencies

要安装所需的 SDL 库,我们可以创建一个 conanfile.txt 文件,声明项目的依赖项。

[requires]
sdl_image/[~2.6]
sdl_ttf/[~2.0]
sdl/[~2.28]

[generators]
CMakeToolchain
CMakeDeps

[layout]
cmake_layout

第一部分 [requires] 列出了我们使用的 3 个 SDL 库:sdl、image 和 ttf。对于这些库,我们使用了 版本范围 来让 Conan 选择可用的最佳补丁版本(这就是 ~ 表示的含义)。这是一种有效的方法来处理 依赖项菱形问题,在使用 SDL 时,图形中确实存在此问题。例如,sdl/[~2.0] 表示任何介于 2.0.02.0.x 之间的 SDL 版本(但不包括 2.1.0)都是可以接受的。

我们将使用 CMakeDeps 为 CMake 的 find_package 生成配置文件,该配置文件用于 CMakeLists.txt。此外,还有 CMakeToolchain 用于生成构建系统所需的所有信息。另外,请注意,我们为项目声明了一个 [layout] 作为 cmake_layout,这将有助于在构建时保持项目文件夹的组织。您可以查看 Conan 文档中的 使用包教程部分 以获取更多信息。

conan install . --build=missing

使用 conan install 命令,我们将在本地安装所有必要的包,并生成构建应用程序所需的文件。请注意,我们使用了 --build=missing 参数,以防某些二进制文件无法从远程获取。此外,如果您正在运行 Linux 并且系统上缺少某些必要的缺失系统库,您可能需要将 -c tools.system.package_manager:mode=install-c tools.system.package_manager:sudo=True 参数添加到命令行中(文档参考)。

构建并运行我们的游戏

现在让我们构建项目并运行应用程序。如果您安装了 CMake>=3.23,则可以使用 CMake 预设

# Linux, macOS
cmake --preset conan-release
cmake --build --preset conan-release
cd build/Release
./sdl-example 
# Windows
cmake --preset conan-default
cmake --build --preset conan-release
cd build\Release
sdl-example.exe

否则,您可以为 CMake 添加必要的参数

# Linux, macOS
cmake . -G "Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=build/Release/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
cmake --build .
./sdl-example

# Windows. Assuming Visual Studio 17 2022 
# is your VS version and that it matches 
# your default profile
cmake . -G "Visual Studio 17 2022"
-DCMAKE_TOOLCHAIN_FILE=./build/generators/conan_toolchain.cmake
cmake --build . --config Release
sdl-example.exe

结论

SDL2 是一个功能强大的库,它具有易于使用的 API,使各种技能水平的 C 和 C++ 开发人员能够快速创建图形界面。

我们涵盖了安装 SDL、设置项目以及运行我们的第一个游戏!该游戏是一个很好的起点,包含基本窗口、图像、游戏控制和渲染循环,以及事件文本。这为我们提供了一个简单的“游戏”,我们可以使用它在窗口中四处移动我们的 Conan 2.0 方块。

如果您想继续学习,添加更多对象并检测交集将是一个很好的补充。

那么,您还在等什么呢?深入了解,尝试使用 SDL2,看看它如何与您的项目相结合!

额外资料