Векторная графика хороша, пока её не становится много :] Когда её становится много, то процессор начинает не выдерживать нагрузки и игра начинает заметно тормозить. На помощь приходит растеризация векторной графики!
Есть 2 пути растеризовать векторную графику: перевести всю графику в растровые аналоги (например в .png) или же растеризовать вектор программно.
В этой статье мы рассмотрим второй вариант — программную растеризацию.
Мои наработки основаны на 3-х классах Антона Карлова, о них он пишет в своей статье «Из вектора в растр».
В своей версии я изменил и дополнил по большей части классы AnimCache и Actor.
Рассмотрим все классы подробнее.
Классы модуля
Первый класс MaxAnimCache — это хранилище всех растеризованных клипов. Он выполняет процесс кэширования любых указанных клипов и хранит результаты своей работы, пока не будет очищен. Можно создавать отдельные экземпляры класса, для разных блоков графики.
Методы класса MaxAnimCache
cacheAnimClipFromLib(clipClass:Class):MaxAnim |
Выполняет кэширование указанного клипа, заносит его в хранилище и возвращает результирующую анимацию. |
cacheAnimFromDisplayObj(aObj: DisplayObject, aKey: String): MaxAnim |
Выполняет кэширование DisplayObject-а, можно указать ключевое имя для него. Возвращает результирующую анимацию. |
cacheAnim(aKey: String, aAnim: MaxAnim): MaxAnim |
Заносит указанную анимацию в хранилище. |
cacheSpriteFromAddr(aAddr: String, aIsLocalLaunch: Boolean): MaxAnim |
Кеширование изображения прям из указанного адреса. Нужно указать, флешка запущенна локально или нет. |
getAnim(key:String): MaxAnim |
Извлекает копию анимации из кэша по идентификатору. |
addClipToCacheQueue(clipClass:Class):void |
Добавляет имя клипа в очередь для кэширования. clipClass — имя класса клипа, которое необходимо растеризовать. |
addClipsToCacheQueue(clipClasses:Array):void |
Добавляет несколько имен клипов в очередь для кэширования. clipClasses — массив классов. |
makeCache():void |
Запускает процесс кэширования. |
isAnimAvailabe(aKey: String): Boolean |
Есть ли такая анимация в коллекции? |
clear():void |
Очищает хранилище. |
Второй класс MaxAnim — это класс самой анимации, он является хранителем всех кадров конкретного клипа. Все кадры внутри класса хранятся в публичном массиве frames в формате BitmapData. Так же в этом классе находятся методы для преобразования векторного клипа в растр. С помощью этого класса можно локально растеризовать какие-либо отдельные клипы с целью взять растеризованный результат.
Методы класса MaxAnim
cacheFromLib(clipClass:Class):void |
Кэширование клипа из библиотеки. clipName — Имя класса клипа в библиотеки клипов. |
cacheFromClip(clip:MovieClip):void |
Кэширование из экземпляра клипа. clip — клип который необходимо растеризовать. |
cacheFromDisplayObject(aObj: DisplayObject):void |
Кэширование из экземпляра DisplayObject. |
cacheFromAddress(aAddr: String, aIsLocalLaunch: Boolean):void |
Кэширует картинку из указанного адреса. |
cacheFromAnimAtlas(aObj: DisplayObject, aW:Number, aH:Number):void |
Кэширование графики спрайтового атласа с автоматической разбивкой на кадры. |
clone():MaxAnim |
Создает новый экземпляр анимации эдентичный текущей. |
clearFrames():void |
Удаляет все кадры |
И последний класс MaxActor — это класс графическая сущность, которая занимается воспроизведением и рендером растровых анимаций. Это тот самый класс, который идет к вам на замену векторных MovieClip. Этот класс унаследован от Sprite и имеет такие же методы, как у MovieClip для работы с анимацией. То есть фактически, используя этот класс, вы работаете как самым обычным MovieClip, но лишь с той разницей, что вся графика внутри него растровая.
Рассмотрим основные методы.
Методы класса MaxActor
addAnim(anim: MaxAnim, uniqueName: String = null, switchToAnim: Boolean = false):void |
Добавляет новую анимацию с возможным указанием имени и флагом переключиться ли сразу на эту анимацию. |
addAnimFromCache(aCache: MaxAnimCache, key: String, uniqueName: String = null, switchToAnim: Boolean = false):void |
Добавляет новую анимацию из кэша анимаций. |
switchAnim(animName:String, aPlay: Boolean = false):void |
Переключается на заданную анимацию. |
|
|
|
|
|
|
Остальные методы вы можете посмотреть в коде, большая часть прокомментирована.
Вся разница в том, что я значительно дополнил класс Actor-а, а так же выделил AnimCache из синглтона в обычный класс, что позволяет указывать при создании MaxActor определённый AnimCache. Можно, например, создавать отдельные AnimCache для меню и для игры и кешировать в игровой клипы лишь при необходимости, что разгружает оперативную память, а если ещё учитывать именно те клипы, которые будут использоваться на уровне и не кешировать другие, то вообще супер.
Давайте посмотрим на 2 идентичных проекта. Отличия будут только в том, что во втором мы будем применять растеризацию графических объектов.
Я не буду выкладывать оба проекта в статью, т.к. FPS будет зависеть от других флешек на этой же странице. Поэтому я сделал их на отдельных страницах, чтобы вы смогли сами посмотреть и потестировать FPS.
В первом случае используем векторные изображения: Тест без растеризации.
А во втором случае те же самые векторные изображения растеризуем тем способом, который я хочу вам представить: Тест с растеризацией.
Рассмотрим код первой программы. Он состоит всего из 2-х файлов: главного и объекта, который создаётся и летает по сцене.
Основной файл Main.as
package
{
import flash.display.MovieClip;
import flash.display.SimpleButton;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.text.TextField;
import flashdynamix.utils.SWFProfiler;
[Frame(factoryClass="Preloader")]
public class Main extends Sprite
{
private var _objs: Vector.<Ashigaru> = new Vector.<Ashigaru>();
private var _objDummy: Sprite = new Sprite();
private var _guiDummy: Sprite = new Sprite();
// gui
private var _tfStat: TextField;
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);
// entry point
addChild(_objDummy);
addChild(_guiDummy);
// создаём гуи
var gui: MovieClip = new GUI_mc();
_guiDummy.addChild(gui);
_tfStat = gui.getChildByName("tfStat") as TextField;
gui.addEventListener(MouseEvent.CLICK, onGUIClick);
// организцем профайлер
SWFProfiler.init(stage, this);
SWFProfiler.show();
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onGUIClick(e:MouseEvent):void
{
if (!(e.target is SimpleButton))
return;
switch ((e.target as SimpleButton).name)
{
case "btn10":
for (var i:int = 0; i < 10; i++) {
addObject();
}
break;
case "btn50":
for (var j:int = 0; j < 50; j++) {
addObject();
}
break;
case "btnClear":
clear();
break;
}
}
/**
* Добавляем объект со случайной скоростью (линейной и вращения) и в случайное место.
*/
private function addObject():void
{
var obj: Ashigaru = new Ashigaru();
obj.x = 5 + Math.random() * (stage.stageWidth - 5);
obj.y = 5 + Math.random() * (stage.stageHeight - 5);
obj.vel.x = Math.random() * 2 - 1;
obj.vel.y = Math.random() * 2 - 1;
obj.an_v = Math.random() * 3 - 1.5;
_objDummy.addChild(obj);
_objs.push(obj);
updateStat();
}
/**
* Удаляем все объекты
*/
private function clear():void
{
for (var i:int = 0; i < _objs.length; i++) {
_objs[i].free();
}
updateStat();
}
private function updateStat():void
{
_tfStat.text = "objs: " + String(_objs.length);
}
private function onEnterFrame(e:Event):void
{
update(1 / stage.frameRate);
}
private function update(dt: Number):void
{
for (var i:int = _objs.length - 1; i >= 0; i--)
{
if (_objs[i].destroyed) {
_objs.splice(i, 1);
updateStat();
continue;
}
_objs[i].update(dt);
// логика отскакивания объекта от стен
if (_objs[i].x <= 0)
_objs[i].vel.x = -_objs[i].vel.x;
if (_objs[i].x >= stage.stageWidth)
_objs[i].vel.x = -_objs[i].vel.x;
if (_objs[i].y <= 0)
_objs[i].vel.y = -_objs[i].vel.y;
if (_objs[i].y >= stage.stageHeight)
_objs[i].vel.y = -_objs[i].vel.y;
}
}
}
}
Тут в целом всё должно быть понятно, тем более есть некоторые комментарии.
Рассмотрим класс объекта.
Класс объекта Ashigaru
package
{
import flash.display.MovieClip;
import flash.display.Sprite;
import flash.geom.Point;
public class Ashigaru extends Sprite
{
private var _main: MovieClip;
private var _idle1: MovieClip;
private var _idle2: MovieClip;
private var _curr: MovieClip;
private var _timer: Number;
// вектор скорости
private var _vel: Point = new Point();
// угловая скорость в градусах
private var _an_v: Number = 0;
private var _destroyed:Boolean;
/**
* @CONSTRUCTOR
*/
public function Ashigaru()
{
super();
_main = new Ashigaru_mc();
_main.visible = false;
addChild(_main);
_idle1 = new AshigaruIdle1_mc();
_idle1.visible = false;
addChild(_idle1);
_idle2 = new AshigaruIdle2_mc();
_idle2.visible = false;
addChild(_idle2);
setMainAnim();
setTimer();
}
//////////////////////////////////////////////////////////////////////////////////////////
//{ PRIVATE METHODS
private function setTimer():void
{
_timer = Math.random() * 2 + 1;
}
private function setMainAnim():void
{
_curr = null;
_idle1.gotoAndStop(1);
_idle1.visible = false;
_idle2.gotoAndStop(1);
_idle2.visible = false;
_main.gotoAndPlay(1);
_main.visible = true;
}
private function setRandomIdleAnim():void
{
_curr = _idle1;
if (Math.random() > 0.5)
_curr = _idle2;
_idle1.gotoAndStop(1);
_idle1.visible = false;
_idle2.gotoAndStop(1);
_idle2.visible = false;
_main.gotoAndPlay(1);
_main.visible = false;
_curr.gotoAndPlay(1);
_curr.visible = true;
}
//} private
//////////////////////////////////////////////////////////////////////////////////////////
//{ EVENTS METHODS
private function onAnimComplete():void
{
setMainAnim();
setTimer();
}
//} events
//////////////////////////////////////////////////////////////////////////////////////////
//{ PROPERTIES METHODS
public function get vel():Point
{
return _vel;
}
public function set vel(value:Point):void
{
_vel = value;
}
public function get an_v():Number
{
return _an_v;
}
public function set an_v(value:Number):void
{
_an_v = value;
}
public function get destroyed():Boolean
{
return _destroyed;
}
//} properties
//////////////////////////////////////////////////////////////////////////////////////////
//{ PUBLIC METHODS
public function free():void
{
_destroyed = true;
removeChildren();
_main = null;
_idle1 = null;
_idle2 = null;
_curr = null;
_vel = null;
if (parent) parent.removeChild(this);
}
public function update(aDT: Number):void
{
if (_destroyed)
return;
if (_curr) {
if (_curr.currentFrame == _curr.totalFrames)
onAnimComplete();
}
if (_timer > 0) {
_timer -= aDT;
if (_timer <= 0)
setRandomIdleAnim();
}
rotation += _an_v;
x += _vel.x;
y += _vel.y;
}
//} public
}
}
Коротко опишу логику класса.
В констуркторе сразу создаются все клипы анимаций, которые затем проигрываются случайным образом. Клип _curr является ссылкой на клип, который отображается в данный момент. В функции update() происходит вся логика перемещения, вращения и смены анимации по её завершённости или по таймеру базовой статической анимации.
В общем про код первого теста достаточно сказать, что отображение и анимация в нём сделана обычными способами с векторной графикой.
Куда интереснее рассмотреть код второго теста, посмотреть как именно он изменился и подойти к организации растеризации.
Главный класс Main проекта с растеризацией
package
{
import flash.display.MovieClip;
import flash.display.SimpleButton;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.text.TextField;
import flashdynamix.utils.SWFProfiler;
import monax.animation.MaxAnimCache;
[Frame(factoryClass="Preloader")]
public class Main extends Sprite
{
private var _objs: Vector.<Ashigaru> = new Vector.<Ashigaru>();
private var _objDummy: Sprite = new Sprite();
private var _guiDummy: Sprite = new Sprite();
// gui
private var _tfStat: TextField;
// cache
private var _animCache: MaxAnimCache;
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);
// растеризуем спрайтовую графику
_animCache = new MaxAnimCache();
_animCache.addClipsToCacheQueue([
Ashigaru_mc, AshigaruIdle1_mc, AshigaruIdle2_mc
]);
_animCache.onCompleteCallback = onCacheDone;
_animCache.makeCache();
}
private function onCacheDone():void
{
addChild(_objDummy);
addChild(_guiDummy);
// создаём гуи
var gui: MovieClip = new GUI_mc();
_guiDummy.addChild(gui);
_tfStat = gui.getChildByName("tfStat") as TextField;
gui.addEventListener(MouseEvent.CLICK, onGUIClick);
SWFProfiler.init(stage, this);
SWFProfiler.show();
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onGUIClick(e:MouseEvent):void
{
if (!(e.target is SimpleButton))
return;
switch ((e.target as SimpleButton).name)
{
case "btn10":
for (var i:int = 0; i < 10; i++) {
addObject();
}
break;
case "btn50":
for (var j:int = 0; j < 50; j++) {
addObject();
}
break;
case "btnClear":
clear();
break;
}
}
private function addObject():void
{
var obj: Ashigaru = new Ashigaru(_animCache);
obj.x = 5 + Math.random() * (stage.stageWidth - 5);
obj.y = 5 + Math.random() * (stage.stageHeight - 5);
obj.vel.x = Math.random() * 2 - 1;
obj.vel.y = Math.random() * 2 - 1;
obj.an_v = Math.random() * 3 - 1.5;
_objDummy.addChild(obj);
_objs.push(obj);
updateStat();
}
private function clear():void
{
for (var i:int = 0; i < _objs.length; i++)
{
_objs[i].free();
}
updateStat();
}
private function updateStat():void
{
_tfStat.text = "objs: " + String(_objs.length);
}
private function onEnterFrame(e:Event):void
{
update(1 / stage.frameRate);
}
private function update(dt: Number):void
{
for (var i:int = _objs.length - 1; i >= 0; i--)
{
if (_objs[i].destroyed) {
_objs.splice(i, 1);
updateStat();
continue;
}
_objs[i].update(dt);
if (_objs[i].x <= 0)
_objs[i].vel.x = -_objs[i].vel.x;
if (_objs[i].x >= stage.stageWidth)
_objs[i].vel.x = -_objs[i].vel.x;
if (_objs[i].y <= 0)
_objs[i].vel.y = -_objs[i].vel.y;
if (_objs[i].y >= stage.stageHeight)
_objs[i].vel.y = -_objs[i].vel.y;
}
}
}
}
Обратим внимание на изменение инициализации. В самом начале создаётся класс MaxAnimCache, это хранилище всех растеризованных клипов, которые мы укажем атрибутом в массиве для функции .addClipsToCacheQueue(), после этого указываем калбэк-функцию завершения кэширования и запускаем процесс кэширования анимации. Так же можно указать калбэк-функцию которая будет вызываться в процессе кэширования — .onProgressCallback. Пригодится, если у вас много клипов для кэширования и вы хотите как-либо отобразить этот процесс.
Далее по завершению кэширования выполняется калбэк-функция onCacheDone() и мы можем продолжать инициализировать игру.
Таким образом создаётся предкэширование всей необходимой графики и анимации. Например я сделал кэш игровых объектов, а GUI оставил векторным, т.к. его нагрузка не существенна.
Ещё изменился конструктор объекта, теперь в него необходимо передать наш банк кэша, чтобы использовать кэш уже в классе объекта.
Класс объекта Ashigaru с растеризацией
package
{
import flash.display.MovieClip;
import flash.display.Sprite;
import flash.geom.Point;
import monax.animation.MaxActor;
import monax.animation.MaxAnimCache;
public class Ashigaru extends Sprite
{
private var _vis: MaxActor;
private var _timer: Number;
// вектор скорости
private var _vel: Point = new Point();
// угловая скорость в градусах
private var _an_v: Number = 0;
private var _destroyed:Boolean;
public function Ashigaru(aCache: MaxAnimCache)
{
super();
_vis = new MaxActor();
_vis.addAnimFromCache(aCache, "Ashigaru_mc", "main");
_vis.addAnimFromCache(aCache, "AshigaruIdle1_mc", "idle1");
_vis.addAnimFromCache(aCache, "AshigaruIdle2_mc", "idle2");
addChild(_vis);
setMainAnim();
setTimer();
}
private function setTimer():void
{
_timer = Math.random() * 2 + 1;
}
private function setMainAnim():void
{
_vis.switchAnim("main");
setTimer();
}
private function setRandomIdleAnim():void
{
if (Math.random() > 0.5)
_vis.switchAnim("idle1");
else
_vis.switchAnim("idle2");
_vis.gotoAndPlay(1);
_vis.onCompleteCallback = onAnimComplete;
}
private function onAnimComplete():void
{
_vis.onCompleteCallback = null;
setMainAnim();
setTimer();
}
public function get vel():Point
{
return _vel;
}
public function set vel(value:Point):void
{
_vel = value;
}
public function get an_v():Number
{
return _an_v;
}
public function set an_v(value:Number):void
{
_an_v = value;
}
public function get destroyed():Boolean
{
return _destroyed;
}
public function free():void
{
_destroyed = true;
removeChildren();
_vis.free();
_vis = null;
if (parent) parent.removeChild(this);
}
public function update(aDT: Number):void
{
if (_destroyed)
return;
if (_timer > 0) {
_timer -= aDT;
if (_timer <= 0)
setRandomIdleAnim();
}
rotation += _an_v;
x += _vel.x;
y += _vel.y;
}
}
}
Сам класс Ashigaru будет выполнять роль контейнера, а для отображения кэшированных клипов используется класс MaxActor. Он создаётся сразу в конструкторе и в него добавляются анимации функцией addAnimFromCache(банк анимаций, имя закэшированного клипа, имя анимации). Переключать анимации в MaxActor будем функцией switchAnim(имя анимации), а отслеживать окончание анимации с помощью указания события onCompleteCallback.
Вся логика данного кэширования заключается в 3-х классах: MaxAnimCache, MaxActor, MaxAnim.
Скачать примеры: архив с примерами.
На этом пока всё, но продолжение следует (напишу про рендер в 1 битмап).