Растровый рендер (анимация + движение + поворот)

Здравствуйте.

На FGB есть статьи посвященные растеризации и растровому рендеру.
1. flashgameblogs.ru/blog/actionscript/667.html
2. flashgameblogs.ru/blog/actionscript/713.html
3. flashgameblogs.ru/blog/actionscript/717.html
Спасибо авторам и надеюсь, они не против использования их статей и исходников в разработке. Копирайты из исходников сохранены. Эти 3 статьи легли в основу движка о котором эта статья.

Задача:
Двигать по полю размером 1280х1200 (видимая область флешки 640х600) юнитов с анимацией в разных направлениях с максимальной производительностью. Изначально юниты в векторе.

Решение:
Нужно объединить растеризацию MovieClip (1 статья) и растровый рендер (2 и 3).

PageUP – добавить 500 юнитов, PageDown – убрать 500 юнитов, Стрелки – двигать камеру. Правый клик – профайлер.
megaswf.com/serve/1179048
Тоже, но с фоном.
megaswf.com/serve/1179177

Весь движок состоит из 4 основных классов и 1 вспомогательного.

Что происходит в оригинальных классах более подробно можно прочитать в статьях.
Описание дано только для внесенных изменений.

1. Main.as
Код:
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.events.KeyboardEvent;
    import flash.geom.Point;
    import flash.geom.Rectangle;
    import flash.text.TextField;
    import flash.text.TextFieldAutoSize;
    import flash.ui.Keyboard;
    import utils.SWFProfiler;

           /**
         *  @author Platon Skedow
         *  refactoring & optimization noopic
         *  update Marcus
         */

    public class Main extends Sprite
    {
        public static var bitmapX:Number;
        public static var bitmapY:Number;
      
        private static const STEP:int = 500;
      
        private var n:int;
        private var textField:TextField = new TextField();
        private var _stageWidth:Number;
        private var _stageHeight:Number;
        private var _bgWidth:Number;
        private var _bgHeight:Number;
       
        private var bitmapData:BitmapData;
        private var bitmapDataBG:BitmapData;
        private var bitmap:Bitmap;
        private var bgRect:Rectangle;
        private var rectangle:Rectangle = new Rectangle();
        private var units:Array = [];

        private var _rastrender:Rastr;
        private var _ram:ramka_mc;
        private var _ramB:ramkaB_mc;
        private var _background:background_mc;
      
        public function Main():void
        {
            if (stage) init();
            else addEventListener(Event.ADDED_TO_STAGE, addedToStageListener);
        }
       
        private function addedToStageListener(e:Event):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, addedToStageListener);
            init();
        }
   
        private function init():void
        {
       SWFProfiler.init(stage, this);
       _rastrender = new Rastr();
//Cоздаем экземпляр класса Rastr, в котором происходит растеризация всех нужных нам MC.
       setStageParameters();
       _background = new background_mc();
//UPD1 Добавляем задний фон//
       createBitmap()
       createTextField();
       _ram= new ramka_mc();
       _ramB= new ramkaB_mc();
       addChild(_ram);
       addChild(_ramB);
//Добавляется рамка _ramB показывающая поле 1280х1200 и рамка _ram показывающая видимую область 640х600, чтобы визуально видеть края карты.
            addUnits();
            addEventListener(Event.ENTER_FRAME, enterFrameListener);
            stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownListener);
        }
       
        private function createTextField():void
        {
            textField.x = _stageWidth / 2 - 50;
            textField.background = true;
            textField.height = 30;
            textField.selectable = false;
            textField.backgroundColor = 0x000000;
            textField.textColor = 0xFFFFFF;
            textField.autoSize = TextFieldAutoSize.CENTER;
            textField.text = n.toString();
            addChild(textField);
        }
       
         private function keyDownListener(e:KeyboardEvent):void
        {
            if (e.keyCode == Keyboard.PAGE_UP)
                addUnits();
            if (e.keyCode == Keyboard.PAGE_DOWN)
                removeUnits();
       switch (e.keyCode)
       {     
         case 39 :
            this.x -= 20;
            bitmap.x += 20;
            _ram.x += 20;
            textField.x += 20;
            changeBitmapCord();
            break; // Влево
         case 37 :
            this.x += 20;
            bitmap.x -= 20;
            _ram.x -= 20;
            textField.x -= 20;
            changeBitmapCord();
            break; // Вправо
         case 40 :
            this.y -= 20;
            bitmap.y += 20;
            _ram.y += 20;
            textField.y += 20;
            changeBitmapCord();
            break; // Вверх
         case 38 :
            this.y += 20;
            bitmap.y -= 20;
            _ram.y -= 20;
            textField.y -= 20;
            changeBitmapCord();
            break; // Вниз
         }
// Добавлена прокрутка карты   
        }
