Немного об эмбеде ассетов

Хочу рассказать о своем подходе к эмбеду картинок и звуков.

Не секрет, что большинство девелоперов используют векторную графику и, соответственно, swc-файлы, экспортированные из Flash IDE. Там есть свои подходы, о которых я рассказывать не буду. Но, если вы используете растровую графику — информация, изложенная ниже, может помочь ускорить рутинные операции. Если, конечно, эти бояны еще кто-то не знает. Про swc тоже немного напишу.

Итак, сформулируем проблему.

Картинки.

У нас есть каталог (что-то вроде assets/gfx) с картинками.
Картинок может быть много, например, несколько сотен.
Эти картинки мы хотим использовать в игре в качестве спрайтов.

Интернеты подсказывают нам следующее решение:
[Embed(source='../assets/gfx/pic.png')]
private static var pic_class:Class;
private static var pic:Bitmap = new pic();
Ну хорошо, 5-10 картинок можно наколотить руками, но вот когда картинок много — это превращается в pain in the ass.

Еще один недостаток такого прямолинейного подхода — невозможность получить картинку по имени файла.
Например, у нас есть картинки с именами «wall_0.png», «wall_1.png», ..., «wall_18.png» и так далее. Типично хочется пробегать по таким картинкам в цикле и менять только циферку в имени, чего обычный подход не позволяет.

Лично я нашел решение в кодогенерации.

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

Раньше генерировал с помощью локального php, но скилл растет, поэтому переписал недавно на bat.

Пример батника:
@echo off

echo    package ryz.globals 
echo    {
echo    import flash.display.Bitmap;
echo    import flash.display.BitmapData;
echo    import flash.utils.Dictionary;
echo    public class GUIEmbeds 
echo    {

@FOR %%i IN (*.png *.jpg) DO ^
echo [Embed(source='../../../assets/gui/%%i')] && ^
echo private static var k_%%~ni:Class;

echo    private static var wasInit:Boolean = false;
echo    private static var bmps:Dictionary = new Dictionary();
echo    public static function Init():void
echo    {
echo    if (wasInit)
echo    {
echo    return;
echo    }

@FOR %%i IN (*.png *.jpg) DO echo bmps['%%~ni'] = new k_%%~ni().bitmapData;

echo    wasInit = true;
echo    }
                
echo    public static function Get(s:String):BitmapData
echo    {
echo    if (!wasInit)
echo    {
echo    Init();
echo    }
echo    return (bmps[s] as BitmapData);
echo    }
echo    public static function Bmp(s:String):Bitmap
echo    {
echo    return (new Bitmap(Get(s)));
echo    }
echo    }
echo    }
Запускать нужно в каталоге с картинками.
Для реального использования нужно поменять имя пэкеджа, класса и относительный путь.

Что, собственно, происходит:
@FOR %%i IN (*.png *.jpg) DO ^
echo [Embed(source='../../../assets/gui/%%i')] && ^
echo private static var k_%%~ni:Class;
Бежим по всем png и jpg, для каждого файла пишем имя файла с расширением ("%%i") и без расширения ("%%~ni"). Во втором случае дополнительно добавляем префикс «k_» (просто для красоты).

@FOR %%i IN (*.png *.jpg) DO echo bmps['%%~ni'] = new k_%%~ni().bitmapData;
Вот тут опять бежим по всем файлам и заносим в словарь ссылку на битмапдату.
Остальное просто вывод статичного текста, чтобы руками не копипастить.

В итоге генерируется вот такой файл:
package ryz.globals
{
        import flash.display.Bitmap;
        import flash.display.BitmapData;
        import flash.utils.Dictionary;
        
        public class GUIEmbeds
        {
                [Embed(source='../../../assets/gui/3stars.png')]
                private static var k_3stars:Class;
                [Embed(source='../../../assets/gui/blue_block.png')]
                private static var k_blue_block:Class;
//.............. тут много много эмбедов......... 
                [Embed(source='../../../assets/gui/unlock_over.png')]
                private static var k_unlock_over:Class;

                private static var wasInit:Boolean = false;
                private static var bmps:Dictionary = new Dictionary();
                
                public static function Init():void
                {
                        if (wasInit)
                        {
                                return;
                        }
                        bmps['3stars'] = new k_3stars().bitmapData;
                        bmps['blue_block'] = new k_blue_block().bitmapData;
//.............. тут много много записей в словарь.........
                        bmps['unlock_over'] = new k_unlock_over().bitmapData;
                        wasInit = true;
                }
                
                public static function Get(s:String):BitmapData
                {
                        if (!wasInit)
                        {
                                Init();
                        }
                        return (bmps[s] as BitmapData);
                }
                
                public static function Bmp(s:String):Bitmap
                {
                        return (new Bitmap(Get(s)));
                }
        }
}
Ну, здесь все понятно. Эмбедятся картинки, при первом обращении происходит инициализация. Можно просить битмапдату или новую битмапу.
Выглядит примерно так:
var tt:BitmapData = GUIEmbeds.Get('ig_top_lside');
Можно добавить какие-то константы, функции, и все такое прочее.
Но я не добавляю, мне не надо. Но добавить можно, не спорю.

