Как мне структурировать расширяемую систему загрузки активов?

19

Для движка хобби в Java я хочу написать простой, но гибкий менеджер ресурсов / ресурсов. Активы - это звуки, изображения, анимация, модели, текстуры и так далее. После нескольких часов просмотра и экспериментов с кодом я все еще не уверен, как создать эту вещь.

В частности, я ищу способ разработки менеджера таким образом, чтобы он абстрагировался от того, как загружаются конкретные типы активов и откуда загружаются активы. Я хотел бы иметь возможность поддерживать как файловую систему, так и хранилище СУБД, и остальная часть программы не должна знать об этом. Точно так же я хотел бы добавить актив описания анимации (FPS, фреймы для рендеринга, ссылку на изображение спрайта и т. Д.), Который является XML. Я должен быть в состоянии написать класс для этого с функциональностью, чтобы найти и прочитать файл XML, а также создать и вернуть AnimationAssetкласс с этой информацией. Я ищу управляемый данными дизайн.

Я могу найти много информации о том, что должен делать менеджер активов, но не о том, как это сделать. Используемые генерики, кажется, приводят к некоторой форме каскадирования классов или некоторой форме вспомогательных классов. Однако я не видел четкого примера, который не выглядел бы как личный взлом или точка согласия.

user8363
источник

Ответы:

23

Я бы начал с того, что не думал о менеджере активов . Размышления о вашей архитектуре в произвольно определенных терминах (например, «менеджер») имеют тенденцию позволять вам мысленно скрывать многие детали под ковриком, и, следовательно, становится труднее найти решение.

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

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

interface ITypeLoader {
  object Load (Stream assetStream);
}

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

Ваш основной загрузчик ресурсов должен иметь возможность регистрировать и отслеживать следующие типовые загрузчики:

class AssetLoader {
  public void RegisterType (string key, ITypeLoader loader) {
    loaders[key] = loader;
  }

  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

Используемый здесь «ключ» может быть любым, и вам не обязательно должна быть строка, но с нее легко начать. Ключ будет влиять на то, как вы ожидаете, что пользователь идентифицирует конкретный актив, и будет использоваться для поиска соответствующего загрузчика. Поскольку вы хотите скрыть тот факт, что реализация может использовать файловую систему или базу данных, у вас не может быть пользователей, обращающихся к ресурсам по пути файловой системы или чему-либо подобному.

Пользователи должны ссылаться на актив с минимальным количеством информации. В некоторых случаях достаточно одного только имени файла, но я обнаружил, что часто желательно использовать пару тип / имя, чтобы все было очень явно. Таким образом, пользователь может ссылаться на именованный экземпляр одного из ваших файлов анимации XML как "AnimationXml","PlayerWalkCycle".

Здесь AnimationXmlбудет ключ, под которым вы зарегистрированы AnimationXmlLoader, который реализует IAssetLoader. Очевидно, PlayerWalkCycleидентифицирует конкретный актив. По имени типа и имени ресурса ваш загрузчик ресурсов может запросить в своем постоянном хранилище необработанные байты этого ресурса. Поскольку здесь мы стремимся к максимальной общности, вы можете реализовать это, передав загрузчику средство доступа к хранилищу при его создании, позволяя вам заменить носитель на что-нибудь, что позже может предоставить поток:

interface IAssetStreamProvider {
  Stream GetStream (string type, string name);
}

class AssetLoader {
  public AssetLoader (IAssetStreamProvider streamProvider) {
    provider = streamProvider;
  }

  object LoadAsset (string type, string name) {
    var loader = loaders[type];
    var stream = provider.GetStream(type, name);

    return loader.Load(stream);
  }

  public void RegisterType (string type, ITypeLoader loader) {
    loaders[type] = loader;
  }

  IAssetStreamProvider provider;
  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

Очень простой поставщик потока просто ищет в указанном корневом каталоге ресурсов подкаталог с именем, typeзагружает необработанные байты указанного файла nameв поток и возвращает его.

Короче говоря, у вас есть система, в которой:

  • Есть класс, который знает, как читать необработанные байты из некоторого внутреннего хранилища (диск, база данных, сетевой поток и т. Д.).
  • Есть классы, которые знают, как превратить необработанный поток байтов в определенный тип ресурса и возвращать его.
  • У вашего фактического «загрузчика ресурсов» просто есть набор вышеупомянутых и он знает, как передать выходные данные поставщика потоков в загрузчик для конкретного типа и, таким образом, создать конкретный ресурс. Предоставляя способы настройки поставщика потоков и загрузчиков для конкретного типа, вы получаете систему, которая может быть расширена клиентами (или вами) без необходимости изменения фактического кода загрузчика ресурсов.

Некоторые предостережения и заключительные замечания:

  • Приведенный выше код в основном на C #, но должен переводиться практически на любой язык с минимальными усилиями. Чтобы облегчить это, я пропустил много вещей, таких как проверка ошибок или правильное использование, IDisposableи другие идиомы, которые могут не применяться напрямую в других языках. Те оставлены в качестве домашней работы для читателя.

  • Точно так же я возвращаю конкретный актив, как objectуказано выше, но вы можете использовать шаблоны или шаблоны или что-то еще, чтобы создать более конкретный тип объекта, если вам нравится (вам следует, с ним приятно работать).

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

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


источник
1
Хороший вопрос и хороший ответ, который направляет решение не только на проектирование, основанное на данных, но и на то, как начать мыслить на основе данных.
Патрик Хьюз
Очень хороший и подробный ответ. Мне нравится, как вы интерпретировали мой вопрос и сказали мне именно то, что мне нужно было знать, пока я так плохо сформулировал. Благодарность! Случайно, не могли бы вы указать мне некоторые ресурсы о потоках?
user8363
«Поток» - это просто последовательность (потенциально без определяемого конца) байтов или данных. Я специально думал о C # Stream , но вы, вероятно, больше интересуетесь потоковыми классами Java - хотя, предупреждаю, я не знаю слишком много Java, так что это может быть не идеальный класс для использования.
Потоки, как правило, с состоянием, в том смысле, что данный объект потока обычно имеет текущую позицию чтения или записи в потоке, и любой ввод-вывод, который вы выполняете, происходит с этой позиции - поэтому я использовал их в качестве входных данных для интерфейсов ресурсов выше, потому что они, по сути, говорят: «Вот некоторые необработанные данные и откуда начать читать, читать с них и делать свое дело».
Этот подход учитывает некоторые основные принципы SOLID и OOP . Браво.
Адам Нейлор