Макросы Haxe. Автоматическое встраивание ресурсов (assets embedding).
Запись от Dima_DPE размещена 12.05.2013 в 23:41
Цитата:
Исходники первой статьи с рабочей версией для Haxe 3 и решенным дополнительным заданием тут. В файле Main3.hx можно найти еще пару вариантов getBuildDate с упрощенным синтаксисом из Haxe 3.
Из названия статьи понятно, что мы будем что-то встраивать, а именно звуки и графику, как самые распространенные ассеты. Для начала посмотрим, как в Haxe реализуется встраивание ресурсов без макросов для flash платформы:
@:sound("file.wav|mp3") class MySound extends flash.media.Sound {} @:bitmap("myfile.png|jpg|gif") class MyBitmapData extends flash.display.BitmapData {}
Идея очень простая: напишем макрос, который автоматически встроит все ассеты из папки, а также создаст статические переменные для доступа к экземплярам встроенных BitmapData и Sound (опять-таки не самое изящное решение, но для примера и для упрощения задачи оно вполне сгодится).
Для того, чтобы создать статические переменные в уже определенном ранее классе, нам нужно узнать еще один мета тег:
Данная конструкция говорит компилятору, чтобы он (компилятор) при создании класса A, вызвал статическую макро функцию build из класса MyMacro. При этом макрос MyMacro.build возвращает не Expr, как обычные макросы, а массив haxe.macro.Field структур, которые дополнят определение класса новыми полями и методами и/или отредактируют существующие:
Описание Field структуры можно найти в модуле haxe.macro.Expr (самое время туда заглянуть, если вы этого еще не сделали). Скажу лишь, что Field может описать любую переменную или функцию внутри класса (в нашем случае это класс A). Еще, обратите внимание, что мета тег @:macro превратился в аксессор macro. Видно, что в Haxe 3 макросы стали неотъемлемой частью языка и заслужили отдельный аксессор.
Но вернемся к нашей идее получить список всех файлов из указанной папки и попробуем написать базу для нашего макроса:
package deep.macro; import haxe.macro.Context; import haxe.macro.Expr; import sys.FileSystem; class AssetsMacros { macro static public function embed(path:String):Array<Field> { trace(path); path = Context.resolvePath(path); trace(path); for (f in FileSystem.readDirectory(path)) { trace(f); } return []; } }
Давайте разбираться подробнее. Если класс Assets и мета перед ним, надеюсь, понятны, то в макро функции есть кое-что новое. Для начала обратите внимание, что метод embed принимает в качестве параметра константу строкового типа - path (изначально макросы могли принимать в роли параметров только Expr структуры, но потом и тут сделали упрощение и позволили использовать константы базовых типов, массивы и анонимные структуры, содержащие константы базовых типов напрямую). А теперь посмотрим на результат выполнения данной макро функции, а чтобы ее выполнить нужно обязательно упомянуть класс Assets где-то в коде, импорта класса будет достаточно:
src/deep/macro/AssetsMacros.hx:11: ../assets src/deep/macro/AssetsMacros.hx:13: src/../assets src/deep/macro/AssetsMacros.hx:15: haxe.png src/deep/macro/AssetsMacros.hx:15: folder src/deep/macro/AssetsMacros.hx:15: flash2.png src/deep/macro/AssetsMacros.hx:15: 2.mp3 src/deep/macro/AssetsMacros.hx:15: 1.mp3
Для определения типов, неважно, будь то класс, enum или typedef структура, существует специальная структура TypeDefinition (доступная в модуле haxe.macro.Expr):
typedef TypeDefinition = { var pack : Array<String>; var name : String; var pos : Position; var meta : Metadata; var params : Array<TypeParamDecl>; var isExtern : Bool; var kind : TypeDefKind; var fields : Array<Field>; }
var clazz:TypeDefinition = { pos : filePos, fields : [], params : [], pack : ["assets"], name : getPrefix(type) + name, meta : [ { name : getMetaName(type), params : [ { expr :EConst(CString("data:" + data)), pos :filePos } ], pos : filePos } ], isExtern : false, kind : getKind(type), };
Обратите внимание на поле meta. Дело в том, что его содержимое не просто строка с путем к файлу, а само содержимое файла в виде строки с префиксом “data:”. В документации по Haxe я не нашел информации о префиксе data, но Николас в своих макросах использовал его таким образом, поэтому я сделал по аналогии.
Тип ассета и методы, возвращающие префикс, базовый класс и название меты выглядят следующим образом:
#if macro enum AssetType { AImage; ASound; } #end ... #if macro // Название мета тега static function getMetaName(type:AssetType) { return switch (type) { case AImage: ":bitmap"; case ASound: ":sound"; } } // Базовый тип static function getKind(type:AssetType) { return switch (type) { case AImage: TDClass( { pack : ["flash", "display"], name : "BitmapData", params :[] } ); case ASound: TDClass( { pack : ["flash", "media"], name : "Sound", params :[] } ); } } // префикс класса static function getPrefix(type:AssetType) { return switch (type) { case AImage: "Bitmap_"; case ASound: "Sound_"; } } #end
Подведем очередной итог: мы получили список всех файлов в папке, определили тип файлов - AssetType и в зависимости от типа создали класс в пакете assets, так что картинки начинаются с префикса “Bitmap_”, а звуки - с “Sound_” и связали с этими классами внешние ассеты с помощью метатегов.
Остается только сказать компилятору, чтобы он использовал эти классы наравне с другими:
и тогда можно создавать наши автоматически сгенерированные ассеты и работать с ними:
var s = new assets.Sound_1(); s.play(); var i = new assets.Bitmap_flash(0, 0); Lib.current.addChild(new Bitmap(i));
Context.defineType - очередной метод класса Context, который продолжает нас радовать. Вдумайтесь: мы только что, создали новый класс и компилятор его знает, но текстового файла с этим классом нет и не будет. А для того, чтобы компилятор знал откуда этот класс взялся, мы создадим для него свой Position, на месте файла ассета (помните в прошлой статье я говорил, что Position иногда нужно создавать самому? И это как раз тот самый случай):
Ну и напоследок создадим и инициализируем наши статические аксессоры:
var res = Context.getBuildFields(); ... res.push( { name : getPrefix(type).toLowerCase() + name, access : [APublic, AStatic], doc : null, kind : FVar(null, { expr : ENew( { pack : ["assets"], name : getPrefix(type) + name, params : [] }, getArgs(type)), pos : pos } ), meta : [], pos : pos, });
Как я и предупреждал, все мало-мальски сложные макросы строятся на Expr, и macro reification тут встречается не часто. Осталось только показать, как работает автодополнение в случае такого вмешательства в класс Assets:
Как видно, все сгенерированные налету статические поля видны в автодополнении, что только упрощает работу и дает надежду на светлое будущее .
На этом я, пожалуй, остановлюсь. Можно еще сделать рекурсивную обработку подпапок, можно не инициировать все ассеты сразу, а сделать геттеры, которые будут создавать ассеты только по необходимости, можно оптимизировать макрос для автодополнения (исключив часть инструкций и, тем самым, ускорив процесс автодополнения), можно дополнить тип ассетов шрифтами и бинарными файлами, да и текстовыми тоже можно! Займитесь этим на досуге. Считайте это заданием к этому уроку. Тем более, что на этот раз я приложу все исходники, которые позволят вам увидеть весь проект целиком и даже те несколько строк кода, которые я скрыл из статьи. Вот вам еще идея: налету оптимизировать графику и звуки, правда такие вещи лучше кешировать в файлы и делать оптимизацию только по необходимости, иначе каждое автодополнение будет опять таки пережимать файлы, можно например создавать image_80.jpg для 80% сжатой картинки и т.д.
Ну и конечно же ссылка на макрос Николаса, которым я вдохновлялся.
Проект целиком можно найти на гитхабе.
Цитата:
Отдельная благодарность Александру Хохлову, Александру Кузьменко и SlavaRa за помощью в написании статьи, подсказки, замечания и поиск допущенных мной ошибок.
Всего комментариев 4
Комментарии
13.05.2013 11:45 | |
Шикарно! Теперь построение классов стало для меня понятнее. Раньше избегал этого функционала, но сейчас уже вижу, как буду улучшать свои наработки
|
13.05.2013 14:31 | |
Спасибо за статью. Приятно, что автодополнение работает)
|
14.05.2013 22:03 | |
Душевное спасибо!
|
Последние записи от Dima_DPE