Форум Flasher.ru
Ближайшие курсы в Школе RealTime
Список интенсивных курсов: [см.]  
  
Специальные предложения: [см.]  
  
 
Регистрация Блоги Правила Справка Пользователи Календарь Поиск рулит! Сообщения за день Все разделы прочитаны
 

Вернуться   Форум Flasher.ru > Блоги > Dima_DPE

Рейтинг: 4.67. Голосов: 3.

Макросы Haxe. Автоматическое встраивание ресурсов (assets embedding).

Запись от Dima_DPE размещена 12.05.2013 в 23:41

Цитата:
Исходники первой статьи с рабочей версией для Haxe 3 и решенным дополнительным заданием тут. В файле Main3.hx можно найти еще пару вариантов getBuildDate с упрощенным синтаксисом из Haxe 3.
Как и ожидалось, первая статья вызвала хоть и не большой, но интерес. И дабы не остужать его, было решено сделать что-то более интересное и полезное. Полезным это будет для flash разработки, но идеи и техники, описанные в статье, можно будет использовать в различного рода макросах. Еще этот макрос я решил писать под Haxe 3. А все потому, что вышел Haxe 3 RC 2 и самое время его скачать и начать использовать, особенно легко это сделать пользователям FlashDevelop, т.к. им достаточно указать папку с Haxe 3 в табе SDK настроек проекта, а для линукса надо всего лишь собрать Haxe из исходников самому. Да и под Haxe 2 макрос отказывался работать со странной ошибкой, которую, видимо, поправили в 3-й версии.

Из названия статьи понятно, что мы будем что-то встраивать, а именно звуки и графику, как самые распространенные ассеты. Для начала посмотрим, как в Haxe реализуется встраивание ресурсов без макросов для flash платформы:

Код AS3:
@:sound("file.wav|mp3") class MySound extends flash.media.Sound {}
@:bitmap("myfile.png|jpg|gif") class MyBitmapData extends flash.display.BitmapData {}
Выглядит очень неплохо. Но как только таких встраиваний надо сделать более 20, начинаются сложности: рутинная работа, сложно уследить за всеми ассетами, менять и редактировать существующие. И тут на помощь приходят макросы.

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

Для того, чтобы создать статические переменные в уже определенном ранее классе, нам нужно узнать еще один мета тег:

Код AS3:
@:build(MyMacro.build()) class A {}
Данная конструкция говорит компилятору, чтобы он (компилятор) при создании класса A, вызвал статическую макро функцию build из класса MyMacro. При этом макрос MyMacro.build возвращает не Expr, как обычные макросы, а массив haxe.macro.Field структур, которые дополнят определение класса новыми полями и методами и/или отредактируют существующие:

Код AS3:
macro public static function build():Array<Field>
Описание Field структуры можно найти в модуле haxe.macro.Expr (самое время туда заглянуть, если вы этого еще не сделали). Скажу лишь, что Field может описать любую переменную или функцию внутри класса (в нашем случае это класс A). Еще, обратите внимание, что мета тег @:macro превратился в аксессор macro. Видно, что в Haxe 3 макросы стали неотъемлемой частью языка и заслужили отдельный аксессор.

Но вернемся к нашей идее получить список всех файлов из указанной папки и попробуем написать базу для нашего макроса:

Код AS3:
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 [];
	}
}
И класс, который мы и будем “редактировать” (в нем то и появятся наши статические переменные):

Код AS3:
package assets;
@:build(deep.macro.AssetsMacros.embed("../assets")) class Assets { }
Давайте разбираться подробнее. Если класс Assets и мета перед ним, надеюсь, понятны, то в макро функции есть кое-что новое. Для начала обратите внимание, что метод embed принимает в качестве параметра константу строкового типа - path (изначально макросы могли принимать в роли параметров только Expr структуры, но потом и тут сделали упрощение и позволили использовать константы базовых типов, массивы и анонимные структуры, содержащие константы базовых типов напрямую). А теперь посмотрим на результат выполнения данной макро функции, а чтобы ее выполнить нужно обязательно упомянуть класс Assets где-то в коде, импорта класса будет достаточно:

Код AS3:
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
Первая строчка - это путь, указанный при вызове метода в мете @:build, а вторая строка - путь, отформатированный методом Context.resolvePath. Второй путь более правильный, и понятный Haxe компилятору. А вот дальше перечисляется содержимое папки assets (видно, что там находятся два изображения, два аудио файла и одна вложенная папка folder). Содержимое папки нам удалось получить благодаря методу FileSystem.readDirectory из пакета sys. Раньше реализации класса FileSystem для каждой из доступных платформ находились в отдельных пакетах, но потом их объединили в общий пакет sys, доступный и для макросов. Итого: мы передали путь к папке, где искать ассеты, и из макроса считали содержимое папки. Самое время отобрать из всего обилия нужные нам файлы и встроить их в проект (тут я позволю себе пропустить ту часть, где я нахожу имя и расширение файла и по нему определяю тип ассета).

Для определения типов, неважно, будь то класс, enum или typedef структура, существует специальная структура TypeDefinition (доступная в модуле haxe.macro.Expr):

Код AS3:
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>;
}
Взгляните, как можно заполнить эту структуру для решения нашей задачи:

Код AS3:
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),
};
Видно, что мы создадим класс в пакете assets с одной метой и без полей. Название класса собирается из двух частей: префикса, зависящего от типа ассета, и названия файла без расширения. Вид меты и базовый класс также зависит от типа ассета.

Обратите внимание на поле meta. Дело в том, что его содержимое не просто строка с путем к файлу, а само содержимое файла в виде строки с префиксом “data:”. В документации по Haxe я не нашел информации о префиксе data, но Николас в своих макросах использовал его таким образом, поэтому я сделал по аналогии.

Тип ассета и методы, возвращающие префикс, базовый класс и название меты выглядят следующим образом:

Код AS3:
#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
Уф, как много получилось, а ведь я так хотел сделать все попроще. Сразу уточню, что #if macro тут действительно нужен, хотя и не обязателен, однако он позволяет отделить часть кода, которая доступна только из макро функций и невидима для остальных. Надеюсь, что работа методов getMetaName и getPrefix понятна, и на них мы останавливаться не будем. А вот метод getKind возвращает один из конструкторов enum-а TypeDefKind (тут вы уже точно ищите его в haxe.macro.Expr и внимательно изучите весь модуль, пожалуйста). Если присмотреться, то видно, что для картинки базовый класс окажется flash.display.BitmapData, а для звуков flash.media.Sound. Enum AssetType сделал я сам и в будущем его можно будет дополнить, например, AFont и ABytes, как вариант.

Подведем очередной итог: мы получили список всех файлов в папке, определили тип файлов - AssetType и в зависимости от типа создали класс в пакете assets, так что картинки начинаются с префикса “Bitmap_”, а звуки - с “Sound_” и связали с этими классами внешние ассеты с помощью метатегов.

Остается только сказать компилятору, чтобы он использовал эти классы наравне с другими:

Код AS3:
Context.defineType(clazz);
и тогда можно создавать наши автоматически сгенерированные ассеты и работать с ними:

Код AS3:
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 иногда нужно создавать самому? И это как раз тот самый случай):

Код AS3:
var filePos = Context.makePosition( { min:0, max:0, file:file } );
Ну и напоследок создадим и инициализируем наши статические аксессоры:

Код AS3:
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,
});
Для этого дополним массив res статическими публичными переменными [APublic, AStatic] с именами, совпадающим с именами классов, но с маленькой буквы, и сразу инициализируем переменные, вызвав конструктор ENew классов. Массив res изначально хранит все поля класса, которые были ранее в нем определенны, если этого не сделать, то все, что бы мы не определили в классе Assets в итоге исчезнет, все методы и свойства, кроме новых, созданных макросом.

Как я и предупреждал, все мало-мальски сложные макросы строятся на Expr, и macro reification тут встречается не часто. Осталось только показать, как работает автодополнение в случае такого вмешательства в класс Assets:



Как видно, все сгенерированные налету статические поля видны в автодополнении, что только упрощает работу и дает надежду на светлое будущее .

На этом я, пожалуй, остановлюсь. Можно еще сделать рекурсивную обработку подпапок, можно не инициировать все ассеты сразу, а сделать геттеры, которые будут создавать ассеты только по необходимости, можно оптимизировать макрос для автодополнения (исключив часть инструкций и, тем самым, ускорив процесс автодополнения), можно дополнить тип ассетов шрифтами и бинарными файлами, да и текстовыми тоже можно! Займитесь этим на досуге. Считайте это заданием к этому уроку. Тем более, что на этот раз я приложу все исходники, которые позволят вам увидеть весь проект целиком и даже те несколько строк кода, которые я скрыл из статьи. Вот вам еще идея: налету оптимизировать графику и звуки, правда такие вещи лучше кешировать в файлы и делать оптимизацию только по необходимости, иначе каждое автодополнение будет опять таки пережимать файлы, можно например создавать image_80.jpg для 80% сжатой картинки и т.д.

Ну и конечно же ссылка на макрос Николаса, которым я вдохновлялся.

Проект целиком можно найти на гитхабе.

Цитата:
Отдельная благодарность Александру Хохлову, Александру Кузьменко и SlavaRa за помощью в написании статьи, подсказки, замечания и поиск допущенных мной ошибок.
Всего комментариев 4

Комментарии

Старый 13.05.2013 11:45 RealyUniqueName вне форума
RealyUniqueName
Шикарно! Теперь построение классов стало для меня понятнее. Раньше избегал этого функционала, но сейчас уже вижу, как буду улучшать свои наработки
Старый 13.05.2013 14:31 Inet_PC вне форума
Inet_PC
 
Аватар для Inet_PC
Спасибо за статью. Приятно, что автодополнение работает)
Старый 13.05.2013 14:34 Dima_DPE вне форума
Dima_DPE
Не за что. Да, автодополнение работает, правда ради этого Haxe фактически пересобирает проект, но с появлением инкрементной компиляции и демона сборки это стало довольно быстро работать. Да и сами макросы можно и нужно упрощать в случае автодополнения.
Старый 14.05.2013 22:03 firsoff вне форума
firsoff
Душевное спасибо!
 

 


Часовой пояс GMT +4, время: 21:09.


Copyright © 1999-2008 Flasher.ru. All rights reserved.
Работает на vBulletin®. Copyright ©2000 - 2020, Jelsoft Enterprises Ltd. Перевод: zCarot
Администрация сайта не несёт ответственности за любую предоставленную посетителями информацию. Подробнее см. Правила.