Кеширование MovieClip

Когда я заметил, что мои игры сильно тормозят с релизной графикой я решил её как-то оптимизировать. Стандартный cacheAsBitmap не помог. Порывшись на форумах и гуглах я выяснил, что лучше самому кешировать клипы в Bitmap. Даже нашёл исходники кеширования от TouchMyPixel, разработчиков игры Scary Girl. Но он был слишком не универсальным, заставлял художника сделить за размером кадров. Поэтому взяв его за основу я написал свой класс, который превращает любой MovieClip в набор Bitmap'ов с учётом всех смещений. В дальнейшем этот класс можно будет использовать для создания атласов анимации, которые можно будет использовать при портировании Flash игр на другие платформы.

package elmortem.animations {
        import flash.display.Bitmap;
        import flash.display.BitmapData;
        import flash.display.MovieClip;
        import flash.display.Sprite;
        import flash.geom.Matrix;
        import flash.geom.Point;
        import flash.geom.Rectangle;
        
        public dynamic class CacheMovieClip extends Sprite {
                private var clip:MovieClip;
                private var bmp:Bitmap;
                public var frames:Vector.<BitmapData>;
                public var offsets:Vector.<Point>;
                public var labels:Vector.<String>;
                public var currentFrame:int;
                
                public function CacheMovieClip() {
                        clip = null;
                        addChild(bmp = new Bitmap());
                        frames = new Vector.<BitmapData>();
                        offsets = new Vector.<Point>();
                        labels = new Vector.<String>();
                        currentFrame = 1;
                }
                public function free():void {
                        if (parent != null) parent.removeChild(this);
                        clip = null;
                        removeChild(bmp);
                        bmp = null;
                        frames = null;
                        offsets = null;
                        labels = null;
                }
                
                public function buildFromLibrary(Name:String):void {
                        var cls:Class = getDefinitionByName(Name);
                        if(cls != null) buildFromClip(new cls());
                }
                public function buildFromClip(Clip:MovieClip):void {
                        if (Clip == null) throw("Clip not found.");
                        if (clip != null) {
                                clip = null;
                                frames = new Vector.<BitmapData>();
                                offsets = new Vector.<Point>();
                        }
                        
                        clip = Clip;
                        var r:Rectangle;
                        var bd:BitmapData;
                        var m:Matrix = new Matrix();
                        
                        var i:int;
                        var len:int = clip.totalFrames;
                        for (i = 1; i <= len; ++i) {
                                clip.gotoAndStop(i);
                                r = clip.getBounds(clip);
                                bd = new BitmapData(Math.max(1, r.width), Math.max(1, r.height), true, 0x00000000);
                                m.identity();
                                m.translate(-r.x, -r.y);
                                m.scale(clip.scaleX, clip.scaleY);
                                bd.draw(clip, m);
                                frames.push(bd);
                                offsets.push(new Point(r.x * clip.scaleX, r.y * clip.scaleY));
                                labels.push(clip.currentLabel);
                        }
                        
                        gotoAndStop(1);
                }
                
                public function get totalFrames():int {
                        return frames.length;
                }
                public function get currentLabel():int {
                        if(totalFrames == 0) return "";
                        return labels[currentFrame - 1];
                }
                
                
                public function gotoAndStop(frame:int):void {
                        if (totalFrames == 0) return;
                        currentFrame = Math.max(1, Math.min(totalFrames, frame));
                        bmp.bitmapData = frames[currentFrame - 1];
                        bmp.x = offsets[currentFrame - 1].x;
                        bmp.y = offsets[currentFrame - 1].y;
                }
                
                
                
                /* STATIC */
                static private var clips:Object = { };
                
                static public function getClip(Name:String):CacheMovieClip {
                        if(clips[Name] == null) {
                                var m:CacheMovieClip = new CacheMovieClip();
                                m.buildFromLibrary(Name);
                                clips[Name] = m;
                        }
                        
                        var clip:CacheMovieClip = new CacheMovieClip();
                        clip.frames = clips[Name].frames;
                        clip.offsets = clips[Name].offsets;
                        clip.labels = clips[Name].labels;
                        clip.gotoAndStop(1);
                        return clip;
                }
        }
}

Обратите внимание, что используя статичный метод getClip у нас создаётся библиотека клипов, откуда затем используются фреймы и оффсеты, чтобы не плодить их копии.

Update. Исправил ошибки, добавил лейблы.
  • +28

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