Звуки.

Я не буду тут расписывать полностью работоспособный менеджер звуков, потому что это тема для отдельной большой статьи.
Поэтому про звуки покороче, потому что подход похожий на картинки.

Как обычно происходит работа с музыкантом, по крайне мере у меня:
1. Пишем список звуков.
2. Создаем каталог с пустышками.
3. Пишем динамическую загружалку из файлов.
4. Вставляем вызовы проигрывания звуков.
5. Отправляем музыканту.
6. Он заменяет пустышки на нормальные звуки и отправляет обратно.
7. Заменяем динамическую загружалку на эмбеды.

Все это тоже можно ускорить.

Итак, создали список звуков в текстовом файле:
01_plasma_shoot.mp3
02_plasma_blast.mp3
...
54_land_blast2.mp3
Я обычно нумерую звуки, потому что так проще.

Кидаем в каталог маленький, но рабочий(!) .mp3-файл. Это будет наша исходная «пустышка».

Теперь для каждого звука надо создать свой файл.
Используем следующий .bat:
FOR /F %%i IN (muz_list.txt) DO copy empty.mp3 %%i
Для каждой строчки в файле «muz_list.txt» копируем «empty.mp3».

Получили заполненный каталог.
Пустышку можно удалить.

Теперь для каждого звука надо нагенерить динамическую загрузку. Подразумевается, что файлы валяются прямо рядом с swf, поэтому никакие пути не нужны.
Генерируем:
FOR %%i IN (*.mp3) DO echo songs['%%~ni']= new Sound(new URLRequest('%%i'));
Вот тут для каждого файла создается запись в словаре.

Выглядит так:
songs['01_plasma_shoot']= new Sound(new URLRequest('01_plasma_shoot.mp3'));
songs['02_plasma_blast']= new Sound(new URLRequest('02_plasma_blast.mp3'));

Дальше можно использовать такой же подход как с картинками и проигрывать звуки через строковые константы, но это опасно, потому что, в отличии от картинок, звуки добавляются обычно в конце игры в самые неожиданные места.
Ошибиться в названии звука легко, а определить пропущенный звук третьего шага двадцать восьмого врага на тринадцатом уровне на максимальной сложности довольно трудно.

Мое решение — перенести проверку на компилятор. Заодно он и подсказывать будет, чтобы не запоминать названия.

Для каждого звука я генерирую маленькую однострочную функцию следующего вида:
public static function s_plasma_shoot():void  { Play('01_plasma_shoot'); }
public static function s_plasma_blast():void  { Play('02_plasma_blast'); }
Где «s_» — префикс, «Play()» — функция, непосредственно проигрывающая звук.

Батник (с ним мне пришлось сегодня повозиться, именно он стал причиной написания поста):
setlocal EnableDelayedExpansion
FOR %%i IN (*.mp3) DO set v=%%~ni && set vv=s_!v:~3,-1!():void && echo public static function !vv! { Play('%%~ni'); }

endlocal
Как я уже писал, у меня все файлы звуков пронумерованные, а в функциях я хочу использовать имена без номеров, поэтому батник немного отличается от предыдущих.
setlocal EnableDelayedExpansion
Без этой команды изменение переменных в цикле не работает.
set v=%%~ni
Копируем значение во временную переменную, потому что сабстринг применять к переменной цикла нельзя.
set vv=s_!v:~3,-1!():void
Вот эта команда делает сабстринг в новую переменнуи одновременно добавляет префикс «s_» и постфикс "()void". Постфикс нужен тут, потому что в echo эта переменная выводится с лишним пробелом в конце. Баг, который я так и не смог победить. В данном конкретном случае это неважно, просто я код скопипастил из другого места :)