private function changeBitmapCord():void
      {
         bgRect.x = bitmapX = bitmap.x;
         bgRect.y = bitmapY = bitmap.y;
      }
// UPD1 Сохранение координат bitmap в публичные переменные вынесено отдельно

        private function addUnits():void
        {
            n += STEP;
            for (var i:int = 0; i < STEP; i++)
            {
                units.push(new Unit(Amath.randomRangeInt(1,5)));
            }
            textField.text = n.toString();
        }
       
        private function removeUnits():void
        {
            if (n > STEP)
            {
                n -= STEP;
                units.splice(n, STEP);
                textField.text = n.toString();
            }
        }
       
        private function setStageParameters():void
        {
            stage.align = StageAlign.TOP_LEFT;
            stage.scaleMode = StageScaleMode.NO_SCALE;

        private function createBitmap():void
        {
            _stageWidth = 640;//App.stageWidth;
            _stageHeight = 600;//App.stageHeight;
            _bgWidth = 1280;//App.gameWidth;
            _bgHeight = 1200;//App.gameHeight;
            bitmapData = new BitmapData(_stageWidth, _stageHeight, true , 0x00000000);
            bitmapDataBG = new BitmapData(_bgWidth, _bgHeight, true , 0x00000000);
            bitmapDataBG.draw(_background);
            bgRect = new Rectangle(0, 0, _bgWidth, _bgHeight);
// UPD1 Создаем задний фон в виде Bitmapdata, отрисовываем только видимую часть
            bitmap = new Bitmap(bitmapData,"never",true );
            rectangle.width = _stageWidth;
            rectangle.height = _stageHeight;
            addChildAt(bitmap, 0);
            bitmapX = bitmap.x;
            bitmapY = bitmap.y;
        }

        private var i:int = 0;
        private var unit:Unit;

        private function enterFrameListener(e:Event):void
        {
            bitmapData.lock();
            bitmapData.fillRect(rectangle, 0x00000000);
//Можно убрать если есть фон, добавляющийся ниже и камера не выходит за границы игровой карты.
   
            bitmapData.copyPixels(bitmapDataBG, bgRect, bgPoint, null, null, false);
// UPD1 Добавляем задний фон. Цена - потеря ~3 FPS и пары МБ памяти (тест при 10 000 юнитов). При отсутствии фона закомментировать.

       while ( i < n)
            {
               unit = units[i];
               unit.move();
           if (unit._draw)
            {
               bitmapData.copyPixels(unit.bitmapData, unit.rectangle, unit.point, null, null, true);
            }
                //var matrix:Matrix = new Matrix(1, 0, 0, 1, unit.point.x, unit.point.y);
                //bitmapData.draw(bullet.bitmapData, matrix, null, null, null, true);
            i += 1;
            }
         i = 0;
            bitmapData.unlock();
        }
    }
}


Можно заметить, что используется bitmap'а размеров в видимую область которая просто сдвигается и перерисовывается.
В данном классе есть возможность реализовать трансформации (в нашем случае повороты) с помощью метода draw, но этот метод слишком медленный по сравнению с copyPixels, поэтому будет применяться другой способ.
Этот способ даст нам большой выигрыш по производительности, с вполне допустимым увеличением потребляемой памяти. (В нашем случае незначительным).