+4
1. getRect надо заменить на getBounds. Ибо метод getBounds выдает прямоугольник с учетом толщины линий, выступаюшего блюра/глоу. В итоге в твоем примере во многих случаях будут обрезаны края.
2. Прямоугольник, который отдается в этих методах имеет нецелые вообще говоря параметры. То-есть чтобы опять таки не получить обрезки по краям — применять Math.ceil для ширины/высоты и Math.floor для x и y.
3. Кстати матрицу пересоздавать каждый луп цикла не стоит, не мучайте гарбадж коллектор, да и вообще подобный подход сумарно на всем коде съэкономит перфоманс.
0
Принято, поправлю. Спасибо.
0
getBounds вроде как тоже не слижком с тенями и глоу дружит, нет?
0
Сорри, год назад этот класс тюнил :) В несколько этапов. Первый — требуется getBounds, а то реальные обрезки сразу получаются с ходу. Почему-то мне кажется что я где-то подробно читал что и блюр тоже вылавливается этим методом. Но не гарантирую. Все равно далее у меня в коде дополнительно бордюр 1 пиксель в битмапу (с соответствующим сдвигом) :). Здесь возможно либо глоу вылавливал, либо еще глюки. Вообще, каждый все равно думаю сам себе класс запилит-потюнит.
+3
Просто вот еще что хотелось бы отметить — вообще говоря параметры методов писать с большой буквы… такого нейминг конвеншна еще не видел, противоречит здравому смыслу. Вон подсветка тоже сдурела :)
0
Я в исходниках Flixel такое видел. Видимо это фишка скриптов кеширования :). Хотя сам такое практиковать стал. Удобно…
0
Это нужно, чтобы избавится от this. Обычно я так не делаю, просто недавно много писал на Flixel. (:
+2
Если потомки кэшируемого тоже клипы, то их тоже нужно щелкать на соответствующий кадр.
0
+1 В исходниках от touchmypixel есть нужный кусок кода. Иначе в составных мувиклипах будет некорректно кэшироваться анимация.
+1
Кстати, я от этого отказался. Слишком много граблей будет. Лучше всего просто отказаться от вложенных клипов по типу MovieClip а вкладывать как Graphic. Живее будете :)
0
Погляжу, спасибо. Так, глядишь, и доработаем класс. (:
0
Я вот понять не могу, кеширование мувиков переводит вектор в растр и заливает в битмапы?
а если в мувиках, в кадрах уже лежат картинки (такая обертка битмапа в мувик) производительность по сравнению с кешированием меняется?
0
Не должна, поидее. По сути я же тоже создаю спрайт, в который пихаю битмапку. Разве что в MovieClip много всего лишнего создаётся, а у меня голый Sprite.
0
полезно +1
0
Когда познакомился с библиотечкой от ТачМайПикселя, так и не понял, почему авторы наплевали на геометрические размеры элементов дальше первого кадра.

А камраду elmortem плюс конечно.
0
Там есть небольшой нюанс о котором не каждый знает:

1. Создаешь прямоугольник и конвертируешь его в MovieClip, прямоугольник обязательно должен быть установлен в координаты 0,0 т.е. в левый верхний угол.
2. Добавляешь данный клип на отдельный слой своего клипа, где у тебя анимация, подгоняешь размер прямоугольника, чтобы вся анимация оставалась внутри него. Присваиваешь имя прямоугольнику e_bounds.

И ваяля, ничего за края не выходит и вся анимация отображается как надо.
Пришлось изучить весь код тачмайпиксель, чтобы увидеть этот нюанс
0
Ну да, я тоже таким вот костылем изначально и пролечил :) Потом уже код немного дописал.
0
Да, у них так. Но ничего хорошего в этом не вижу. Живой пример номер один — у меня есть анимация взрыва. Понятно, что сначала он махонький, потом разрастается. При правильном подходе экономится память очень сильно. Пример номер два — бабочка, вид сверху, машет крыльями. Пример номер три — мячик падает на землю и подскакивает раза три. При подходе с e_bounds будет куча полупустых битмап, ибо размерчик надо будет делать большой, чтобы в одном углу поместился мячик в верхней фазе а в другом углу — внизу. Теоретически можно в клипе убрать перемещение, а реализовывать его программно, но в играх полно таких моментов, которые удобно аниматору синхронизировать и рисовать одновременно.

В этом подходе очень экономится память.

