Диспетчер ресурсов с СФМЛ


Я решил написать 2D-игру с СФМЛ, и в начале игры я писал классы для управления ресурсами, как звуки, музыка, текстуры... Хотелось бы получить комментарии на мой код: как бы вы написать программу, как я могу улучшить его?

Я также написал класса "логгер", чтобы сделать отладку легче игру. Хотелось бы получить отзывы на этот класс.

Объект ResourceManager.ГЭС

#ifndef RESOURCE_MANAGER_HPP
#define RESOURCE_MANAGER_HPP

#include "FontManager.hpp"
#include "MusicManager.hpp"
#include "TextureManager.hpp"
#include "SoundManager.hpp"

class ResourceManager
{
public:
    static ResourceManager& getInstance();

    void loadResources();

private:
    ResourceManager();

    ~ResourceManager();

    FontManager& fontManager = FontManager::getInstance();
    MusicManager& musicManager = MusicManager::getInstance();
    TextureManager& textureManager = TextureManager::getInstance();
    SoundManager& soundManager = SoundManager::getInstance();
};

#endif // RESOURCE_MANAGER_HPP

ResourceManager.cpp

#include "ResourceManager.hpp"

ResourceManager::ResourceManager()
{

}

ResourceManager::~ResourceManager()
{

}

ResourceManager& ResourceManager::getInstance()
{
    static ResourceManager instance;
    return instance;
}

void ResourceManager::loadResources()
{
    musicManager.addMusic("music1", "music1.wav");
    musicManager.addMusic("music2", "music2.wav");
    fontManager.addFont("font1", "font1.ttf");
    fontManager.addFont("font2", "font2.ttf");
    textureManager.addTexture("texture1", "texture1.png");
    soundManager.addSound("sound1", "sound1.wav");
}

SoundManager.ГЭС

#ifndef SOUND_MANAGER_HPP
#define SOUND_MANAGER_HPP

#include "Logger.hpp"
#include <unordered_map>
#include <SFML/Audio.hpp>

class SoundManager
{
public:
    static SoundManager& getInstance();

    int addSound(const std::string& id, const std::string& soundPath);

    std::unique_ptr<sf::Sound> getSound(const std::string &id);

private:
    SoundManager();

    ~SoundManager();

    std::unordered_map<std::string, std::unique_ptr<sf::Sound>> soundMap_;
    std::vector<sf::SoundBuffer> soundBufferVec_;

};

#endif // SOUND_MANAGER_HPP

SoundManager.cpp

#include "SoundManager.hpp"

SoundManager::SoundManager()
{

}

SoundManager::~SoundManager()
{

}

SoundManager& SoundManager::getInstance()
{
    static SoundManager instance;
    return instance;
}

int SoundManager::addSound(const std::string& id, const std::string& soundPath)
{
    auto it = soundMap_.find(id);
    if(it != soundMap_.end())
    {
        Logger::getInstance().log(LogLevel::ERROR, "Unable to add sound: '"
            + id + "' already exists");
        return -1;
    }

    std::unique_ptr<sf::SoundBuffer> soundBuffer = 
        std::make_unique<sf::SoundBuffer>();

    if(!soundBuffer->loadFromFile(soundPath))
    {
        Logger::getInstance().log(LogLevel::ERROR, "Unable to open sound '" + 
            soundPath + "'");
        return -1;
    }
    soundBufferVec_.push_back(std::move(*(soundBuffer)));
    unsigned int soundBufferVecSize = soundBufferVec_.size();
    std::unique_ptr<sf::Sound> sound = std::make_unique<sf::Sound>
        (std::move(soundBufferVec_[soundBufferVecSize - 1]));

    soundMap_.emplace(id, std::move(sound));

    Logger::getInstance().log(LogLevel::INFO, "'" + soundPath + 
        "' has been added");
    return 0;
}

std::unique_ptr<sf::Sound> SoundManager::getSound(const std::string &id)
{
    auto it = soundMap_.find(id);
    if(it == soundMap_.end())
    {
        Logger::getInstance().log(LogLevel::ERROR, "Unable to load sound: " +
            id + " doesn't exist");
        return nullptr;
    }

    return std::move(it->second);
}

