Растровый рендер в as3. Двигаем тысячи картинок

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

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

Вообще обычно у меня не бывает времени на написания каких-либо статей либо ведения блогов, но тут на днях пришлось как раз добиваться примерной производительности для большого количества объектов на экране. В результате применил метод, а заодно подготовил небольшую демку, которую и опубликую тут — тем более, что частенько слышу вопросы о том, что это вообще такое и как вообще использовать.

Итак, задача начального уровня: двигать по экрану пару-тройку тысяч битмапов. Пусть это будут разноцветные разнокалиберные шары.
Решение: чисто физически ничего двигать мы не будем. А будем на каждой итерации рассчитывать координаты объектов, и рисовать их на холсте.

Сноска: код сделан для публикации в Flash Develop'е, но его легко применить и в других средах.

Создадим класс для наших летающих шариков. Называется класс cube, потому что в первой версии летали кубики :) Переименовывать было лень.

package objects 
{
        import com.greensock.easing.Linear;
        import flash.display.BitmapData;
        import flash.display.Shape;
        import flash.geom.Point;
        import flash.geom.Rectangle;
        import utils.xorRand;
        /**
         * ...
         * @author Platon Skedow
         */
        public class cube 
        {
                public var point:Point;
                public var rect:Rectangle;
                private var _bitmapData:BitmapData; //здесь храним наше изображение
                private var _x:Number; 
                private var _y:Number;
                private var _this:cube;
                private var desPoint:Point;
                private var desX:Number;
                private var desY:Number;
                private var speed:int;
                private var stepX:Number;
                private var stepY:Number;
                public function cube() 
                {
                        
                }
                //генератор цвета
                private function get generateRndColor():uint
                {
                        var color:uint = Math.random() * 0x1000000;
                        return color;
                }
                //генератор круга
                private function doDrawCircle(size:uint):Shape {
            var child:Shape = new Shape();
            var halfSize:uint = size / 2;
            child.graphics.beginFill(generateRndColor);
            child.graphics.drawCircle(halfSize, halfSize, halfSize);
            child.graphics.endFill();
                        return child;
        }
                
                public function init():void 
                {
                        _this = this;
                        //создаем шарик рандомного цвета, и сохраняем картинку
                        var sizeXY:int = xorRand.randRange(1, 20);
                        var shape:Shape = doDrawCircle(sizeXY);
                        _bitmapData = new BitmapData(sizeXY, sizeXY, true, 0);
                        _bitmapData.draw(shape);
                        rect = new Rectangle(0, 0, sizeXY, sizeXY);
                        
                        //начальные координаты
                        var r:int = xorRand.randRange(1, 4);
                        switch ® 
                        {
                                case 1://право
                                {       
                                        x = -80+xorRand.XORrandom*20;
                                        y = xorRand.XORrandom * Main.sHeight;
                                        break;
                                }
                                case 2://лево
                                {       
                                        x = Main.sWidth + 80+xorRand.XORrandom*20;
                                        y = xorRand.XORrandom * Main.sHeight;
                                        break;
                                }
                                case 3://верх
                                {       
                                        x = xorRand.XORrandom * Main.sWidth;
                                        y = -80+xorRand.XORrandom*20;
                                        break;
                                }
                                case 4://низ
                                {       
                                        x = xorRand.XORrandom * Main.sWidth;
                                        y = Main.sHeight + 80+xorRand.XORrandom*20;
                                        break;
                                }
                        }
                        point = new Point(x, y);
                        
                        setNewDirection();
                }
                
                //задаем направление
                private function setNewDirection():void 
                {
                        var r:int = xorRand.randRange(1,4);
                        
                        switch ® 
                        {
                                case 1://право
                                {       
                                        desX = -80;
                                        desY = xorRand.XORrandom * Main.sHeight;
                                        break;
                                }
                                case 2://лево
                                {       
                                        desX = Main.sWidth + 80;
                                        desY = xorRand.XORrandom * Main.sHeight;
                                        break;
                                }
                                case 3://верх
                                {       
                                        desX = xorRand.XORrandom * Main.sWidth;
                                        desY = -80;
                                        break;
                                }
                                case 4://низ
                                {       
                                        desX = xorRand.XORrandom * Main.sWidth;
                                        desY = Main.sHeight + 80;
                                        break;
                                }
                        }
                        
                        desPoint = new Point(desX, desY);
                        
                        
                        //скоростть движения
                        speed = xorRand.randRange(1, 3);
                        
                        var dist:Number = Point.distance(point,desPoint);
                        var numSteps:int = Math.floor(dist / speed);
                        var dist_x:Number = x - desX;
                        var dist_y:Number = y - desY;
                        
                        //скорости смещения по осям
                        stepX = dist_x / numSteps;
                        stepY = dist_y / numSteps;
                        
                        
                        
                }
                
                public function get bitmapData():BitmapData 
                {
                        return _bitmapData;
                }
                
                public function set bitmapData(value:BitmapData):void 
                {
                        _bitmapData = value;
                }
                
                public function get x():Number 
                {
                        return _x;
                }
                
                public function set x(value:Number):void 
                {
                        _x = value;
                }
                
                public function get y():Number 
                {
                        return _y;
                }
                
                public function set y(value:Number):void 
                {
                        _y = value;
                }
                
                //каждый шаг проверяем попадание в радиус конечной точки, и либо пересчитываем координаты, либо задаем новый вектор
                public function move():void {
                        
                
                        if (Point.distance(point,desPoint)> 20) 
                        {
                                x -= stepX;
                                y -= stepY;
                                
                        }
                        else {
                                setNewDirection();
                        }
                        
                        //используется при рендере
                        point = new Point(x, y);
                        
                }
                
        }

}


