首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >用于小游戏的简单C++ SDL2包装器

用于小游戏的简单C++ SDL2包装器
EN

Code Review用户
提问于 2018-08-20 03:47:26
回答 2查看 2.4K关注 0票数 5

我正在为一个突破的克隆创建一个简单的SDL2包装器。该项目的真正学习目标是:

了解如何通过包装器正确地管理资源,了解更多关于SDL2的知识,我正在构建这些包装器,因为我正在学习Lazy的教程,并希望获得一些关于我正在做的事情以及我可以做的其他事情的反馈。

到目前为止,这是我的SDL_Surface包装器:

代码语言:javascript
复制
// Surface.h
#pragma once
#include <iostream>
#include <SDL.h>
#include <string>
#include "Window.h"


namespace SDL2 {
    class Surface
    {
    private:
        SDL_Surface* ScreenSurface = nullptr;
        bool IsWindowSurface = false;
        std::string ImagePath = "";

    public:
        // This function creates a Surface object associated with a Window (gets cleaned when Window is destroyed)
        Surface(SDL2::Window& window);
        Surface(std::string path);
        // Move constructor
        Surface(Surface&& source);
        // Disable copy constructor
        Surface(const Surface& source) = delete;
        ~Surface();
        // Disable copy by assignment
        Surface& operator=(const Surface& source) = delete;
        // Move assignment
        Surface& operator=(Surface&& source);
        void FillRect();
        void Update(SDL2::Window& window);
        bool LoadBMP();
        // TODO: Add more parameters at future SDL2 tutorial
        void BlitSurface(SDL2::Surface& destination);
    };
}



// Surface.cpp
#include "Surface.h"


SDL2::Surface::Surface(SDL2::Window& window) :
    ScreenSurface{ SDL_GetWindowSurface(window.GetSDLWindow()) },
    IsWindowSurface{ true }
{
}


SDL2::Surface::Surface(std::string path) :
    ImagePath{ path },
    IsWindowSurface{ false }
{
}

SDL2::Surface::Surface(Surface && source) :
    ScreenSurface{ source.ScreenSurface },
    IsWindowSurface{ source.IsWindowSurface },
    ImagePath{ source.ImagePath }
{
    source.ScreenSurface = nullptr;
    source.IsWindowSurface = false;
    source.ImagePath = "";
}


SDL2::Surface::~Surface()
{
    // If surface is not associated with Window, need free the surface with SDL2 call
    if (!IsWindowSurface) {
        SDL_FreeSurface(ScreenSurface);
    }
}


SDL2::Surface & SDL2::Surface::operator=(Surface && source)
{
    if (&source == this)
        return *this;

    // If surface is not associated with Window, need free the surface with SDL2 call
    if (!IsWindowSurface) {
        SDL_FreeSurface(ScreenSurface);
    }

    ScreenSurface = source.ScreenSurface;
    IsWindowSurface = source.ScreenSurface;
    ImagePath = source.ImagePath;

    // Return source to stable state
    source.ScreenSurface = nullptr;
    source.IsWindowSurface = false;
    source.ImagePath = "";

    return *this;

}

// TODO: Define SDL_Rect parameter and Color parameter (make wrappers for these)
void SDL2::Surface::FillRect()
{
    SDL_FillRect(ScreenSurface, NULL, SDL_MapRGB(ScreenSurface->format, 0x00, 0x00, 0xFF));
}


void SDL2::Surface::Update(SDL2::Window& window)
{
    SDL_UpdateWindowSurface(window.GetSDLWindow());
}


// This function is called after a non-window surface is created.  Check on this return value for success/fail.
bool SDL2::Surface::LoadBMP()
{
    // Load image from path
    ScreenSurface = SDL_LoadBMP(ImagePath.c_str());

    if (!ScreenSurface) {
        std::cout << "Unable to load image " << ImagePath << " SDL Error: " << SDL_GetError() << '\n';
        return false;
    }
    return true;
}