TextureManager.ГЭС

#ifndef TEXTURE_MANAGER_HPP
#define TEXTURE_MANAGER_HPP

#include "Logger.hpp"
#include <unordered_map>
#include <SFML/Graphics.hpp>

class TextureManager
{
public:
    static TextureManager& getInstance();

    int addTexture(const std::string& id, const std::string& texturePath);

    std::unique_ptr<sf::Texture> getTexture(const std::string &id);

private:
    TextureManager();

    ~TextureManager();

    std::unordered_map<std::string, std::unique_ptr<sf::Texture>> textureMap_;
};

#endif // TEXTURE_MANAGER_HPP

TextureManager.cpp

#include "TextureManager.hpp"

TextureManager::TextureManager()
{

}

TextureManager::~TextureManager()
{

}

TextureManager& TextureManager::getInstance()
{
    static TextureManager instance;
    return instance;
}

int TextureManager::addTexture(const std::string& id, 
    const std::string& texturePath)
{
    auto it = textureMap_.find(id);
    if(it != textureMap_.end())
    {
        Logger::getInstance().log(LogLevel::ERROR, "Unable to add texture: '"
            + id + "' already exists");
        return -1;
    }

    std::unique_ptr<sf::Texture> texture = std::make_unique<sf::Texture>();

    if(!texture->loadFromFile(texturePath))
    {
        Logger::getInstance().log(LogLevel::ERROR, "Unable to open texture '"
            + texturePath + "'");
        return -1;
    }

    textureMap_.emplace(id, std::move(texture));

    Logger::getInstance().log(LogLevel::INFO, "'" + texturePath + 
        "' has been added");
    return 0;
}

std::unique_ptr<sf::Texture> TextureManager::getTexture(const std::string &id)
{
    auto it = textureMap_.find(id);
    if(it == textureMap_.end())
    {
        Logger::getInstance().log(LogLevel::ERROR, "Unable to load texture: " +
            id + " doesn't exist");
        return nullptr;
    }

    return std::move(it->second);
}

Логгер.ГЭС

#ifndef LOGGER_HPP
#define LOGGER_HPP

#include <sstream>
#include <iostream>

enum class LogLevel
{
    NO = 0,
    ERROR = 1,
    INFO = 2,
    DEBUG = 3
};

class Logger
{
public:
    static Logger& getInstance();

    void setLogLevel(const LogLevel& logLevel);

    void log(const LogLevel& messageLevel, const std::string& message);


private:
    Logger();

    ~Logger();

    void setPrefix(const LogLevel& messageLevel);

    std::string logPrefix_;
    LogLevel currentLogLevel_;
    LogLevel messageLevel_;

};

#endif // LOGGER_HPP

Logger.cpp

#include "Logger.hpp"

Logger::Logger()
{

}

Logger::~Logger()
{

}

void Logger::setPrefix(const LogLevel& messageLevel)
{
    switch(messageLevel)
    {
        case LogLevel::DEBUG:
            logPrefix_ = "[DEBUG] ";
            break;
        case LogLevel::INFO:
            logPrefix_ = "[INFO] ";
            break;
        case LogLevel::ERROR:
            logPrefix_ = "[ERROR] ";
            break;
        default:
            break;
    }
}

Logger& Logger::getInstance()
{
    static Logger instance;
    return instance;
}

void Logger::setLogLevel(const LogLevel& logLevel)
{
    currentLogLevel_ = logLevel;
}


void Logger::log(const LogLevel& messageLevel, const std::string& message)
{
    if(messageLevel == LogLevel::NO)
        return;
    if(currentLogLevel_ != LogLevel::DEBUG)
    {
        if(messageLevel == LogLevel::ERROR && currentLogLevel_ !=
            LogLevel::ERROR)
            return;
        if(messageLevel == LogLevel::INFO && currentLogLevel_ !=
            LogLevel::INFO)
            return;
    }

    setPrefix(messageLevel);

    std::cout << logPrefix_ << message << std::endl;
}

Как использовать эти классы

main.cpp

#include <iostream>
#include "Logger.hpp"
#include "ResourceManager.hpp"