Здесь xorRand.XORrandom возвращает случайное число (от 0 до 1), randRange — возвращает случайное целое число в указанном диапазоне. Замена стандартного Math.random — более рандомный и более быстрый.


package utils
{
        public class xorRand
        {
                private static const MAX_RATIO:Number = 1 / uint.MAX_VALUE;

                private static var r:uint = Math.random() * uint.MAX_VALUE;
                /* Возвращает случайное целое число в указанном диапазоне */
                public static function randRange(minNum:int, maxNum:int):int 
                {
                        return (Math.floor(XORrandom * (maxNum - minNum + 1)) + minNum);
                }
                public static function get XORrandom():Number
                {
                  r ^= (r << 21);

                  r ^= (r >>> 35);

                  r ^= (r << 4);

                  return (r * MAX_RATIO);
                }
                
        }
}


Главный класс, где мы создаем наши шарики и оживляем их.
Внимание
Для хранения ссылок на объекты используется aCubes типа array — потому что проект публиковался под девятую версию плейера. Можно раскомментить закомменченные строчки кода, и тогда будет использоваться более шустрый Vector.

package 
{
        import flash.display.Bitmap;
        import flash.display.BitmapData;
        import flash.display.Sprite;
        import flash.display.StageAlign;
        import flash.display.StageScaleMode;
        import flash.events.Event;
        import flash.geom.Rectangle;
        import objects.cube;
        import utils.Stats;
        
        /**
         * ...
         * @author Platon Skedow
         */
        public class Main extends Sprite 
        {
                private var field:BitmapData;
                private var fieldBMP:Bitmap;
                private var stageRect:Rectangle;

                //private var aCubes:Vector.<cube>;
                private var aCubes:Array;
                
                public static var sWidth:Number;
                public static var sHeight:Number;
                
                public function Main():void 
                {
                        if (stage) init();
                        else addEventListener(Event.ADDED_TO_STAGE, init);
                }
                
                private function init(e:Event = null):void 
                {
                        removeEventListener(Event.ADDED_TO_STAGE, init);
                        stage.align = StageAlign.TOP_LEFT;
                        stage.scaleMode = StageScaleMode.NO_SCALE;
                        // entry point
                        stage.addEventListener(Event.RESIZE, stage_resize);

                        //определяем размер сцены
                        sWidth = stage.stageWidth;
                        sHeight = stage.stageHeight;

                        //наш холст. В него рисуем все объекты
                        field = new BitmapData(sWidth, sHeight, true, 0);
                        fieldBMP = new Bitmap(field);
                        stageRect = new Rectangle(0, 0, sWidth, sHeight);
                        
                        //aCubes = new Vector.<cube>();
                        aCubes = new Array();
                        for (var i:int = 0; i < 2000; i++) 
                        {
                                var cub:cube = new cube();
                                cub.init();
                                aCubes.push(cub);
                        }

                        addChild(fieldBMP);
                        addChild(new Stats());
                        addEventListener(Event.ENTER_FRAME, update);
                }
                
                private function update(e:Event):void 
                {
                        // очищаем холст
                        field.fillRect(stageRect, 0);
                        //aCubes.forEach(drawBitmaps); // раскомментить для использования Vector
                        for (var i:int = 1; i < aCubes.length; i++) 
                        {
                                var item:cube = aCubes[i];
                                // пересчитываем координаты
                                item.move();

                                //отрисовываем объект на холсте
                                field.copyPixels(item.bitmapData, item.rect, item.point, null, null, true);
                        }
                }

        /*      private function drawBitmaps(item:cube, index:int, vector:Vector.<cube>):void {
                        item.move();
                        field.copyPixels(item.bitmapData, item.rect, item.point, null, null, true);
                };*/
                
                private function stage_resize(e:Event):void 
                {
                        sWidth = stage.stageWidth;
                        sHeight = stage.stageHeight;
                        // можно как-нибудь более элегантно отресайзить холст, но работает и так.
                        removeChild(fieldBMP);
                        field = new BitmapData(sWidth, sHeight, true, 0);
                        fieldBMP = new Bitmap(field);
                        stageRect = new Rectangle(0, 0, sWidth, sHeight);
                        addChild(fieldBMP);
                }
                
        }
        
}