// The source Surface is `this`.  This function applies the image to the destination surface (though not updated on the screen)
void SDL2::Surface::BlitSurface(SDL2::Surface & destination)
{
    // Args: Source, FUTURE, Destination, FUTURE
    SDL_BlitSurface(this->ScreenSurface, NULL, destination.ScreenSurface, NULL);
}

任何指点都将不胜感激!

EN

回答 2

Code Review用户

回答已采纳

发布于 2018-08-20 08:56:42

像这样包装SDL_Surface以在析构函数中实现自动清理是个好主意。但是,当前的实现做出了一些不一定正确的假设。

曲面创建

有很多方法可以抓住一个表面。从Window获取它,从文件中加载,用SDL_CreateSurface手动创建它,或者使用SDL_TTF之类的东西来呈现字体。

Surface类不应该关心曲面是如何创建的。它只需接受一个表面,并知道是否应在销毁时清理该表面,例如:

代码语言:javascript
复制
Surface(SDL_Surface* surface, bool freeOnDestruction);

应该替换当前的Windowstring构造函数。可以使LoadBMP函数成为接受string并返回Surface的空闲函数。

窗口

SurfaceWindow的依赖似乎是反向的。由于SDL_Window拥有自己的SDL_Surface,因此依赖关系反过来就更有意义了。因此,Window类可以有一个GetSurface()函数,返回一个Surface,而Update()函数作为Window类中的UpdateSurface()更有意义(对于非窗口表面来说,这是没有意义的)。

接口与

错误校验

BlitSurface令人困惑,因为您必须记住是调用source.BlitSurface还是dest.BlitSurface。作为成员函数,最好同时使用BlitFrom(source)BlitTo(dest),它们都调用单个Blit函数。

最好使用免费函数来代替:

代码语言:javascript
复制
void FillSurface(Surface& surface, Color const& color); // fill entire surface!
void FillSurface(Surface& surface, Rect const& rect, Color const& color); // fill a rect on the surface
void BlitSurface(Surface& source, Surface const& dest); // blit whole surfaces
// TODO: rect versions...

SDL_BlitSurface有一些条件,您应该在检查之前和之后检查(它不工作在锁定/空表面,它返回一个错误代码失败)。这些应该被处理并输出一条消息/抛出一个错误/终止程序。

GetSDLSurface()类中有一个Surface函数(类似于GetSDLWindow()函数)是有意义的。在返回表面指针之前,我们可以检查(断言/抛出/打印消息)该函数中的表面指针是否为非空指针。如果BlitFill函数是空闲函数,而不是成员,则它们不能直接访问私有成员,必须使用该函数。这意味着当使用曲面时,它们将自动执行非空错误检查。

使用标准库

C++标准库已经包含了一个类来管理对象的生存期,比如:std::unique_ptr

与检查是否必须释放复制/移动构造函数中的曲面不同,我们可以使用自定义删除器将其包装在unique_ptr中。然后,我们可以使用默认的move构造函数并移动操作符:

代码语言:javascript
复制
#include <iostream>
#include <functional>
#include <memory>
#include <cassert>

// placeholder for testing...
struct SDL_Surface
{
    SDL_Surface() { std::cout << "constructor" << std::endl; }
    ~SDL_Surface() { std::cout << "destructor" << std::endl; }
};

// placeholder for testing...
void SDL_FreeSurface(SDL_Surface* surface)
{
    delete surface;
}

class Surface
{
public:

    Surface(SDL_Surface* sdlSurface, bool freeOnDestruction);

    Surface(Surface&&) = default;
    Surface& operator=(Surface&&) = default;

    Surface(Surface const&) = delete;
    Surface& operator=(Surface const&) = delete;

    SDL_Surface* GetSDLSurface() const; // Blit / Fill etc. should use this to get the SDL_Surface.

private:

    using SDLSurfacePtrDeleterT = std::function<void(SDL_Surface*)>;
    using SDLSurfacePtrT = std::unique_ptr<SDL_Surface, SDLSurfacePtrDeleterT>;
    SDLSurfacePtrT m_sdlSurfacePtr;
};

Surface::Surface(SDL_Surface* sdlSurface, bool freeOnDestruction)
{
    assert(sdlSurface != nullptr); // no null surfaces!
    // (means we don't have to check the inner pointer every time we e.g. blit).

    auto deleter = freeOnDestruction ?
        SDLSurfacePtrDeleterT([] (SDL_Surface* surface) { SDL_FreeSurface(surface); }) :
        SDLSurfacePtrDeleterT([] (SDL_Surface*) { });

    m_sdlSurfacePtr = SDLSurfacePtrT(sdlSurface, deleter);
}

SDL_Surface* Surface::GetSDLSurface() const
{
    assert(m_sdlSurfacePtr != nullptr); // check this surface hasn't been moved from
    // (if the m_sdlSurfacePtr is valid, the inner surface should be valid (or at least non-null) due to the assert in the constructor).

    return m_sdlSurfacePtr.get();
}

int main()
{
    std::cout << "Construct + Destruct:" << std::endl;

    {
        auto sdlSurface = new SDL_Surface();
        auto surface = Surface(sdlSurface, true);
        // should get destructor call...
    }

    std::cout << "Construct + Leak:" << std::endl;

    {
        auto sdlSurface = new SDL_Surface();
        auto surface = Surface(sdlSurface, false);
        // will not get destructor call...
    }

    std::cout << "Construct + Move + Destruct:" << std::endl;

    {
        auto sdlSurface = new SDL_Surface();
        auto surface = Surface(sdlSurface, true);
        auto surface2 = std::move(surface);
        // should get destructor call...
    }
}
票数 6
EN

Code Review用户

发布于 2018-08-20 21:50:21

代码语言:javascript
复制
#pragma once

请注意,您在这里放弃了可移植性,因为这是一个常见的非标准编译器扩展。对于几乎所有的应用程序,只要使用支持它的实现,无论是物理上还是逻辑上都不复制文件,而且文件系统不会触发假阳性,那么#杂注一次就可以了。否则,坚持标准,包括警卫,并作出一些努力,以区别警卫的名称。

代码语言:javascript
复制
#include <iostream>
#include <SDL.h>
#include <string>
#include "Window.h"

标题不应依赖于首先包含的其他标头。确保这一点的一种方法是在任何其他标头之前包含标头。

可以避免潜在的使用错误,方法是确保组件的.h文件本身解析-没有外部提供的声明或定义.将.h文件作为.c文件的第一行,可以确保.h文件中不缺少组件物理接口所固有的任何关键信息(或者,如果存在,您将在编译.c文件时立即找到该信息)。

也就是说,您的包含应该按照以下顺序进行:

  1. 原型/接口(表面)
  2. 项目/专用标题(window.h)
  3. 来自非标准、非系统库(QT、特征等)的头.
  4. 标准C++头(向量、字符串、cstdint等)
  5. 系统头(windows.h、dirent.h等)

当您向下移动列表时,库更稳定,使用范围更广(因此对其进行了测试)。进一步排序每个子组,例如按路径/名称按字典顺序排列,使维护人员更容易在包含列表变长时快速找到包含。

不要在头文件中使用#include <iostream>。许多C++实现透明地将静态构造函数插入到包含<iostream>的每个转换单元中,即使客户机从未使用<iostream>工具。

代码语言:javascript
复制
        bool IsWindowSurface = false;
        std::string ImagePath = "";

每当您有与其他值的存在相关联的布尔值时,请使用optional类型(助推Mnmlstc愚昧C++17)。

你的表面真的需要知道它是从哪里创建的吗?

代码语言:javascript
复制
        Surface(std::string path);

复制std::string可能会很昂贵。通过引用const传递此输入参数。