Теперь, из любого места можно проиграть звук просто вызвав функцию:
SoundPlayer.s_plasma_blast();
На мой взгляд очень удобно.

Когда музыкант присылает звуки обратно и настает время делать финальную сборку, добавляем эмбеды:
[Embed(source='../assets/mfx96/01_click.mp3')]
private static var k_click:Class;
private static var s_click:Sound = new k_click();
и заменяем URLRequest'ы на присваивания:
songs['click'] = s_click;


Батники уже постить не буду, надоело.

SWC.
Чтобы уже добить пост до конца, в 2 словах расскажу про эмбед из swc.
Собственно, при использовании swc самая большая проблема остается открытой — по имени класса нельзя получить экземпляр или ссылку на класс.

Все это решается примерно так же, как с файлами.
Просто запихиваем в словарь ссылки с помощью кодегена.

Расскажу на словах (кода не будет, я так делал всего пару раз и технология не отработана).

Переименовываем .swc в .zip и вытаскиваем catalog.xml
Он выглядит примерно так:
<?xml version="1.0" encoding ="utf-8"?>
<swc xmlns="http://www.adobe.com/flash/swccatalog/9">
  <versions>
    <swc version="1.2" />
    <flash version="11.5" build="d349" platform="WIN" />
  </versions>
  <features>
    <feature-script-deps />
    <feature-files />
  </features>
  <libraries>
    <library path="library.swf">
      <script name="Dron" mod="1333043131062" >
        <def id="Dron" /> 
        <dep id="AS3" type="n" /> 
        <dep id="flash.display:MovieClip" type="i" /> 
      </script>
      <script name="DronBulletBlast" mod="1333043131062" >
        <def id="DronBulletBlast" /> 
        <dep id="AS3" type="n" /> 
        <dep id="flash.display:MovieClip" type="i" /> 
      </script>
    </library>
  </libraries>
  <files>
  </files>
</swc>


Строчки:
<def id="Dron" /> 
<def id="DronBulletBlast" /> 
это и есть имена классов.

Собственно вот и все.
Вытаскиваем xml, пробегаем по нодам, для каждого нужного нода — генерируем код.
  • +35

Комментарии (29)

+2
Дорогой сударь, я не прекращаю восхищаться вами и вашей работой, и в знак своего уважения оставляю сей комментарий и символичный «плюс» под вашим постом.
  • SeeD
  • SeeD
0
Эх… Сколько я искал такую возможность год назад. Сейчас уже как-то научился обходиться.
Но, в любом случае, это решение пригодится на 100%.
+1
Только кажется нелогичным в первом классе название метода Get, при наличии Bmp. Более очевидно bmd() и bmp() (у меня это вообще было бы getBitmapData() и getBitmap() — не экономлю на буквах в названиях методов).
А по сути — здорово! Спасибо.
0
Спасибо, пригодится.
Пойду еще почитаю про батники.
0
Можешь статами поделиться из игры XenoSquad? Сколько пикч там.
0
В гуи — 147.
Текстурки картинками — 73.
Остальное в зипах и загружается другим способом.
+2
Да, очень задалбывает руками ассеты добавлять. У меня похожий скрипт на python.

Есть пара моментов, которые я хотел бы дополнить:
1. Удобно сделать не функции Get(s:String):BitmapData, а статические переменные в классе:
public static var someAssetName:BitmapData

Т.к. в итоге автодополнение подсказывает имена ассетов при использовании.
2. Удобно иметь возможность сгенерить два варианта класса: первый выдает за-embed-енные картинки, а второй динамически загружает картинки при старте флешки, так художник может сам без помощи программиста пробовать новые картинки, да и даже самому удобнее графику тестировать.
3. Удобно по картинкам в одной папке с названиями 01.png, 02.png и т.д. делать объявление массива, а не кучи переменных:
public static var someAssetName:Vector<BitmapData>

4. У меня еще этот скрипт делает из png два jpeg с цветом и альфаканалом для экономии места.
+1
Чтобы не быть голословным вот мой скрипт. Он не умеет пока делать массивы, но я обязательно это сделаю. Еще ему надо сделать так, чтобы он даты результирующих файлов подставлял как у исходных, а то при каждом запуске куча файлов обновляется…
Код кривой-кривой, но работает.
import Image, os, sys, fnmatch

BASE_PATH = os.path.dirname(sys.argv[0])
SRC_PATH = os.path.join(BASE_PATH, "gfx_src")
DST_PATH = os.path.join(BASE_PATH, "gfx")
GFX_AS_FILE = os.path.join(BASE_PATH, "src", "Gfx.as")

