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

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

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

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

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

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

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

Но это я не со зла, просто еще отойти не могу от своих развлечений с оптимизацией :)
0
Мне до твоих опытов, камрад, как до луны на собаке :)
0
задача начального уровня: двигать по экрану пару-тройку тысяч битмапов
Я у себя на полном экране насчитал только 1520, где остальные?
+8
Недавно писал аналогичный брутальный тест (жать стрелки) copyPixels'а )
+2
3500 анимированных спрайтов при 30 фпс — неожиданно, можешь тоже написать немного об этом? :)
0
А исходник можно? Один друг просил на посмотреть :D
+5
А че в исходниках к посту цветные кружочки заменить на цветные спрайтики, уже не в состоянии самостоятельно?))
+2
Да куда мне, я и читаю то с трудом. :)
А если серьезно, то реализация разная у каждого.
Притом я слышал что XProger еще и, прошу прощения, «охуенен», вот и интересно на его код посмотреть.
0
Да, XProger крутой чувак, но судя по фпсу, там такой же как и «с кружочками» брутфорс копипикселс, а в части рендера там все у всех одинаково: залочил, очистил, в цикле скопировал, разлочил — для большого количества маленьких движущихся объектов это самый оптимальный вариант. Если есть большие битмапы то можно еще зоны перекрытия учитывать, и то что перекрывается не рисовать. Для малодинамичной картинки, можно вычислять область перерисовки, и только ее перерисовывать. Ну, и иногда полезно промежуточные результаты тоже отдельно где-то сохранять, например бэкграунд и пр. статичные куски.
0
Ну да, у всех все почти одинаково, но меняешь 3 строчки и плюс 5-10 fps. Хотя да, все почти одинаково.
0
тоже так-то прикольно, у меня 5000 при 30 фпс :)… 6000 при 25… тоже напиши! 8)
+2
Кстати, для битмап без альфы отрисовка в графикс graphics.beginBitmapFill() быстрее работает чем copyPixels
+3
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 =(
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
Та они задолбали, сначала быстрее было то, как я написал, есть несколько сборок перфоманс советов где это было так. Потом с каким-то плеером уже наоборот. Потом опять.
Но я даже говорил тут о неприкащенном обращении к элементу массива.
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
Под молехил пора уже туторы писать :)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.