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

В прошлый раз я не смог показать встраивание шрифтов из ttf файлов. Скажу честно, меня это сильно расстроило, и я провел небольшое расследование и нашел рабочий метод. Поэтому и появилась третья статья про встраивание ресурсов.

Но первое, что мы сделаем, будет автосборка. Заменим наше прошлое определение класс Assets:

@:build(deep.macro.AssetsMacros.embed("../assets")) class Assets {}

на более короткую и менее понятную:

class Assets implements IAssets<"../assets"> { }

Класс Assets остался, но только теперь он реализует некий интерфейс IAssets, да еще и параметр какой-то непонятный, переданный в роли дженерика, равный нашей папке из прошлого примера. А все дело в очередной магической мета дате - @:autoBuild объявленной в интерфейсе IAssets:

package deep.macro;

@:autoBuild(deep.macro.AssetsMacros.embed()) interface IAssets<Const> { }

Не знаю, что вы на этот раз воскликните. Но да, это почти тотже @:build, но без параметров в которых был бы указан путь к папке с ассетами. Значит, что у нас вышло. У нас есть интерфейс с указанным макросом в автобилде и класс, который реализует этот интерфейс. Все это заставит компилятор выполнить наш макрос по разу для каждого класса, реализующего наш интерфейс. В роли интерфейса может выступить и класс. В документации сказано, что autoBuild добавит всем наследующим классам @:build мета тег с аналогичным содержимым.

Польза от @:autoBuild понятная - более короткая запись наследующих классов и автоматическое добавление мета тега. Минусов не знаю, разве что можно в ветке иерархий забыть и/или не заметить, какие билд методы вызываются.

Но мы забыли один важный момент. А именно путь к папке ассетов. Если раньше мы передавали его в виде строки, то теперь он спрятан в параметре родительского интерфейса. И нам придется найти способ прочитать это значение из макроса. И вот тут начнется самое интересное.

Метод Context.getLocalClass() вернет нам Type (haxe.macro.Type) текущего класса, в нашем случае Assets, а внимательно изучив Type, мы сможем получить и искомую строку. Сказать по правде, Context.getLocalClass() вернет Ref, где Ref это обертка, с одним методом get():T (еще есть toString, но о нем не сейчас). Обертка эта нужна, чтобы не давать доступ сразу к экземпляру класса T, а лишь по требованию - get(), тем самым уменьшает нагрузку и создает экземпляры ClassType только по необходимости. Весь метод получения пути из параметра интерфейса выглядит вот так:

static function getPath(type:ClassType):String {
               
        for (i in type.interfaces) {
                if (i.t.toString() == "deep.macro.IAssets") {
                        switch (i.params[0]) {
                                case TInst(t, _) :
                                        var ct:ClassType = t.get();
                                        switch (ct.kind) {
                                                case KExpr( { expr:EConst(CString(s)) } ): return s;
                                                case _: throw "assert";
                                        }
                                       
                                case _: throw "assert";
                        }
                }
        }
        throw "assert";
}

Внимательно изучим данный метод. Уж больно он “крутой” получился. ClassType - это огромная структура, лучше взглянуть на нее самим. Но нас интересует лишь список интерфейсов - type.interfaces. Переберем все интерфейсы и найдем тот, который deep.macro.IAssets. Далее взглянем на параметры интерфейса, а точнее на его единственный параметр - i.params[0], мы точно знаем что он строка, точнее мы хотим, чтобы он был строкой. Не сложно понять, что он будет выглядеть примерно так:

TInst(Ref<KExpr({expr:EConst(CString(S)), pos:position})>)

Кому сложно, сделает просто trace и сам все увидит. Если бы не Ref в TInst, можно было бы это записать в один case (спасибо крутому pattern matching из Haxe 3), но пришлось сделать в два switch-а. Сначала получив Ref, а потом уже распарсив весь ClassType в один case - case KExpr( { expr:EConst(CString(s)) } ). В последней s и будет храниться наша строка “../assets”. Остальные случаи для нас ошибочны, поэтому я вставил там “подробный” throw “assert”, как обычно и делает Николас в таких местах.