Поэтому у себя сделал примерно так как автор топика. Точнее, у меня отдельно класс «куча bmp кадров» и отдельно «bmp мувиклип», который по интерфейсу как Мувиклип, сам играет себя, в конце может дернуть экшн итд.
0
По мимо ускорения и экономии памяти, главный плюс моего кода в том, что мы можем сделать клип с центром в (0; 0), закешировать его и вращать. И всё будет корректно.
0
Хороший скрипт. На его основе удобные функции управления анимацией сделать можно.
0
У меня как раз есть класс для удобной анимации MovieClip'ов, поэтому я сделал свой CacheMovieClip динамическим и использую тот же поход. (:
0
А тепер вопрос.
Что если в скешированом мувике есть лейблы и управляющий код в кадрах? Как тогда? :)
0
Если код в кадрах то он потеряется при кэшировании. Код переносишь в соответствующий класс.
+2
Тогда надо оторвать руки тому, кто это делал. (:
0
Очень не помешал бы тест производительности.
0
Для клипов с одинаковым размером кадров можно глянуть тест производительности на ссылке в посте, т.к. подход один в один как у TouchMyPixel. Для клипов с разным размером кадров будет соответствующая экономия памяти под битмапы.
+2
offsets.push(new Point(r.x, r.y)); ---> offsets.push(new Point(r.x*clip.scaleX, r.y*clip.scaleY));
0
Кстати да, спасибо большое за найденный баг.
0
Для меня это самый полезный пост за время фгб. Спасибо большое.
0
из доработок предлагаю еще анализ кадров на наличие кода.
0
Зачем? Анимация должна быть анимацией. Без кода. Код в кадрах — злое зло.
0
ну gotoAndPlay() stop() добавлять кодом?
0
Зачем? Я выше написал — всяко лучше вставлять клип как Graphic. И gotoAndPlay() stop() там просто как номер стартового кадра и просто длина линии в леере.
0
Код в кадрах кстати бывает удобен. Иногда даже необходим. А вот для тех типов анимаций, которые будут в битмапы переводиться, я не вижу необходимости.
0
Для баннеров разве что…
0
+1
тоже использую переработанный класс от ТрогатьМойПиксель :)
сам не догадался до обрезания картинок…
из добавленного — сохраняю названия лэйблов, на случай если необходимо знать в каком месте анимация…

labels.push(clip.currentLabel);
...
public function get currentLabel():String{labels[int(currentFrame-1)];}
0
Добавил.
0
Опечатка: два totalFrames()
0
Спасибо, исправил.
0
тож писал подобную штуку. код только потолще. я если честно не понял зачем совать в разные битмап даты (ну и что что надо задавать область отрисовки? кроме области возможно еще нужен будет бибокс или какой нибудь шейп под физику кароче лично меня не напрягает кинуть в клип прозрачный прямоугольник draw_frame). если мы суем всё в одну битмап дату (или несколько, но имеется в виду спрайтщит именно) то вроде как лежат кадры в памяти близко и работа по отрисовке должна быть шустрее но это вряд ли так для флеша. опять же если мы так делаем (ЕСЛИ это реально работает) то придется проводить сортировку объектов до рендера сортируя по используемому ресурсу — а потом уже рисовать. по крайней мере для каких-нибудь частиц это делается элементарно. повторюсь понятия не имею верно ли это для флеша. еще при таком кешировании мы теряем основное преимущество мувиклипа — работа с вложенными мувиклипами (типа уйти ИМ на такой-то кадр). т.е. приходится составлять объекты уже в коде.

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

еще расскажи пожалуйста как ты реализовал переход по лейблу на нужную анимацию? типа gotoAndPlay(label:String) как у тебя работает? Был бы очень признателен за инфу. Сам выбрал довольно брутальный метод — лейблы это тупо uint и переход осуществляется по uint на номер лейбла а не по имени. Это не наглядно, а когда не наглядно голова болит.

Еще такой вопрос всплыл. Работая с физикой мы имеем дело с поворотами и пр. Приходится использовать матрицы и draw. Хотя copyPixel круче в разы. Насколько быстрее флеш рисует битмап дату (методом draw) чем не сложный вектор? И стоит ли заморачивать память кешированием всего и вся? Кто-нибудь делал сравнения? elmortem я как понял у тебя сейчас конкретная игра с этой технологией — поделись инфой сколько памяти выделяется под кеш?

За пост спасибо. Когда писал свой компилятор особо открытых кодов не было, приходилось сильно ломать голову)) Ну и да, согласен, можно сделать автоматический экспортер графики (резалку спрайтщитов)(куда нибудь в png) но надо подумать о сохранении данных об анимации/кадрах.
+1
Про битмапы. В моём случае много бетмапов нужно как раз для экономии памяти, Например при анимации взрыва, когда объект из точки превращается в большой шар дыма. При использовании прозрачного прямоугольника в первых кадрах у нас будет много ненужных пикселей.

Про встроенные клипы. Ими можно пожертвовать, если цена — значительное ускорение отрисовки. Или просто не кешировать такие объекты, если их не много (а обычно это так и есть).