int main()
{
    Logger::getInstance().setLogLevel(LogLevel::DEBUG);
    ResourceManager& resourceManager = ResourceManager::getInstance();
    resourceManager.loadResources();

    std::unique_ptr<sf::Texture> texture = TextureManager::getInstance().
        getTexture("texture1");
    if(texture == nullptr)
        return -1;

    sf::RenderWindow window(sf::VideoMode(800, 600), "Window");
    sf::Sprite sprite;
    sprite.setTexture(*(texture));

    std::unique_ptr<sf::Sound> sound = SoundManager::getInstance().
        getSound("sound1");
    if(sound == nullptr)
        return -1;

    while(window.isOpen())
    {
        sf::Event event;
        while(window.pollEvent(event))
        {
            if(event.type == sf::Event::Closed)
                window.close();
            else if(event.type == sf::Event::KeyPressed)
                sound->play();

        }

        window.clear(sf::Color::Black);
        window.draw(sprite);
        window.display();
    }

    return 0;
}

Я не выкладываю в двух классах-менеджер (FontManager и MusicManager), так как код является в основном такой же как SoundManager и TextureManager.



873
5
задан 11 апреля 2018 в 01:04 Источник Поделиться
Комментарии
2 ответа

Дизайн - Одиночек

Одноэлементный может быть разумный выбор для регистратора (он используется везде и там должно быть только одно из них).

Но для других систем, то лучше придерживаться обычных экземпляров класса, и передают ссылки на них по мере необходимости. Это может показаться больше, но это делает более четкие зависимости. Это также делает время жизни, а порядок инициализации и разрушения системы хорошо определены.

Различные менеджеры могут просто быть открытыми членами ResourceManager.


Код - SoundBuffer

Загрузка SoundBuffer можно упростить немного:

sf::SoundBuffer soundBuffer;

if(!soundBuffer.loadFromFile(soundPath))
{
Logger::getInstance().log(LogLevel::ERROR, "Unable to open sound '" +
soundPath + "'");
return -1;
}

soundBufferVec_.push_back(std::move(soundBuffer));

std::unique_ptr<sf::Sound> sound = std::make_unique<sf::Sound>(soundBufferVec_.back());

Однако, из СФМЛ документы похоже SoundManager следует хранить sf::SoundBufferС, не sf::SoundС. В sf::SoundBuffer супертяжелый объект, требующий много времени для загрузки, в то время как sf::Sound может иметь более короткую продолжительность жизни, и быть привязана к буфер по мере необходимости.


Код - Значения Ошибок

int add*(id, path);

Не возвращают целые числа в качестве индикатора того, что работал или нет. Логическое будет лучше, так как он имеет только два возможных значения, определяющее успех или провал (похоже СФМЛ именно так).

Но даже тогда это не явно в коде, что истинно или ложно имею в виду, так enum class LoadResult { LoadSuccess, LoadFailure }; может быть в дальнейшем совершенствовании.

Или... так ResourceManager::loadResources() игнорировать коды возврата, мы могли бы просто возвращать Void!


Дизайн - Удаление Дубликатов Кода

Если SoundManager меняется в магазине sf::SoundBuffersкак сказано выше, то единственное существенное различие между менеджером классы add* функция.

Очевидное исправление для этой проблемы является использование одного шаблона AssetManager (или аналогичным названием) класса.

Вместо того, чтобы определять различные add функции-члены для разных типов (это может быть сделано с помощью шаблона специализации), это проще, чтобы переместить фактическую нагрузку логике выходит AssetManager класс, оставив его как оболочку вокруг карте:

template<class AssetT>
class AssetManager
{
public:

void addAsset(const std::string& id, std::unique_ptr<AssetT> asset);
std::unique_ptr<AssetT> getAsset(const std::string& id);

private:

std::unordered_map<std::string, std::unique_ptr<AssetT>> assetMap_;
};

template<class AssetT>
void AssetManager<AssetT>::addAsset(const std::string& id, std::unique_ptr<AssetT> asset)
{
// check it's not already there, then add it to the map
}

template<class AssetT>
std::unique_ptr<AssetT> AssetManager<AssetT>::getAsset(const std::string& id)
{
// find and return / log error
}