Согласен, было проще, но теперь вы знаете, чего можно добиться простым методом Context.getLocalClass(), при том, что он всегда возвращает именно тот тип, в котором вызван макрос. А вот и пример использования метода getPath:

var ref:ClassType = Context.getLocalClass().get();
var path = getPath(ref);

И я снова считаю, что получилось отлично. Да, немного громоздко, зато универсально. Если вы запустите проект сейчас, то все заработает как и раньше, главное, убрать у функций embed обязательный параметр path.

Ну, и напоследок вернемся к шрифтам. Здесь нужно сделать небольшое, но важное отступление.

Раньше мы встраивали ассеты с помощью хака, когда в тело меты вписывался весь контент ассета после префикса “data:”. Так вот, для шрифтов он не работает. Поэтому было решено отказаться от трюка, и во все мета даты записывать только путь к файлу, это позволит унифицировать код для шрифтов, битмап и звуков, да и сам отказ от хака только плюс.

В Haxe 3 появился удобный метод встраивания шрифтов для flash проектов. Точнее удобная мета дата:

@:font("path.ttf", range="")

Если с первым параметром все понятно, это обычный путь к файлу, то вот второй параметр интереснее, он вроде как не обязателен, но вряд ли кто-то решится встраивать весь шрифт целиком. А для остальных случаев нужно передать строку типа "a-z0-9", которая укажет, какие именно символы встраивать во флешку.

Для небольшого упрощения, предположим, что для всех шрифтов вам нужен один набор символов. И сделаем макро метод, для задания этого набора:

static var fontsRange:String = "a-zA-Z0-9.,;:'\"`@#$%^&*()[]{}";

macro static function setFontsRange(range:String) {
        fontsRange = range;
        return macro null;
}

Макрос просто изменит статическую переменную fontsRange, внутри AssetsMacros класса и вернет macro null. Т.е. вызов макроса в результате заменится на null; Можно туда же вернуть 0; Но вот как совсем ничего не возвращать макросом я не нашел, да и почти уверен, что нет такого.

Все, что нам осталось - это собрать мета параметры, т.к. большинство утилитарных методов было адаптированны под AFont еще в коде для предыдущей статьи. Возьмем то, что было в прошлый раз, точнее почти то же, но без “data:”, а просто путь к файлу. И добавим fontsRange если у нас шрифт и если fontsRange задан:

var metaParams = [ macro $v{file} ];
if (type == AFont && fontsRange != null)
        metaParams.push( macro $v{fontsRange} );

Теперь, для fontsRange = "a-zA-Z0-9.,;:'\"`@#$%^&*()[]{}" мы увидим такую картину:

Обратите внимание, что в PT Sans остался 161 символ, т.к. мы взяли его из другого swf. А вот Arial импортировали из arial.ttf 86 символов. Важно: нужно бы импортировать и символ пробела, иначе пробелов в тексте вам не видать. Ну а теперь выполнив:

var f = Assets.arial;
trace([f.fontName, f.fontStyle]);

мы увидим [Arial, regular]. Именно то, чего мы и ожидали.

Ура! Мы это сделали. Теперь встраиваются и шрифты, и картинки, и звуки, и текстовые файлы. Правда, во время тестов обнаружилась одна проблема - не удается указать интервалы символов отличных от латинских, но это, видимо, ограничение neko работы с utf-8 строками и если кто-то вкурсе как это обойти, то подскажите или сделайте pull request на гитхабе.

На этом я снова остановлюсь, и на этот раз точно. Я убедил себя, что рекурсивная обработка нам ни к чему, так же как встраивание бинарных файлов. Сегодня вы научились работать с новым мета тегом autoBuild, узнали кое-что новое о haxe.macro.Type и его содержимом. И главное, что теперь работает встраивание шрифтов из ttf файлов.

Исходный код всего урока лежит на гитхабе.

Отдельная благодарность Александру Хохлову и Александру Кузьменко за помощью в написании статьи.