2. Unit.as
Код:
package
{
    import flash.display.BitmapData;
    import flash.geom.Point;
    import flash.geom.Rectangle;
          /**
         *  @author Platon Skedow
         *  refactoring & optimization noopic
         *  update Marcus
         */
    public class Unit
    {
        private static const MIN_SPEED:int = 1;
        private static const MAX_SPEED:int = 5;
        private static const RIGHT:int = 0;
        private static const LEFT:int = 1;
        private static const UP:int = 2;
        private static const DOWN:int = 3;
        private static const PADDING:Number = -20;
      
        public static const ANGLE_STEP:Number = 45;

        public var shoot:BmpFrames;
        public var _allmc:Array;

        private var i:int;
        private var n:int=0; //Кадр клипа из массива
        private var nStep:int=360/ANGLE_STEP;
       
        // Данные для отрисовки в BitmapData
        public var bitmapData:BitmapData;
        public var rectangle:Rectangle;
        public var point:Point = new Point();
        public var _draw:Boolean = true; // флаг приносящий нам пару fps, подробнее ниже
       
        private var destination:Point = new Point();
        private var stepX:Number;
        private var stepY:Number;
      
        private var tempX:Number=0;
        private var tempY:Number=0;
       
        private var type:int; // Тип юнита (в нашем случае есть 5 разных юнитов)
        private var state:int=2; // состояния юнита (идет, стоит , умирает и т.д.) у нас 2=идет

        private var _rotPos:int = 0; // в какую позициу смотрит юнит
        private var animationCount:Number=1;
        public var animationDelay:Number=1; //замедление движения и анимации

        private var gW:Number=1280;//App.gameWidth;
        private var gH:Number = 1200;//App.gameHeight;
        private var sW:Number=640;//App.stageWidth;
        private var sH:Number=600;//App.stageHeight;
      
        public function Unit(tp:int)
        {
       type = tp;
            init();
        }
        private function init():void
        {
       _allmc=Rastr._enemyR[type-1];
// Присваиваем нашему локальному массиву, массив растеризованных кадров определенного типа
       changeUnit(state);
//Выбираем состояние юнита, в нашем случае 2=ходьба
            setPosition(point);
            setNewDestination();
       tempX=point.x;
       tempY=point.y;
//зачем дублируем будет объесненно ниже
        }
   private function changeUnit(st:int):void
   {
   n = 0;
   shoot = _allmc[st-1];
   bitmapData = shoot.frames[n][_rotPos];
   rectangle = shoot.frameRs[n][_rotPos];
   _rtX = rectangle.x;
   _rtY = rectangle.y;
   i = shoot.totalFrames;
   }

   private function setNewDestination():void
        {
   setPosition(destination);
        calculateStep();
   changeAngle();
        }
       
        private function setPosition(point:Point):void
        {
            var direction:int = Amath.randomRangeInt(0, 3);
            if (direction == LEFT)
            {
                point.x = -PADDING;
                point.y = Amath.randomAdv * gH;
                return;
            }
            if (direction == RIGHT)
            {
                point.x = gW + PADDING;
                point.y = Amath.randomAdv * gH;
                return;
            }
            if (direction == UP)
            {
                point.x = Amath.randomAdv * gW;
                point.y = -PADDING;
                return;
            }
            if (direction == DOWN)
            {
                point.x = Amath.randomAdv * gW;
                point.y = gH + PADDING;
                return;
            }   
        }

   private var distance:Number;
   private var speed:Number;
   private var _rtX:int;
   private var _rtY:int;
   private var numSteps:int;
   private var distanceX:Number;
   private var distanceY:Number;

        private function calculateStep():void
        {
            speed = Amath.randomRangeNumber(MIN_SPEED, MAX_SPEED);
            distance = Amath.distance(point.x,point.y,destination.x,destination.y);
            numSteps = int(distance / speed);
            distanceX = destination.x - point.x;
            distanceY = destination.y - point.y;
            if (numSteps == 0)
            {
                stepX = 0;
                stepY = 0;
            }
            else
            {
                stepX = (distanceX / numSteps)/animationDelay;
                stepY = (distanceY / numSteps)/animationDelay;
            }
        }
   
        private var _angle:Number;

   private function changeAngle():void
   {
         // находим угол поворота юнита и задаем соответствующее значение переменной _rotPos;
         _angle = Amath.getAngleDeg(point.x,point.y, destination.x,destination.y);
         _rotPos = Math.round(_angle / ANGLE_STEP);
         if (_rotPos == nStep)
         {
         _rotPos=0;
         }
   }

        public function move():void
        {
         point.x = tempX += stepX; //320
         point.y = tempY += stepY; //300
//Центр отсчета в левом верхнем углу BitmapData, поэтому если мы захотим отобразить юнита например по центру (320,300), то он будет правее и ниже центра
// поэтому юнит смещается левее и выше (его bitmapdata) и его центр получается в нужных координатах
// оригинальные координаты сохраняются в tempX и tempY, по ним идет расчет движения юнита
// в point находятся координаты со всеми смещениями и служат для правильной визуализации (показывают точку куда копируется bitmapdata)

            if ( (Amath.distance(tempX,tempY,destination.x,destination.y) )<= MAX_SPEED)
//Измеряется растояние м/у точками
            {
            setNewDestination();
       }   
    if (animationCount >= animationDelay)
   {
   animationCount = 1;

        n++;
   if (n>=i)
   {
   n=0;
   }
//Зацикливаем анимацию

   point.x -= Main.bitmapX;
   point.y -= Main.bitmapY;
   if (point.x<=sW && point.x>=0)
            {
               if (point.y<=sH && point.y>=0)
               {
//Основной трюк повышающий производительность, если юнит не находится в видимой области, то он и не передается в битмапу и для него не находятся //поправки и прочее что нужно для визуализации. Позволяет в разы (у меня почти в 3) поднять производительность. Минусом подхода является, то что если 
//эти юниты будут находятся все разом в видимой области это вызовет падение fps (что в реальной игре врят ли случиться).
//Но зато позволяет создать карту большого размера с юнитами действующими в реальном времени.

               bitmapData = shoot.frames[n][_rotPos];
               point.x += Number(shoot.frameXs[n][_rotPos] + _rtX);
               point.y += Number(shoot.frameYs[n][_rotPos] + _rtY);
               _draw = true;
//Этот флаг нужен чтобы перерисовывать юнита только если это необходимо, а не постоянно, даже если его не видно
               }
               else
               {
                  _draw = false;
               }
            }
            else
            {
               _draw = false;
            }
         }
         else
         {
            animationCount++;
         }
        }
    }
}