Экземпляры каждого типа управляющей компании могут быть членами ResourceManager:

    AssetManager<sf::Texture> textureManager;
AssetManager<sf::SoundBuffer> soundManager;
// ...

Тогда мы можем определить функции-члены в ResourceManager для фактического актива загрузки и вызова addAsset() на соответствующего руководителя.

void ResourceManager::loadTexture(const std::string& id, const std::string& path)
{
// load as before
// add texture to the relevant asset manager
}

Обратите внимание, что код, используя этот актив всегда знает, какой тип активов его ожидает, поэтому он может позвонить getAsset() С правильным типом:

auto texture = resourceManager.textureManager.getAsset<sf::Texture>("id");


Дизайн - Тип Указателя

Что делать, если вы хотите использовать ту же текстуру (или что там) в более чем одном месте? На данный момент актив хранится в std::unique_ptrи в getFoo() право собственности на него передается из FooManager.

Хотя запись по-прежнему присутствует в карте, если вы спросите, Что же актива во второй раз, он даст вам пустой указатель.

Чтобы исправить это, менеджер должен либо вернуть не владея сырой указатель (и сохранить право собственности на std::unique_ptr) или мы можем использовать std::shared_ptr вместо. С std::shared_ptrколичество общих указателей на актив учитывается, и объект только уничтожается, когда самый последний (наверное еще в управляющей компании) истекает. например:

template<class AssetT>
std::shared_ptr<AssetT> AssetManager<AssetT>::getAsset(const std::string& id)
{
// find or error...

return it->second; // note no move... we create another shared pointer pointing to the same object
}


Дизайн - С Удовольствие++

Выше устраняет дублирование кода в менеджеры, и нет никакой реальной необходимости, чтобы идти дальше. Смело игнорируйте все, с этого момента!

std::shared_ptr еще одна особенность мы можем воспользоваться. Создание std::shared_ptr<void> аналогично void* в с. Он может хранить объекты любого типа.

Это означает, что вместо создания нескольких различных AssetManager экземплярах (по одному для каждого типа актива) мы можем изменить код, чтобы иметь только один AssetManager экземпляр, который может хранить активов любого типа.

Сниппет ниже приведен пример такого класса от одного из моих собственных проектов:

namespace Assets
{

class Cache
{
public:

Cache() = default;

PIC_NOT_COPYABLE(Cache); // deletes copy constructor and copy assignment operator
PIC_NOT_MOVEABLE(Cache); // deletes move constructor and move assignment operator

template<class T>
void Add(ID const& id, std::shared_ptr<T> const& t);

template<class T>
std::shared_ptr<T> Find(ID const& id);

void Clear();

private:

struct TypeID
{
std::type_index TypeIndex;
ID ID;
};

struct TypeIDHash
{
std::size_t operator()(TypeID const& typeID) const;
};

struct TypeIDEquality
{
bool operator()(TypeID const& a, TypeID const& b) const;
};

using DataT = std::unordered_map<TypeID, std::shared_ptr<void>, TypeIDHash, TypeIDEquality>;
DataT m_data;
};

template<class T>
void Cache::Add(ID const& id, std::shared_ptr<T> const& t)
{
PIC_ASSERT(t != nullptr);

auto entry = m_data.emplace(TypeID{ std::type_index(typeid(T)), id }, t);

PIC_ASSERT(entry.second);
}

template<class T>
std::shared_ptr<T> Cache::Find(ID const& id)
{
auto entry = m_data.find({ std::type_index(typeid(T)), id });

if (entry == m_data.end())
return std::shared_ptr<T>();

return std::static_pointer_cast<T>(entry->second);
}

} // Assets

Основное различие заключается в том, что мы пара актив код строки std::type_index в ключе карте. Это необходимо, чтобы найти правильный тип актива позже, как мы уже отброшены тип информации, выбрав std::shared_ptr<void>. Она также позволяет активов разных типов, чтобы иметь тот же идентификатор и сохранить отдельные записи в карте.

Функции нагрузки остаются похожими на те, выше, но все они загружены актива в один и тот же экземпляр кэша.

6
ответ дан 12 апреля 2018 в 08:04 Источник Поделиться

Не синглтоны

