
Растровый рендер в 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
- Platon
Комментарии (67)
Зачем в функции move() делается каждый раз «new point()»? Чтобы чаще дергать GC?
Геттеры/сеттеры не нужны.
Ну и всякое там еще по мелочи.
Метод хороший.
Для тех, кто не в курсе будет полезно.
Еще надо переделать на связный список и по максимуму избавиться от вызовов методов.
Убрать деления и считать дистанс руками, чтобы без корня.
Еще дистанс можно считать один раз, а потом только апдейтить линейно.
Но это я не со зла, просто еще отойти не могу от своих развлечений с оптимизацией :)
А если серьезно, то реализация разная у каждого.
Притом я слышал что XProger еще и, прошу прощения, «охуенен», вот и интересно на его код посмотреть.
на перебор по двухсвязанному списку
Моя реализация односвязного списка работает медленнее for =(
baseObject должен содрежать next и prev типа baseObject
При этом у меня количество объектов, которые можно показать без тормозов снижается в ~20 раз.
Пример
Исходники
Могу написать урок об этом =)
хотя бы:
Другое интерсно, раньше проверял и было выгодно писать
Ибо из Array общее приведение начинает более геморную по времени проверку, а если прямо указать что я уверен что там Circle, то проверки нет. Надо бы глянуть в байткод.
jacksondunstan.com/articles/1368
Но я даже говорил тут о неприкащенном обращении к элементу массива.
А вот MovieClip(mcs[i]) наоборот :)
jacksondunstan.com/articles/1305
Я проверил на 20000 кружочков, было 18-19 fps, но я точно знаю что можно сделать 26-28 fps, а может и больше.
Я щитаю :)
Правда с Flixel давно уже не работал… ммм…
членфпс?