Растеризация графики во Flash

Векторная графика хороша, пока её не становится много :] Когда её становится много, то процессор начинает не выдерживать нагрузки и игра начинает заметно тормозить. На помощь приходит растеризация векторной графики!

Есть 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 битмап).