Паттерн синглтон пытается решить ровно одна проблема, и это, чтобы убедиться, что вы всегда только один экземпляр объекта. Проблема с ваш код, что данное требование не нужны. Зачем есть только один менеджер ресурсов? Есть какие-то критические проблемы, когда есть более чем один?

Как такой паттерн синглтон вот это неправильно, чтобы предоставить один глобальный доступ к менеджерам, который ведет прямо в следующем разделе.

Нет глобального доступа

Хотя это отличная идея, чтобы иметь глобальный доступ к менеджерам, как вы можете получить к ним доступ везде, где вы нуждаетесь в них, в долгосрочной перспективе это вызовет больше проблем, чем решает, поскольку вы не можете отслеживать состояние управляющих.

Будучи в состоянии легко рассуждать о потоке кода и государств, участвующих объектов становится более важной, чем больше проект получает. Если один отключен кусок кода может иметь побочные эффекты, на другой кусок кода, а затем будучи в состоянии определить причину состояния очень трудно, и вы можете в конечном итоге с ошибками, Что взять тебя навсегда для отладки.

Лучшим подходом является внедрение зависимостей, вы передайте менеджеров через конструктор, чтобы все классы, которые нуждаются в доступе к. Это внесет ясность в поток кода и как преимущество делать так, вам придется пересмотреть некоторые из Ваших дизайнерских решений, ведь вы действительно не хотите, чтобы впрыснуть менеджеры везде, но только в тех местах, где они действительно нужны. В целом по классам развязки, вы будете в конечном итоге с лучшим дизайном.

Один полезный инструмент для того, чтобы передать менеджерам каждый объект, чтобы использовать шаблон фабрики. Объект/субъект управления имеет доступ к менеджерам ресурса и создают объекты/субъекты и как таковые могут обеспечить каждое подразделение, ресурс напрямую, без необходимости проходить весь менеджер.

std::unique_ptr означает владение

При возврате ресурсов, вы на самом деле возвращается std::unique_ptrно unique_ptr не очень владеет объект, как таковой, возвращая unique_ptr не ты рвешь судовладельца от менеджера, что делает менеджер не менеджер, а "загрузчик".

Я думаю, что есть еще ошибки, связанные с этим, потому что когда вы делаете std::move(it->second) ты реально извлекать ресурс из Manager, поэтому если вы никогда не пробовали получить доступ к тому же ресурсу снова, вы будете работать в переехал-от объекта а к неопределенному поведению.

Лучше на самом деле возвращает ссылку на ресурс и оставить в своей собственности с менеджером. Кроме того, вы также можете воспользоваться общей собственности с std::shared_ptr. Тогда объект будет жить пока как-то еще использовать экземпляр shared_ptr.

Обработка ошибок

Это очень упрямый темы, как таковой, если вы чувствуете себя уверенно с вашим текущим решением может придерживаться его. В своей книге, вы либо функции, которые вы знаете, может не в одну сторону, в этом случае можно использовать логический качестве возвращаемого типа. Или у вас есть функции, которые не очень плохо, но когда есть что-то, что может пойти не так, что это вне вашего контроля (например, файл не существует), вы бросаете исключение. Кроме того, вы можете предоставить чеки, чтобы пользователь кода на самом деле можно сначала проверить, если что-то работает, и не просто попробовать, а потом реагировать на него.

Примеры

Как большинство пользователей СФМЛ закончим написанием или иной форме администратора ресурса рекомендуется проверить то, что другие создали в прошлом. В СФМЛ Разработка игр книга имеет аккуратный реализации ResourceHolder. Поскольку тот же автор также сопровождающий Тора, вы также найдете очень похожую реализацию в ТОР.

Лесозаготовки

Опять же, синглтон использование действительно не имеет смысла здесь, так почему вы должны быть в состоянии иметь несколько экземпляров регистратор?

Лично я считаю, что лучше всего пойти с существующей библиотеки в области лесозаготовок, поскольку они обеспечивают необходимую оптимизаций и оптимальных подходов к ведению журналов (файл, консоль, труба и т. д.). В одном из моих последних проектов я использовал spdlog. Это простота установки и, кажется, хорошо работать.

6
ответ дан 12 апреля 2018 в 09:04 Источник Поделиться