Як я повинен структурувати систему завантаження розширюваних активів?


19

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

Зокрема, я шукаю, як я можу спроектувати менеджера таким чином, щоб він резюмував, як завантажуються конкретні типи активів і звідки завантажуються активи. Я хотів би мати можливість підтримувати і файлову систему, і сховище RDBMS, без решти програми про це не потрібно знати. Так само я хотів би додати актив опису анімації (FPS, кадри для візуалізації, посилання на образ спрайту тощо), що є XML. Я повинен мати можливість написати клас для цього з функціоналом для пошуку та читання XML-файлів та створення та повернення AnimationAssetкласу з цією інформацією. Я шукаю дизайн, керований даними .

Я можу знайти багато інформації про те, що повинен робити менеджер активів, але не про те, як це зробити. Залучені дженерики, здається, призводять до якоїсь форми каскадування класів чи певної форми допоміжних класів. Однак я не бачив чіткого прикладу, який би не був схожим на особисту хакку чи точку консенсусу.

Відповіді:


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
Хороший запитання та хороша відповідь, що спрямовує рішення не тільки на керовану даними конструкцію, але і на те, як почати мислити способом, керованим даними.
Патрік Х'юз

Дуже приємна і поглиблена відповідь. Мені подобається, як ти інтерпретував моє запитання і сказав мені саме те, що мені потрібно знати, поки я його так погано сформулював. Спасибі! Ви будь-яким випадком могли б вказати мені на деякі ресурси про Streams?
user8363

"Потік" - це лише послідовність (потенційно без визначеного кінця) байтів або даних. Я конкретно думав про C # Stream , але вас, мабуть, більше цікавлять потокові класи Java - хоча застережте, я не знаю занадто багато Java, щоб не бути ідеальним класом для використання.

Потоки, як правило, є стаціонарними, оскільки даний об'єкт потоку зазвичай має поточну позицію читання або запису в потоці, і будь-який IO, який ви виконуєте на ньому, відбувається з цієї позиції - саме тому я використовував їх як вхідні дані до інтерфейсів активів, тому що вони по суті говорять "ось кілька необроблених даних і з чого почати читати, читати з них і робити свою справу".

Цей підхід шанує деякі основні принципи як SOLID, так і OOP . Браво.
Адам Нейлор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.