Векторная графика хороша, пока её не становится много :] Когда её становится много, то процессор начинает не выдерживать нагрузки и игра начинает заметно тормозить. На помощь приходит растеризация векторной графики!
Есть 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
|
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
|
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 проекта с растеризацией
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
|
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 с растеризацией
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
|
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 битмап).