Класс каждый кадр присваивает новое значение bitmapdata в зависимости от поворота, типа и состояния юнита.
Сменить состояние можно вызвав метод changeUnit(state:int);

Можно заметить, что вместо трансформации при изменении угла поворота юнита, просто изменяется bitmapdata на заранее созданную.
Всего создано 8 направлений (шаг 45 градусов), т.е. юнит ходит не точно с заданным углом, а с округленным до ближайшего кратного 45.
Все эти bitmapdata с поворотами были заранее созданы при старте в классе Rastr, что дало отличную производительность при, как писалось выше, увеличении потребляемой памяти и не точности поворота юнита. Для большей точности можно создать больше положений.
Без создания поворотов (т.е. только 1 положение) потребляемая память была 8-9 мб, при 8 положениях стала 23 мб.
Все юниты используют 1 общий набор из Rastr, поэтому растеризация проходит 1 раз при запуске и заново не создается, поэтому добавление новых юнитов происходит очень быстро и занимает мало памяти. (при 10 000 занимает +10мб)

3. Rastr.as
Код:
package
{
   import flash.display.MovieClip;
   /**
         *  @author Marcus
         */
   public class Rastr
   {
      public static var _enemyR:Array;
//Массив с растеризованным MC
      private var _mc:MovieClip;

      public function Rastr()
        {
           init();
        }
      
      private function init():void
      {
         Enemy();
      }
      
      private function Enemy():void
      {
         _enemyR = [];
         _mc = new Enemy_mc();
         _mc.stop();
         var k:int = _mc.totalFrames;
         for (var j:int = 0; j < k; j++)
         {
            _mc.gotoAndStop(j+1);
            _enemyR[j] = [];
            var k1:int = _mc.type.totalFrames;
            for (var g:int = 0; g <k1 ; g++)
            {
               _mc.type.gotoAndStop(g + 1);
               _enemyR[j][g] = BmpFrames.createBmpFramesFromMC(_mc.type.state);
            }
            
         }
         _mc = null;
         BmpFrames.disposeScratch();
      }
   }
}