Про дублирование. Оно сделано как раз для того, чтобы не плодить одинаковые данные. Если у нас есть 10 одинаковых объектов, то они будут использовать один набор кадров. Но при этом они независимо анимируются.

Про лейблы. Переход по именам лейблов не делел и вообще не представляю, нафига это нужно. При работе с анимацией я всё равно использую номера кадров. Но сделать это достаточно просто. Либо записать в Object номер кадра по ключу имени кадра, либо перебрать массив labels — индексы массивов совпадают.

Про повороты. В битмап флеш рисует очень быстро, но. В нашем случае это нужно только один раз. Потом Bitmap добавляется в Sprite и крутим мы уже его, штатными средствами.
Про игру. Да, писал всё это под конкретную задачу — ускорить игру. Конкретных цифр не знаю. Но на глаз игра ускорилась раза в 3. Всё и вся, конечно, кешировать не нужно. Например я персонажей не кеширую, они сложно-составные, но их не много. Интерфейс тоже не кеширую. Только блоки, декорации, объекты.

Пожалуйста. Атлас делается в нашем случае просто. Графику — в PNG, данные в XML.
0
Спасибо за ответ.

Про дублирование я плохо выразился — у меня двиг рисует всё в одну битмап дату — экран. Олдскульный такой подход. Сценграф флеша не используется при этом, никакие там new Bitmap() и т.п. не нужны. Поэтому для меня логично что ресурс как таковой не дублируется никаким способом. В твоем случае это нужно чтобы был Bitmap. Так как подходы разные то и методы разные. Мне например удобнее иметь совершенно отдельный класс который работает с единственным экземпляром ресурса переходя по кадрам и отрисовывая его на тот самый экран. Ну собственно в этом контексте и спрашивал что будет быстрее draw несложного вектора или draw того же вектора но кешированного в битмап дату?

Про лейблы всё очень просто — если ты используешь номера кадров это не наглядно. Ты потом сам читаешь код и должен вспоминать куда нафиг уходит этот gotoAndStop(182)? Можно сделать константу const ANIM_RUN:uint=182; и получим gotoAndStop(ANIM_RUN) что уже нагляднее, но удобнее было бы использовать лейблы чтобы не создавать лишнего кода. Попробую использовать Object для этой цели, вроде будет шустрее чем прокручивать список лейблов сравнивая строки.

Про экономию за счет записи множества экземпляров битмап_даты наверное соглашусь, но как-то после 2д опыта на опенГЛ такая мысль в голову просто не пришла)) Но я уже не раз сталкивался с тем что флеш вообще все законы рушит, так что наверное надо попробовать твой способ. Повторюсь спасибо за пост. Вообще приятно читать твои посты по реализации тех или иных методов — жду новых! Как вариант кстати не хватает цельного поста об организации своего редактора. Я б может и написал но у меня орфография кода хромает))
0
Еще добавлю два момента. Во-первых не нашёл dispose() для битмап даты — разве мусорщик очищает память от битмапов натыкаясь на null? И во-вторых было бы полезно добавить прямоугольники кадров для фреймов. Это нужно чтобы этот же класс можно было спокойно использовать с методом copyPixels() — ну либо найди другое решение. Думаю для быстрых партиклов кому-нибудь это точно понадобится. А вот лейблы для каждого кадра помоему не нужны. Можно скинуть в отдельный объект чтоп было start_frame и end_frame умножить на число лейблов, вроде меньше данных выйдет.
0
Про dispose ничего не знаю, надо почитать. Вроде как мусорщик удаляет всё, на что нет ссылок.
Прямоугольники кадров есть в самих кадрах, т.к. у нас каждый кадр рисуется в отдельную битмапу. Которая по размерам как раз равно кадру.
На счёт лейблов — пофиг. Там совсем мелочи, к тому же не дублируются.
0
Мусорщик bitmap data удалит. Но поскольку bitmap data — очень крупный объект, то рекомендуется очищать их самостоятельно, если они больше не нужны. А то пока не запустится следующий проход мусорщика (который произойдет, когда, грубо говоря, «память кончится»), они будут висеть в памяти и занимать очень много места.

Но если bitmap data просто создается на все время жизни флэшки (как кэш, например), то и удалять их явно не нужно.
0
Ну в нашем случае примерно это и происходит. Можно, конечно, для параноиков сделать статичную функцию очищения кеша, которая и будет все dispose делать. Подумаю над этим.
0
Про редактор буду на FlashGAMM рассказывать. Как раз к тому моменту, как выложат записи пост хороший напишу на эту тему.
0
Здорово. Буду ждать. Есть что обсудить)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.