Все. Запускаем, любуемся.
Желающие могут скачать себе скринсейвер.
Или даже исходники.

Что еще нужно для полного щастя? Несколько вещей.
  • Средства для реализации моушн блюра — сглаживать неровности в движении
  • Средства для отрисовки кадров мувиклипов, а так же для трансформации объектов — скейлинг, вращение, альфа и др.
  • Средство кэширования объектов
Про всю это красоту напишу в следующие разы, если обчество эту статейку хорошо примет.
  • +32

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

0
нужно, полезно, плюсую :)
  • hitab
  • hitab
0
оп-оп, плюсую. Делал подобное в игре, но фпс был ниже. Щас гляну как у тебя сделано — буду у себя оптимизировать.
  • z3lf
  • z3lf
+2
Если оптимизировать, то до конца.

Зачем в функции move() делается каждый раз «new point()»? Чтобы чаще дергать GC?
Геттеры/сеттеры не нужны.
Ну и всякое там еще по мелочи.
  • ryzed
  • ryzed
0
Ну пойнт-то все равно придется делать. Да и геттеры с сеттерами — не помешают :) А оптимизировать до конца нет необходимости, это просто демонстрация метода.
0
Да, это я так.
Метод хороший.
Для тех, кто не в курсе будет полезно.
+1
Ай, зацепил.
Еще надо переделать на связный список и по максимуму избавиться от вызовов методов.
Убрать деления и считать дистанс руками, чтобы без корня.
Еще дистанс можно считать один раз, а потом только апдейтить линейно.