Перебирает Enemy_mc и отдает на растеризацию каждое состояние у каждого типа юнитов.
Специфический класс, его необходимо изменить при другой структуре передаваемого MC (в данном случае Enemy_mc).
Здесь в Enemy_mc в каждом кадре клип отражающий тип юнита с instance name = type, в каждом типе находятся кадры с клипом отображающим состояние юнита (стоит, идет, бежит, умирает и т.д.) с instance name = state, в каждом клипе состояния идет покадравая анимация которая собственно и передается на растеризацию.

4. BmpFrames.as
Код:
package
{
        import flash.display.BitmapData;
        import flash.display.MovieClip;
        import flash.geom.Matrix;
        import flash.geom.Point;
        import flash.geom.Rectangle;
          /**
         * @author Alexander Porechnov
         *  update Marcus
         */
        public class BmpFrames
        {
                public var frames :Array;
                public var frameXs :Array;
                public var frameYs :Array;
                public var frameRs :Array;
                public var totalFrames : int;
               
                protected static var scratchBitmapData : BitmapData = null;
                protected static const INDENT_FOR_FILTER : int = 64;
                protected static const INDENT_FOR_FILTER_DOUBLED : int = INDENT_FOR_FILTER * 2;
                protected static var scratchSize : int = 128 + INDENT_FOR_FILTER_DOUBLED;
                protected static const DEST_POINT : Point = new Point(0, 0);
               
                public function BmpFrames() {
                        frames = [];
                        frameXs = [];
                        frameYs = [];
                        frameRs = [];
                        totalFrames = 0;
                }
      private var rotationMatrix:Matrix = new Matrix();
      public static function createBmpFramesFromMC(clipClass : MovieClip) : BmpFrames
      {
                        var clip : MovieClip = clipClass;
                        var res : BmpFrames = new BmpFrames();
                        var totalFrames : int = clip.totalFrames;      
                        var frames :Array = res.frames;
                        var frameXs :Array= res.frameXs;
                        var frameYs :Array = res.frameYs;
                        var frameRs :Array= res.frameRs;
                        var rect : Rectangle;
                        var flooredX : Number;
                        var flooredY : Number;
                        var mtx : Matrix = new Matrix();

                        var _angle:Number;
                        for (var i : int = 1; i <= totalFrames; i++)
         {
                                clip.gotoAndStop(i);
            var it:int = i-1;
            frames[it] = [];
            frameXs[it] = [];
            frameYs[it] = [];
            frameRs[it] = [];
            for (var j:int = 0; j < 8; j++)
            {
            _angle = Amath.toRadians(j * 45);
//Поворачиваем и создаем массивы с уже повернутым изображением

                                rect = clip.getBounds(clip);
                                rect.width = Math.ceil(rect.width) + INDENT_FOR_FILTER_DOUBLED;
                                rect.height = Math.ceil(rect.height) + INDENT_FOR_FILTER_DOUBLED;
                                prepareScratch(rect);
                                flooredX = Math.floor(rect.x) - INDENT_FOR_FILTER;
                                flooredY = Math.floor(rect.y) - INDENT_FOR_FILTER;
                                mtx.rotate(_angle);
                                mtx.tx = -flooredX;
                                mtx.ty = -flooredY;
                                scratchBitmapData.draw(clip, mtx, null, null, null, false);
                                mtx.identity();            
                                var trimBounds : Rectangle = scratchBitmapData.getColorBoundsRect(0xFF000000, 0x00000000, false);
                                trimBounds.x -= 1;
                                trimBounds.y -= 1;
                                trimBounds.width += 2;
                                trimBounds.height += 2;
                               
                                var bmpData : BitmapData = new BitmapData(trimBounds.width, trimBounds.height, true, 0);
                                bmpData.copyPixels(scratchBitmapData, trimBounds, DEST_POINT);
                               
                                flooredX += trimBounds.x;
                                flooredY += trimBounds.y;

                                frames[it][j]=bmpData;
                                frameXs[it][j]=flooredX;
                                frameYs[it][j]=flooredY;
                                frameRs[it][j] = rect;
                 }
                        }
                        res.totalFrames = res.frames.length;
                        return res;
                }

                public static function disposeScratch() : void {
                        scratchBitmapData.dispose();
                        scratchBitmapData = null;
                }
               
                protected static function prepareScratch(rect : Rectangle) : void {
                        var sizeIncreased : Boolean = false;
                        while (rect.width >= scratchSize || rect.height >= scratchSize) {
                                scratchSize *= 2;
                                sizeIncreased = true;
                        }
                        if (scratchBitmapData != null && sizeIncreased) {
                                disposeScratch();
                        }
                        if (scratchBitmapData == null) {
                                scratchBitmapData = new BitmapData(scratchSize, scratchSize, true, 0);
                        } else {
                                scratchBitmapData.fillRect(scratchBitmapData.rect, 0);
                        }
                }
               
        }
}