class MakeGfx:
    varDecl = []
    embedFunc = []
    loadFunc = []

    def processFile(this, path):
        relFname = os.path.splitext(os.path.relpath(path, SRC_PATH))[0]
        print relFname
        image = Image.open(path)
        if image.mode != "RGBA":
            image = image.convert("RGBA")
        outFname = os.path.join(DST_PATH, relFname + "_rgb.jpg");
        outDir = os.path.dirname(outFname)
        if not os.path.exists(outDir):
            os.makedirs(outDir)
        image.save(outFname)
        r,g,b,a = image.split()
        outFname2 = os.path.join(DST_PATH, relFname + "_alpha.jpg");
        a.save(outFname2)

        className = relFname.replace(os.sep, "_").replace("-", "_");
        loadPath = ("../gfx_src/" + relFname).replace(os.sep, "/")
        embedPath = os.path.relpath(outFname, SRC_PATH).replace(os.sep, "/")
        embedPath2 = os.path.relpath(outFname2, SRC_PATH).replace(os.sep, "/")
        this.varDecl.append("\t\t" + "public static var " + className + ":BitmapData;")
        this.embedFunc.append("\t\t\t" + "[Embed(source=\"" + embedPath  + "\")] var " + className + "_RGB:Class;")
        this.embedFunc.append("\t\t\t" + "[Embed(source=\"" + embedPath2  + "\")] var " + className + "_ALPHA:Class;")
        this.embedFunc.append("\t\t\t" + className + " = CreateFromJpegs(" + className + "_RGB, " + className + "_ALPHA);")
        this.embedFunc.append("")
        this.loadFunc.append("""                 function load_{0}():void
                        {{
                                function onLoadComplete(e:Event):void 
                                {{
                                        {0} = (loader.contentLoaderInfo.content as Bitmap).bitmapData;
                                        --loadingCount;
                                        if (loadingCount <= 0)
                                                onComplete();
                                }}
                                ++loadingCount;
                                var loader:Loader = new Loader();
                                loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete);
                                loader.load(new URLRequest("{1}.png"));
                        }}
                        load_{0}();""".format(className, loadPath));


    def processFolder(this, path):
        for filename in os.listdir(path):
            fullname = os.path.join(path, filename)
            if os.path.isdir(fullname):
                this.processFolder(fullname)
            elif os.path.isfile(fullname) and fnmatch.fnmatch(fullname, "*.png"):
                this.processFile(fullname)

    def writeFile(this, filename):
        gfxAs = open(GFX_AS_FILE, "w")
        gfxAs.write("""package
{
        import flash.display.Bitmap;
        import flash.display.BitmapData;
        import flash.display.BitmapDataChannel;
        import flash.display.Loader;
        import flash.events.Event;
        import flash.geom.Point;
        import flash.geom.Rectangle;
        import flash.net.URLRequest;
        public class Gfx
        {
""")
        gfxAs.write("\n".join(this.varDecl))
        gfxAs.write("""
                public static function init(onComplete:Function):void
                {
                        if (Main.EMBED_GFX)
                                initEmbed(onComplete);
                        else
                                initLoad(onComplete);
                }
                private static function initLoad(onComplete:Function):void
                {
                        var loadingCount:int = 0;
""")
        gfxAs.write("\n".join(this.loadFunc))
        gfxAs.write("""
                }
                private static function initEmbed(onComplete:Function):void
                {
""")
        gfxAs.write("\n".join(this.embedFunc))
        gfxAs.write("""
                        onComplete();
                }
                private static function CreateFromJpegs(color:Class, alpha:Class):BitmapData
                {
                        var colorBmp:BitmapData = (new color).bitmapData
                        var alphaBmp:BitmapData = (new alpha).bitmapData
                        
                        var bmp:BitmapData = new BitmapData(colorBmp.width, colorBmp.height);
                        var rc:Rectangle = colorBmp.rect;
                        var pt:Point = new Point();
                        bmp.copyPixels(colorBmp, rc, pt);
                        bmp.copyChannel(alphaBmp, rc, pt, BitmapDataChannel.RED, BitmapDataChannel.ALPHA);
                        
                        colorBmp.dispose();
                        alphaBmp.dispose();
                        
                        return bmp;
                }
        }
}""")
        gfxAs.close()        

