Про ресурсы
После сопливого поста про сейвгеймы в Program Files просто обязан сделать что-то полезное (тем более, что занял им козырный нумер “100″). Например, написать рассказ про то, что должен делать resource manager.
Я уже неоднократно высказывал мысль о том, что как только вы в ресурс-менеджере пишите что-то вида enum { eTexture, eMesh, eSprite, … }; то ваш идеальный ресурс-менеджер идет лесом. Потому что он превращается в ресурс менеджер, к которому (достаточно искуственно) притянуты за уши куски других подсистем. Например, если мы корректируем формат меша, нам придется редактировать и подсистему управления геометрией, и ресурс-менеджер, что недопустимо.
Если мы выбросим enum из ресурс менеджера, то что останется в нем? Если кто-то думает, что исключительно fopen()/fread(), то он глубоко заблуждается.
Сначала вылезем на нужный уровень абстракции. В самом что ни на есть универсальном виде некий произвольный ресурс можно представить как следующий блок данных: {resource_type_id, resource_size, resource_data}. Здесь resource_type_id похож на наш пресловутый enum { eTexture, …}, но мы не забываем о том, что принципиально не хотим определять его. Соответственно, каждая подсистема должна определить себе этот resource_type_id (неважно, что это будет – строка, guid или что-то другое), задача ресурс менеджера заключается только в том, чтобы не допустить пересечения идентификаторов.
Второе размышление о менеджере. Ресурсы могут быть взаимосвязаны. Например, меш или спрайт ссылается на текстуры. В самом простейшем варианте можно возложить заботу о поиске и связывании на конкретные подсистемы. Например, все текстуры имеют имена и хранятся в текстурном словаре. Меш также хранит имена текстур и при своей загрузке ищет/грузит текстуры по именам. Но это “не наш метод” (С). Поэтому добавим в наш блок данных еще одно поле: {resource_type_id, resource_unique_id, resource_size, resource_data}. Поле resource_unique_id (возможно, в связке с resource_type_id) позволит ресурс менеджеру отыскивать нужные ресурсы в общем котле.
Далее. В случае “обычной загрузки” мы, как правило, прямо в процессе загрузки меша ищем и подгружаем нужные нам текстуры. С учетом абстракции data_source подобное может быть сложно реализуемо, поэтому есть два варианта решения:
1. Воспользовашись Бориными yield(), можно написать следующее:
[code lang="cpp"]MeshSystem::load(...)
{
...
ResourceSystem::RequestToLoadResource(resource_unique_id);
while (!ResourceSystem::IsResourceReady(resource_unique_id)) {
Yield();
}
...
}[/code]
2. Дадим эту информацию ресурс менеджеру еще до начала загрузки всего в память (на этапе подготовки ресурсов) и пусть он решает задачу: если ресурс {resource_unique_id1} требует себе {resource_unique_id2}, то к тому моменту, когда первый ресурс начинает загружаться в память, второй ресурс уже загружен и готов к использованию. Оба варианта приводят к организации ацикличного орграфа (меши могут ссылаться на одну и ту же текстуру, но кольцевые связи вгонят систему в ступор); второй вариант предпочтительнее тем, что ресурс-менеджер, заранее зная о взаимосвязях объектов, может проводить разные оптимизации и укладки данных.И еще одно размышление о менеджере. Изначально абстрагируемся от того, что ресурсы хранятся в виде файлов. Представим себе абстрактный источник данных, некоторое data_source, которое может быть как индивидуальными файлами, длинным файлом, zip-архивом или чем-то еще. Более того, data_source может иметь и не файловую структуру, например, поступать в игру по TCP/IP или через pipe.
Теперь со всем этим попробуем взлететь.
Первое. Заводим некоторое описание уровня, в котором находится список всех ресурсов (пространство, модельки, эффекты, навигационная карта AI и все прочее). По данной карте уровня мы можем построить список всех ресурсов, которые требуются для загрузки игрового уровня.
Второе. Список ресурсов, очевидно, может расширяться и укрупняться прямо в процессе экспорта (например, моделька потянула за собой текстуры).
Третье. В компонентах пишется специальный код (“Экспортер”), который подготовит данные в виде {resource_size, resource_data} или даже просто в виде записанного на диск файла.
Четвертое. В игре запускаем ресурс-менеджер на загрузку уровня. Менеджер поднимает данные из data_source и начинает загружать ресурсы в соответствии с глобальной картой. В варианте №1 загрузку можно вести с самого “тяжелого” ресурса, в варианте №2 – наоборот, сначала загружаются “легкие” ресурсы, а только потом те, кто на них завязан. По resource_type_id определдяется владелец ресурса и получает управление на загрузку своей информации.
“Экспортеры” и “Импортеры” конкретных ресурсов регистрируются в ресурс-менеджере через callback или наследуются от соответствующих интерфейсов.
Какие еще вкусности можно приклеить к системе:
1. Вместо простого {resource_size, resource_data} стоит разделить блок на нескольких кусков и типов, например: FIXED (грузиться в режиме memory mappped и остается в памяти до самого конца), DISCARD (грузится в память, после чего удаляется; такое может быть полезно, например, для PC текстур), SAVED (блок памяти грузится в память из сейвгейма и сохраняется в сейвгейм же). На первой загрузке уровня в качестве SAVED либо не используется ничего, либо используется “нулевой сейвгейм”.
2. Блоки в бинарном файле могут раскладываться по разным правилам (если ресурс менеджер знает про орграф). Например, провести топологическую сортировку или оптимизировать систему, разрезав длинный файл на несколько частей. В этом случае уже загруженные части поступают в обработку в “импортер”, а параллельно ресурс-менеджер догружает оставшиеся части (и если надо, раззиповывает их).
3. Разрезка может применяться и для организации стриминга. Засунем некоторые блоки в отдельный файл и будем поднимать его в память и удалять, а на его место грузить другой.
4. Ресурсы с одинаковым содержимым, к которым ведется доступ по readonly, могут быть прошарены вместе (автоматически!). При этом resource_unique_id остаются, но они ссылаются на один и тот же блок данных.
Тут еще можно много чего придумывать и рассказывать. Например, организацию компиляции ресурсов с добавлением новых ссылок (это примерно так же, как если бы из скомпилированного cpp получался бы не только obj, но и еще один cpp файл). Runtime подмену FIXED и SAVED данных даже незаметно для их владельцев. Способы организации unique id. Но я думаю, для затравки хватит. Главное, чтобы не произошло того, что ресурс менеджер узнал, с чем реально ему приходится работать. На правильном уровне абстракции и с соответствующими разумными ограничениями можно найти хорошее решение.
А еще оно все идеально ложится в борины елды, вот.