Можно заметить что везде используются массивы вместо вектора, на практике это оказалось быстрее на пару fps по сравнению с вектором.

5. Amath.as
Содержит в себе помимо методов из статей, дополнительно
Код:
public static function distance(x1:Number, y1:Number, x2:Number, y2:Number):Number
{
      var dx:Number = x2 - x1;
      var dy:Number = y2 - y1;
      return Math.sqrt(dx * dx + dy * dy);
}
public static function getAngleDeg(x1:Number, y1:Number, x2:Number, y2:Number, norm:Boolean = true):Number
      {
         var dx:Number = x2 - x1;
         var dy:Number = y2 - y1;
         var angle:Number = Math.atan2(dy, dx) / Math.PI * 180;
         
         if (norm)
         {
            if (angle < 0)
            {
               angle = 360 + angle;
            }
            else if (angle >= 360)
            {
               angle = angle - 360;
            }
         }
         
         return angle;
      }


Результат работы был показан в самом начале.

UPD1 До кучи добавил задний фон, принцип отрисовки тот же что и у юнитов. В коде подписано UPD1 возле строчек относящихся к фону (класс Main).
  • +19

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

0
О!
Так значительно лучше чем на форуме.
А по поводу бага в исходниках, думаю это не есть хорошо. ИМХО лучше убрать.
0
Перенес пост в колективный блог «ActionScript» и чуть подправил форматирование.
0
полезно, плюсую
0
Можно заметить что везде используются массивы вместо вектора, на практике это оказалось быстрее на пару fps по сравнению с вектором.
Пробовал в Release погонять? Возможно вектора в release будут быстрее.
0
Да, что в релиз, что в дебаг вектора проигрывают на пару фпс. Просто макс фпс в релиз естественно больше, но разница примерно одинаковвая. Просто тут массив 4 порядка, а вектор быстрее только если он простой типа vector.например, а не сложного типа. НА форуме я давал статью на хабре где тестировались сложные вектора и массивы.
0
Профайлер всегда показывает 30 фпс. Странный профайлер.
0
У меня при 10 000 начинает падать. До этого всегда 30 естественно. На более мощных машинах позже может начать снижаться
0
Я специально довёл до 100000, когда уже невооружённым взглядом видно, как всё дико тормозит. Но консоль по прежнему продолжала показывать 30 фпс. Видимо баг какой-то.
0
Специально проверил, текущий и средний фпс показывает. Не знаю из за чего мб.
Делалось под 10+ версию плеера, профайлер стандартный. Можно stats из стать приделать.
0
Спасибо.
А то я никак не успевал закончить статейку :)

Кстати, а зачем использовать самописный код для определения расстояния? Point.distance удобнее.
0
Пожалуйста.
В проекте текущем используется, вот и сюда прилепил )) Сделано было чтоб везде расстояние определялось одинаковой строчкой, а не каждый по разному. Смысл одинаковый.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.