Но это я не со зла, просто еще отойти не могу от своих развлечений с оптимизацией :)
0
Мне до твоих опытов, камрад, как до луны на собаке :)
0
задача начального уровня: двигать по экрану пару-тройку тысяч битмапов
Я у себя на полном экране насчитал только 1520, где остальные?
+8
Недавно писал аналогичный брутальный тест (жать стрелки) copyPixels'а )
+2
3500 анимированных спрайтов при 30 фпс — неожиданно, можешь тоже написать немного об этом? :)
+2
да ты охуенен
0
А исходник можно? Один друг просил на посмотреть :D
+5
А че в исходниках к посту цветные кружочки заменить на цветные спрайтики, уже не в состоянии самостоятельно?))
+2
Да куда мне, я и читаю то с трудом. :)
А если серьезно, то реализация разная у каждого.
Притом я слышал что XProger еще и, прошу прощения, «охуенен», вот и интересно на его код посмотреть.
0
Да, XProger крутой чувак, но судя по фпсу, там такой же как и «с кружочками» брутфорс копипикселс, а в части рендера там все у всех одинаково: залочил, очистил, в цикле скопировал, разлочил — для большого количества маленьких движущихся объектов это самый оптимальный вариант. Если есть большие битмапы то можно еще зоны перекрытия учитывать, и то что перекрывается не рисовать. Для малодинамичной картинки, можно вычислять область перерисовки, и только ее перерисовывать. Ну, и иногда полезно промежуточные результаты тоже отдельно где-то сохранять, например бэкграунд и пр. статичные куски.
0
Для малодинамичной картинки, можно вычислять область перерисовки, и только ее перерисовывать.
боюсь будет лагать, если каждый раз вычислять и держать в кеше бек и стат куски.
0
Ну да, у всех все почти одинаково, но меняешь 3 строчки и плюс 5-10 fps. Хотя да, все почти одинаково.
0
тоже так-то прикольно, у меня 5000 при 30 фпс :)… 6000 при 25… тоже напиши! 8)
+2
Кстати, для битмап без альфы отрисовка в графикс graphics.beginBitmapFill() быстрее работает чем copyPixels
+3
rumblesushi.com/bench/BMFill.html
0
Интересный тест. Однако при 5000+ объектов размера 64х64 картина поменялась с точностью до наоборот. CP — 38fps, BMFill — 34.
0
движок блогов подменил ( r ) на ® :)
0
Плюс!
0
у меня при таком рендере с 1300 до 1700 объектов на 60 фпс скакнуло когда я заменил
for (var i:int = 1; i < aCubes.length; i++) 
на перебор по двухсвязанному списку
0
Фор на две тысячи итераций явно не самое узкое место, он выполняется за пару десятков наносекунд, какого-то видимого изменения замена на списки не может привнести.
0
интересно насколько вырастет если хотя бы сделать

var aCubesLength : int = aCubes.length;
for (var i:int = 1; i < aCubesLength; i++) 
0
или если даже так:
var aCubesLength : int = aCubes.length;
while( --aCubesLength  > -1 ) 
0
а лучше так
while (aCubesLength--)
0
лучше списка не будет)
0
список, лист блин…
0
а не, по вики список
0
Еще можно по сократить время обращения к полям класса, т.е. пользоваться ими по ссылке через локальные переменные (для этого кода:
var arr:Array = aCubes; var f:BitmapData = field; 
... 
f.copyPixels(...)
... т.д.
). Не тестировал — но в теории должно работать быстрее. В байт-коде обращение к локальной переменной осуществляется одним опкодом getlocal, а обращение к полю двумя: через getlocal 0 (т.е. this.), а затем getproperty имя_поля.
0
а как на счет вызова переменной класса в теле функции класса vs. локальной переменной функции в теле функции, есть прирост или так же? )
+1
А не могли бы вы привести код?
Моя реализация односвязного списка работает медленнее for =(
  • AIR
  • AIR
0
завтра напишу пост об этом)
0
провел тестирование на добавление/перебор/удаление, у листа обход вайлом, массив через фор, и скорости перебора сопоставимые..0о mac os 10.6.8 safari 5.1 fp 10.3.181
0
Ну так код показывай.
0
да обычный код)
package {
        import BaseObject;
        public class ListPointer {
                public var first:BaseObject;
                public var last:BaseObject;
                public function ListPointer() {
                        first = null;
                        last = null;
                }
                public function add(obj:BaseObject):void{
                        if (last) {
                                last.next = obj;
                                obj.prev = last;
                                last = obj;
                        }else {
                                first = obj;
                                last = obj;
                        }
                }
                
                public function remove (obj:BaseObject):void{
                        if (obj.next) {
                                if (obj.prev) {
                                        obj.prev.next = obj.next;
                                        obj.next.prev = obj.prev;
                                        obj.prev = null;
                                }else {
                                        obj.next.prev = null;
                                        first = obj.next;
                                }
                                obj.next = null;
                        }else {
                                if (obj.prev) {
                                        obj.prev.next = null;
                                        last = obj.prev;
                                        obj.prev = null;
                                }else {
                                        first = null;
                                        last = null;
                                }
                        }
                }
                
                public function removeALL():void {
                        while (last) {
                                this.remove(last);
                        }
                }
        }
}