def main():
    makeGfx = MakeGfx()
    makeGfx.processFolder(SRC_PATH)
    makeGfx.writeFile(GFX_AS_FILE);
    
if __name__ == "__main__":
  main()
0
… для экономии места
Имеется в виду наверное трафик?
Под местом в данном контексте все же как-то привычнее понимать область памяти, которая в лучшем случае от этого трюка с «PNG -> 2 x JPG» никак не уменьшится.
0
Под местом имеется ввиду размер флешки. Расход памяти по идее не должен меняться, в итоге в памяти та же самая битмапдата остается. Во время загрузки может больше памяти использоваться временно. Не делал тестов для памяти, а размер флешки уменьшается очень заметно.
0
Во время загрузки, да и после нее, упакованные ассеты будет занимать место, которое данный трюк таки экономит. Я скорее о том, что на фоне размеров, до которых потом вся эта красота «разворачивается» — это крохи… которые, все же, в отдельных случаях приходится собирать, согласен.
0
Размер флешки — это заметная характеристика. На том же FGL в параметрах ее все видят, в т.ч. и спонсоры. А Потребление памяти наоборот — слабо заметная характеристика. Если ее хватает.
Время на сборку всего этого в памяти уходит конечно, но не очень много: меньше секунды в моем случае, и обычно все равно на старте куча всякой инициализации и приходится заставлять игрока подождать, на этом фоне вообще не заметно.
0
Ээх :) я наверное сильно погрузился в мобильную разработку. Как раз там картинки в памяти — предмет пристального внимания разработчика. А вот крошечные отличия в весе одной PNG от двух JPG последнее, что приходит на помощь.
0
Конечно для мобилок всё иначе :-)

Но если например компилировать флешку для мобилок, то можно переделать скрипт, а исходники игры в части, где используются ассеты не менять…
0
Да, скрипт гибкий. Только это все стремительно теряет актуальность в мобильной разработке. Там приложение разумнее всего делать на основе Stage3D, отчего такие массы ассетов уже не применяются — пару атласов можно вписать руками.
0
Да, атласы тоже требуют скриптов, но уже совсем других…
0
Ну да ) собрать, разобрать. TexturePacker неплохо собирает.
+3
Собственно, при использовании swc самая большая проблема остается открытой — по имени класса нельзя получить экземпляр или ссылку на класс.
Можно же сделать вот так:
trace(getDefinitionByName("SomeClassName"));


Чтобы класс не нужно было включать в заголовках импорта, можно импортировать библиотеку «особым» образом:


Ну и на самом деле я привык нормально называть объекты, и использовать нормальное обращение
var foo:Bar = new Bar();
0
да, батники полезная вещь)
0
Собственно, при использовании swc самая большая проблема остается открытой — по имени класса нельзя получить экземпляр или ссылку на класс.
Доктор, я наверное что-то не так делаю, но у меня почему-то все ассеты из swc доступны непосредственно (через new MyAssetFromSwcName();) как-то так вот :)

А за батник — спасибо. А то все при них забывают, всякие питоны и прочие скрипты используют, а батники то еще ого-го и устанавливать ничего дополнительного не надо.
0
(через new MyAssetFromSwcName();) как-то так вот :)
Имелось ввиду через строковую константу.
Можно через getDefinitionByName, но как-то мне не понравился такой подход.
Даже не помню почему.
0
наверное потому что getDefinitionByName очень тормозной, прокешировать имена в массиве или объекте уже дает прирост производительности. А если для этого своя структура с хешами, сортировкой и прочими ускорителями, то getDefinitionByName ваще в черной дыре.
+2
Спасибо, полезно.

Дополню пост ссылкой: Как перенаправить стандартный вывод в файл?
  • Rigo
  • Rigo
0

ээээм… что я делаю не так?
отлично вижу имена всех названных классов, через подключенную swc, могу оверроудить да вообще что угодно могу, это же мовиклипы
0
Покажи, как ты играешь звуки из swc.
Например, тридцать седьмой звук.
0
Речь идет о получении классов по строковому имени. Это нужно, например, при загрузке уровней из xml, чтобы не делать огромные switch-case.
0
пишу социалки, по этому описание всех объектов валяется отдельно в json, по описанию генериться имя… но это конечно другое
0
Только у меня бантик ничего не выводит на этих строчках???
@FOR %%i IN (*.png *.jpg) DO ^
echo [Embed(source='../../../assets/gui/%%i')] && ^
echo private static var k_%%~ni:Class;
0
Простите я все понял :)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.