Способы хранение графики в играх и бизнес приложениях |
|||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Содержание
ВведениеВ предыдущей статье я рассказал, как можно считывать растры напрямую из файла ( надеюсь информация оказалась для Вас интересная). Теперь поговорим о том, как создать собственный, удобный для нас, формат хранения графической информации. Рассматриваемый подход пригодится не только для хранения графики, но и для совершенно различных бинарных данных. Это могут быть и музыкальные треки в популярном формате MP3, видео фрагменты, текстовые данные - в общем, любые данные Вашего приложения. В конце предыдущей статьи, я изложил краткий алгоритм для создания таких файлов, теперь я попытаюсь последовательно описать все его тонкости. Возможная структура файлаДля начала набросаем приблизительную структуру нашего будущего формата данных. Для примера, я создам файл для хранения обычных растров в не компрессированном виде.
Так выгладит заголовок файла. Количество элементов ResourceTable соответствует количеству хранимых изображений. Сразу за последней записью в ResourceTable начинаются данные изображений. Для создания подобного форматы нам потребуется написать небольшой "компилятор" или скорее "сшиватель" ресурсного файла. В его задачи будет входить создание файла с описанным выше форматом из обычных BMP файлов. Сразу стоит оговорится, что создание компилятора или упаковщика (это кому как нравится) самый трудоемкий процесс. По этому я решил особо не выпендриваться и написать его с использованием VCL, т.к. во первых это сугубо рабочая утилита, кроме нас её никто видеть не будет, а во вторых тут совсем не принципиальна скорость работы - один раз собрали ресурс и забыли про него. Хотя для больших работ, когда суммарный объем обрабатываемых файлов переваливает за сотни МБ, стоит подумать и о оптимизации. КомпиляторНачнем создание компилятора с заготовки необходимых структур:
Надеюсь тут все понятно. Есть только две небольшие тонкости. Первая нельзя использовать просто String - только фиксированную длину строки! Иначе SizeOf(TGameRusourceHeader) выдаст совершенно не верный результат. Вторая тонкость по организации проекта. Т.к. нам нужно написать и компилятор ресурсов и загрузчик, лучше вынести описание заголовков в отдельный модуль. Как уже писалось выше, я буду использовать VCL компоненты и стандартный набор классов. Это сильно сократит исходный код, да и сделает его понятным. Алгоритм процедуры "сшивания" ресурсов следующий. Процедуре будем предавать список файлов, и название выходного файла. В переменные заведем три потока и другие необходимые переменные:
Для начала работы процедуры проинициализируем все объекты и переменные:
Самой сложной частью процедуры я считаю рассчет смещений, все остальное достаточно прозрачно:
... на последок копирование данных в выходной поток и очистка занятых ресурсов.
Вот собственно и всё. После успешного завершения процедуры у Вас получится файл с описываемой структурой. При работе процедуры, создается файл с расширением OutFileName.text, куда записывается вся информация о размерах структур, смещениях и т.д. Смещения записываются как в обычном десятичном виде, так и в шестнадцатеричной форме. Последняя форма записи очень помогает при анализе полученного файла в любом HEX редакторе (WinHex, Hview и т.д.). Не возможно не упомянуть об одной особенности - уменьшении размера полученного файла. Поясню более подробно. Для примера я скомпилировал набор из 313 BMP файлов различного размера. Суммарный объем файлов 2, 359 Кб, после сборки получился файл размером 2,428 Кб - оно и понятно, мы записываем лишнею информацию. После сжатия архиватором ZIP отдельных BMP файлов получился архив размером 697 Кб, а вот при сжатии выходного файла - 640 Кб. Выигрыш очевиден, причем он растет с увеличением числа хранимых битмапов и уменьшения их размера. При сборке ~500 картинок размером 16x16 выигрыш получается более чем в два раза. Необходимо помнить, что для приложений распространяемых по сети размер дистрибутива до сих пор критичен. И если Ваша игра или утилитка "весит" в 5-6 раз меньше, чем аналоги, шанс что пользователь выберет именно её повышается не однократно. "Загрузчик"Надеюсь с созданием формата данных для хранения информации Вы разобрались, теперь осталась самая легкая часть - написать загрузчик графики из нашего формата. Как и с компилятором ресурсов, я напишу программу используя VCL (ну не знает наш народ API, а при виде одного dpr файла впадает в ступор - "А где же форма ? Где мой любимы TButton.OnClick ???"=) ). Всё действо будет происходить в одной процедуре. Параметр ResourceFileName - путь и имя к файлу, а ImageCount - номер изображения для загрузки (нумерация начинается с 1). В процедуре нам понадобится всего четыре переменные:
Сама процедура чрезвычайно проста, обратите внимание только на получение смещение для загрузки файла:
Процедура работает практически мгновенно, я имею в виду перемещение по файлу, а за скорость загрузки самого изображения ответственность ложится на метод LoadFromStream. Возможно, я приложу к статье пример, показывающий, как можно избежать использования TBitmap и загружать ресурс самостоятельно. Хотя это совсем не сложно сделать объединив материал предыдущей статьи и приведенный выше код. Остановимся на возможности оптимизации. Итак:
Защищенность ресурса от просмотра ниже средней - простой человек не посмотрит, а для программиста средней руки разобрать такой формат раз плюнуть. Но захочет ли он с этим возится? Хранение ресурса в секции PE файлаТеперь настала пора разобраться, как поместить созданный ресурсный файл в исполняемое приложение, т.е. просто "вшить" его в exe файл. Проблемы с соединением не возникнет, а вот как с обращением к ресурсу стоит попотеть. Как известно в PE файле есть различные секции, при этом ничего не мешает Вам писать в секцию импорта свои данные, но есть специальная секция RCData. Она то и предназначенная для записи собственных данных приложения, т.е. в неё можно пихать всё, что угодно (в смысле бинарных данных :)), в разумных пределах конечно. Для примера я создам файл out (с помощью описанного ранее компилятора) содержащий четыре 24-х битных растра. Количество не имеет значение, а 24-битные растры я буду помещать по тому, что их проще загружать. Итак создаем RC файл, например Resource.rc со следующим содержанием:
GAMEDATA - название ресурса, т.е. его идентификатор; Создаем ресурсный файл вызывая компилятор ресурсов:
...ааа вот зачем программистам в Windows нужна командная строка! После успешного завершения, мы получим бинарный ресурсный файл Resource.RES, его можно подключать к проекту директивой компилятора:
Теперь при сборке проекта в получившемся exe файле появится секция RCDATA и в ней ресурс с названием GAMEDATA. Осталось совсем чуть-чуть - написать процедуру загрузки. Она будет достаточна сложна, и если Вы не сильны в таких понятиях как указатели, дескрипторы, плоская модель памяти … смело пропускайте данный материал. Бездумное копирование кода до добра не доводит :) Начнем как всегда с расшифровки переменных:
Ну как, не испугались ? Дальше интереснее будет:
Находим ресурс функцией FindResource по его идентификатору GAMEDATA в секции RT_RCDATA. Загружаем ресурс функцией LoadResource, блокируем доступ к нему и получаем область занимаемой им памяти в указатель P.
В StartAddr получаем адрес памяти указателя P и устанавливаем счетчик I на это же значение. Далее все опрерации будем производить только со счётчиком, так нагляднее. Загружаем заголовок GameRusourceHeader, он находится прамо по адресу I или StartAddr, т.к. в самом начале блока памяти загруженного ресурса. Увеличиваем счетчик на размер структуры TGameRusourceHeader. Параметр функции ImageCount хранит номер растра, который необходимо получить. По этому высчитываем смещение для требуемой таблицы ресурсов: SizeOf(TGameResourceTable)*(ImageCount-1). Получаем таблицу смещений. Из неё можно вытащить смещение требуемого растра: StartAddr+GameResourceTable.Offset. По этому смещению можно последовательно считать BitmapFileHeader, BitmapInfoHeader и BitmapBits. Вот собственно и все! Осталось создать Bitmap и очистить ресурсы:
Не так все и сложно, хотя я представляю лица ( масли, выражения, жесты ... ) тех, кто сел за Delphi месяц назад :))) На самом деле, достаточно сложный для понимания материал. Хотя если Вы изучали C или ASM для Вас должно быть всё тривиально. Пример приведённый выше далеко не оптимален:
Еще хочется остановится на такой проблеме как хранение музыки и другой мультимедиа информации в exe файле. После прочтения выше изложенного материала проблем с этим возникнуть не должно. Я попробую изложить несколько общих принципов, а конкретная реализация зависит от того какие ресурсы Вы хотите поместить в файл и какие методы воспроизведения будите использовать. Общий алгоритм такой - помещаем каждый из ресурсов в секцию RCDATA, присваивая каждому уникальное имя (идентификатор). После этого получаем указатель на начало блока памяти занимаемого ресурсом и выполняем воспроизведение с помощью соответствующих функций. Приведу небольшой пример. Допустим, необходимо подключить к exe файлу композицию в формате MP3. Для маленьких игр это может быть музыкальный фон, звуки спец. эффектов и т.д. Файл будет называться sample.mp3 Создадим ресурсный файл MusicRec.RC и в него добавим строчку:
Соберём бинарный файл ресурса командой:
и подключим к нашему приложению скомпилированный ресурсный файл:
После этих операций в нашем exe файле будет присутствовать MP3 фрагмент sample c идентификатором MUSIC1. Нам осталось только проиграть данный файл. Что для этого потребуется ? Конечно проигрыватель. Все конечно же подумали про WinAMP, но это же проигрыватель внешних файлов, к тому же не интересно привязывать нашу программу к WinAMP'у. Среди свободно распространяемых проигрывателей я выбрал библиотеку BASS. Во первых она позволяет проигрывать не только MP3 файлы, но и файлы трекерных форматов XM, MOD и т.д., что очень актуально для игр и демонстраций. Ведь эти файлы занимают очень мало места, а качество музыки на очень приличном уровне. Еще один большой плюс библиотеки BASS - её бесплатность для не коммерческого использования. К тому же она распространяется в виде динамической библиотеки с открытым интерфейсом и очень проста в использовании - я буквально за 10 минут написал этот пример, при этом ранее библиотеку никогда не видел. Собственно работа со звуком заключается в 4-х операциях: Инициализация биьлиотеки. Нам интересен кусочек загрузки музыкального фрагмента из памяти, по этому я рассмотрю только его.
В качестве основных параметров, процедуре BASS_SampleLoad передается указатель P и размер музыкального фрагмента в байтах (размер описывается к константах). Значение остальных параметров описаны в файле bass.pas или в файле справки. Для лучшего осмысления работы библиотеки BASS посмотрите пример BassTest входящий в комплект поставки. Вот собственно и всё. В следующий части статьи я поробую рассказать о хранении компрессованных ресурсов. | |||||||||||||||||||||||||||||||||||||||||||