代码语言:javascript
复制
SDL2::Surface::Surface(std::string path) :
    ImagePath{ path },
    IsWindowSurface{ false } {}

如果要将参数视为接收器参数,则将std::move(path)转换为ImagePath

代码语言:javascript
复制
bool SDL2::Surface::LoadBMP() {
    // Load image from path
    ScreenSurface = SDL_LoadBMP(ImagePath.c_str());

如果表面已经存在,这里会发生什么?如果该曲面是一个窗口表面副本,则泄漏旧表面,ScreenSurface将指向null,因为ImagePath是空的。如果源是以前的BMP,那么旧的表面仍然泄漏和ScreenSurface指向清洁版本的BMP。

与其将每个函数包装到RAII对象中,不如使用std::unique_ptr来管理生存期对象类型。只要您不使用无法优化的析构函数实例来构造std::unique_ptr,那么std::unique_ptr实际上是免费的。

对于所有的资源,您可能已经注意到创建资源并检查句柄以确保其创建的模式。我们可以创建一个通用助手来创建这些资源类型。

代码语言:javascript
复制
template<typename Result, typename Creator, typename... Arguments>
auto make_resource(Creator c, Arguments&&... args)
{
    auto r = c(std::forward<Arguments>(args)...);
    if (!r) { throw std::system_error(errno, std::generic_category()); }
    return Result(r);
}

现在,我们需要一种在不增加成本的情况下将std::unique_ptr作为删除器的方法。我们可以将其包装成一个常量值,并在std::unique_ptr请求时返回一个删除函数。

代码语言:javascript
复制
template <typename T, std::decay_t<T> t>
struct constant_t {
    constexpr operator T() noexcept const { return t; }
}

使用上述两种帮助,我们可以大量生产资源。首先定义指针类型。

代码语言:javascript
复制
using surface_ptr = std::unique_ptr<
    SDL_Surface, constant_t<decltype(SDL_FreeSurface), SDL_FreeSurface>>;

那么工厂就开始运作了。

代码语言:javascript
复制
inline auto make_surface(const char* filename) {
  return make_resource<surface_ptr>(SDL_LoadBMP, filename);
}

inline auto make_surface(SDL_Window* ptr) {
  return make_resource<surface_ptr>(SDL_GetWindowSurface, ptr);
}

inline auto make_surface(
    std::uint32_t flags, int width, int height, int depth, 
    std::uint32 Rmask, std::uint32 Gmask, std::uint32_t Bmask, std::uint32_t Amask) {
  return make_resource<surface_ptr>(
      SDL_CreateRGBSurface, flags, width, height, depth, 
      Rmask, Gmask, Bmask, Amask);
}

然后,您可以在现有的SDL库中使用此surface_ptr

代码语言:javascript
复制
void meow() {
    auto w = make_window("Purr", SDL_WINDOW_POS_UNDEFINED, SDL_WINDOW_POS_UNDEFINED, 
                              640, 480, 0));
    auto s = make_surface(w.get());
    SDL_FillRect(s.get(), NULL, SDL_MapRGB(s->format, 255, 0, 0));
    SDL_UpdateWindowSurface(w.get());
}

注:上面的constant_t助手对于C++11是必需的,因为存在一个缺陷,std::integral_constant没有被计算为constexpr表达式。使用C++14,您可以执行以下操作

代码语言:javascript
复制
template <typename T, std::decay_t<T> t>
using constant_t = std::integral_t<T, t>;

using surface_ptr = std::unique_ptr<SDL_Surface, constant_t<decltype(SDL_FreeSurface)*, SDL_FreeSurface>>;

使用C++17允许使用auto的非类型模板参数,可以消除冗长的内容。

代码语言:javascript
复制
template <auto t>
using constant_t = std::integral_constant<std::decay_t<decltype(t)>, t>;

using surface_ptr = std::unique_ptr<SDL_Surface, constant_t<SDL_FreeSurface>>;
票数 4
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/202012

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档