baseObject должен содрежать next и prev типа baseObject
0
МужиГ!
0
Для меня оптимизация производительности пока темный лес. Но становится актуальной… спасибо за статью. Плюсую!
0
Хороший метод. Один минус — потеря субпиксельной точности… Движение «по лесенке» напрягает (
0
Можно использовать такой код. И будет вам сглаживание.
var matrix:Matrix = new Matrix(1, 0, 0, 1, circle.point.x, circle.point.y);
bitmapData.draw(circle.bitmapData, matrix, null, null, null, true);

При этом у меня количество объектов, которые можно показать без тормозов снижается в ~20 раз.
+3
Я оптимизировал и заодно отрефакторил эту флешку и она стала работать быстрее в полтора раза.
Пример
Исходники

Могу написать урок об этом =)
0
Пиши!!! :)
0
Однозначно пиши! )
0
+1
0
будет ещё быстрее если напишешь вместо
for (var i:int = 0; i < N; i++){
   var circle:Circle = circles[i];
   ...
}


хотя бы:
var circle:Circle;
for (var i:int = 0; i < N; i++){
   circle = circles[i];
   ...
}
+3
Не будет. Если описать это поведение простыми словами, то в AVM2 все локальные переменные объявляются в начале методов, поэтому даже такой нелепый код, приведенный ниже, скомпилируется и будет работать точно также, как и ваши:
for (var i:int = 0; i < N; i++){
   circle = circles[i];
   ...
}
var circle:Circle;
0
Это правда. Но, кстати, я уже стал выносить много таких объяв в начало метода, чисто потому, что будет ругаться варнингами если у меня еще один такой цикл найдется в теле метода. Это касается и i.
Другое интерсно, раньше проверял и было выгодно писать
circle = Circle(circles[i]);

Ибо из Array общее приведение начинает более геморную по времени проверку, а если прямо указать что я уверен что там Circle, то проверки нет. Надо бы глянуть в байткод.
0
circle = circles[i] as Circle;
0
jacksondunstan.com/articles/830
jacksondunstan.com/articles/1368
0
Та они задолбали, сначала быстрее было то, как я написал, есть несколько сборок перфоманс советов где это было так. Потом с каким-то плеером уже наоборот. Потом опять.
Но я даже говорил тут о неприкащенном обращении к элементу массива.
0
Вообще да, тема неоднозначная, может зависить от типа (релиз/дебаг) и версии плеера и еще кто знает от чего )
0
и кстати по твоей свежей ссылке как раз Circle(circles[i]) быстрее почти в два раза :)
А вот MovieClip(mcs[i]) наоборот :)
0
+ О пользе кастинга
jacksondunstan.com/articles/1305
0
ага я об этом. Меня поражает что Вектор тоже такой, хотя тип то известен.
+1
Не могу увидеть «полтора раза». fps так же упирается в 30, ms также скачет вокруг 33-34.
0
Есть исходники. Я компилировал их, подставляя разные значения. При 6 000 кругов с моим кодом 27-26 fps. При том же количестве с исходным кодом 20 fps. Я ошибся. Не на 50%, а на 30%. Но тоже не мало.
0
Код стал аккуратнее, но не намного быстрее.
Я проверил на 20000 кружочков, было 18-19 fps, но я точно знаю что можно сделать 26-28 fps, а может и больше.
Я щитаю :)
+1
Может и быстрее но у вас периодически все дергается — а у автора статьи таких дерганий нет.
0
Перепутал кнопку увеличивающую рейтинг с кнопкой «Ответ на» =). Где вы заметили дерганье, которого нет у автора статьи?
0
Дергаются (отнюдь не от округления) каждые 3-4ю секунды — причем дергаются под Виндой ФФ гораздо больше чем под маком ФФ. Вы не подумайте что это ваш код косячит — на форуме уже устали обсуждать эту проблему — просто у оригинальной флешки этих дерганий нет вот это и интересно. А вы рендерите обчным методом и ничего волшебного тут нет — все так делают и у всех дергается. Причем (только FYI) пытаются урезать фпс чтобы хоть както сгладить.
0
А я использую Flixel и не заморачиваюсь… :)
Правда с Flixel давно уже не работал… ммм…
0
Мой почтовый ящик насмерть заспамлен сообщениями с блогов. Вот и публикуй после этого статьи :)
+1
предлагают увеличить член фпс?
0
вот и еще один коммент на почту прилетел. Спасибо большое ;)
+2
Под молехил пора уже туторы писать :)
  • ryzed
  • ryzed
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.