Знаючи, який приріст швидкодії дає растеризація графічного вмісту, я давно придивлявся до різних ігрових двигунів з подібною функціональністю. Втім, така вже в мене натура, що я не можу просто взяти чужий код і почати користуватися ним, не розібравшись, як все влаштовано всередині, і оптимізувавши під власні потреби. Тому перехід від векторної графіки до растру зайняв у мене чимало часу. Але у новій грі, яка зараз перебуває в процесі активної розробки, вже буде використано двигун з можливістю растеризації. І те, що виходить в результаті – мені подобається.
Взагалі-то, оптимізація швидкодії флеш-ігор шляхом трансформації MovieClip у послідовність зображень BitmapData – річ не нова. Найбільш просунуті читачі цього блога, безперечно, неодноразово стикалися з таким підходом, а можливо – навіть використовують його у розробці власних ігор. Для тих же, хто вперше чує про растеризацію анімацій чи хоче краще зрозуміти, як і чому це працює, я дозволю собі заглибитися у тему і розібрати все по поличках.
У статті Растеризація як спосіб підвищення швидкодії ігор: Cache as Bitmap вручну я розповів про растеризацію статичних ігрових об’єктів, таких як фони. Обов’язково ознайомтесь з нею, якщо ви цього ще не зробили. Там пояснюється робота вбудованих методів ActionScript 3 для растеризації векторної графіки, які будуть активно використовуватися при растеризації анімацій.
Основна проблема кешування анімацій
Як я вже згадував, основна проблема кешування анімацій полягає в тому, що при спробі растеризувати MovieClip штатним методом Cache as Bitmap, ми не тільки не виграємо у швидкодії, а навпаки – робимо свою гру ще більш повільною. Справа в тому, що флеш намагається створювати растрове зображення кожен кадр кліпу. І таким чином на плечі віртуальної машини, окрім власне обрахунку вектора, лягає ще й робота по створенню растрових зображень.
Але ж можна вручну закешувати кожен кадр векторної анімації, трансформувавши його у растрове зображення, і отриману бібліотеку картинок зберігати у масиві, щоб потім виводити потрібний кадр у потрібному місці. А щоб однакові графічні об’єкти не “з’їдали” оперативну пам’ять – можна створити своєрідну бібліотеку анімацій і просто посилатися на них при створенні нових екземплярів. В такому разі всі роботи щодо растеризації ми беремо у свої руки і знімаємо з флеша відповідальність за них.
Схоже, що вперше ця техніка була опублікована на блозі TouchMyPixel. Принаймні, саме на цю статтю посилаються розробники, які згодом суттєво оптимізували клас растеризації, Антон Карлов і Олександр Поречнов. У Антона Карлова, до речі, є власний цикл статей про растеризацію, з яким теж варто ознайомитись, якщо вам цікава ця тема
Як працює растеризація векторних анімацій
Суть в тому, щоб при запуску гри трансформувати всі векторні анімації у растр і закешувати їх, а потім при необхідності використовувати вже готову графіку, без витрати процесорних ресурсів на складну обробку.
Особисто для мене основна перевага цього методу (окрім швидкодії, ради якої все і робиться) полягає в тому, що в самому swf-файлі графіка зберігається у векторі – отже по-перше, займає менше місця , а по-друге – можна в будь-який момент внести зміни в анімацію, користуючись зручними інструментами Flash IDE. Припускаю, що для людей, які малюють у фотошопі і одразу створюють растрові атласи анімацій, цей метод видасться громіздким та незручним. Але для нас, адептів традиційних MovieClip та Sprite – він просто незамінний.
До недоліків можна віднести той факт, що кожного разу при завантаженні гри потрібно буде растеризувати всю графіку, що іноді може зайняти чимало часу. Але це актуально тільки для по-справжньому великих проектів, яких серед флешок не так і багато. А якщо візуально оформити процес растеризації, як завантаження флешки – то гравець взагалі нічого не помітить.
Також варто пам’ятати, що після кешування вся растрова графіка буде “висіти” у оперативній пам’яті, тож потрібно з розумом підходити до створення зображень і не кешувати зайвого. Як мінімум – не створювати окремі растрові зображення для однакових графічних об’єктів, про що я вже згадував вище.
Класи для растеризації векторних анімацій
Я запропоную вам найпростішу і найбільш наочну реалізацію кешування векторних анімацій. По суті це проапгрейджений і оптимізований варіант класів від TouchMyPixel зі врахуванням розміру кожного кадру та зміщення за вісями X та Y, про які можна детальніше прочитати у статті Олександра Поречнова.
Моя реалізація – це не готовий двигун, а лише приклад того, як можна швидко і зручно растеризувати векторні анімації. Тож для повноцінного впровадження цього коду у власну гру, вам, можливо, доведеться його розширити. Але саме завдяки такому мінімалізму мені вдалося скоротити весь код растеризації до двох основних класів.
Клас Animation – це і є наша анімація, тобто контейнер для всіх кадрів кліпу, який ми растеризували. Всі кадри зберігаються у типізованому масиві frames у форматі BitmapData. Окрім цього у класі Animation є методи для трансформування векторної анімації в послідовність растрових зображень. І оскільки цей клас покликаний замінити традиційні MovieClip – то в ньому є всі необхідні методи для взаємодії з анімацією, такі як: play(), stop(), gotoAndPlay(), gotoAndStop() тощо. Ось тільки наповнення у класу Animaton растрове, а не векторне, як у MovieClip.
Клас AnimationManager – це бібліотека анімацій, в якій зберігаються всі растеризовані кліпи. В ньому всього два методи, які дозволяють додати анімацію в кеш та отримати необхідну анімацію з бібліотеки за її ідентифікатором для подальшого використання.
Далі наведено повний код класів з коментарями.
Клас Animation.as
package com.jarofed { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.MovieClip; import flash.events.Event; import flash.geom.Matrix; import flash.geom.Rectangle; /** * Клас анімації з растровим наповненням, який буде * використовуватися, як заміна стандартних MovieClips * @author jarofed */ public class Animation extends Bitmap { /** * Масив растеризованих зображень (кадрів) анімації у * форматі BitmapData */ public var frames:Vector.<BitmapData>; //Масив зміщень кадрів анімації по Х public var offsetX:Vector.<Number>; //Масив зміщень кадрів анімації по Y public var offsetY:Vector.<Number>; /** * Позиція по X, яка використовується для розміщення * анімації на глобальній сцені */ public var xPos:Number; /** * Позиція по Y, яка використовується для розміщення * анімації на глобальній сцені */ public var yPos:Number; /** * Поточний кадр анімації (використовується для коректної * роботи стандартних методів MovieClip - play(), stop(), * gotoAndPlay(), gotoAndStop() тощо) */ public var currentFrame:Number; //Визначає, чи програється анімація у даний момент private var _playing:Boolean; public function Animation() { frames = new Vector.<BitmapData>(); offsetX = new Vector.<Number>(); offsetY = new Vector.<Number>(); xPos = 0; yPos = 0; currentFrame = 1; _playing = false; } /** * Метод, який відповідає за растеризацію MovieClip у * послідовність кадрів BitmapData * @param aClip Кліп, який потрібно растеризувати */ public function rasterizeMovieClip(aClip:MovieClip):void { var totalFrames:int = aClip.totalFrames; var rect:Rectangle; var bitmapData:BitmapData; var mtx:Matrix = new Matrix(); var flooredX:int; var flooredY:int; //Перебираємо всі кадри кліпу у циклі і растеризуємо їх for (var i:int = 1; i <= totalFrames; i++) { //Переводимо кліп на потрібний кадр aClip.gotoAndStop(i); //Визначаємо чотирикутник зображення rect = aClip.getBounds(aClip); rect.width = Math.ceil(rect.width); rect.height = Math.ceil(rect.height); flooredX = Math.floor(rect.x); flooredY = Math.floor(rect.y); //Додаємо по пікселю з кожного боку чотирикутника //для правильної роботи згладжування rect.x -= 1; rect.y -= 1; rect.width += 2; rect.height += 2; //Створюємо BitmapData з отриманими даними bitmapData = new BitmapData(rect.width, rect.height, true, 0); //Присвоєння значень матриці tx i ty є аналогічним //методу translate, але працює швидше mtx.tx = -flooredX; mtx.ty = -flooredY; bitmapData.draw(aClip, mtx); //Додаємо отриману BitmapData до масиву зображень frames.push(bitmapData); //Також запам'ятовуємо зміщення кожного кадру //за позицями X та Y offsetX.push(flooredX); offsetY.push(flooredY); } } /** * Метод, який відповідає за позиціонування анімації * на сцені. * Позиціонування через безпосередній доступ до змінних * X та Y неможливе, тому що анімація повинна враховувати * зміщення offsetX та offsetY * @param aX позиція по X, в якій буде розміщена анімація * @param aY позиція по Y, в якій буде розміщена анімація */ public function setPosition(aX:Number = 0, aY:Number = 0):void { xPos = aX; yPos = aY; this.x = xPos + offsetX[currentFrame - 1]; this.y = yPos + offsetY[currentFrame - 1]; } /** * Запускає програвання анімації */ public function play():void { if (!_playing) { _playing = true; addEventListener(Event.ENTER_FRAME, enterFrame); } } /** * Зупиняє програвання анімації */ public function stop():void { if (_playing) { _playing = false; removeEventListener(Event.ENTER_FRAME, enterFrame); } } /** * Переходить на вказаний кадр і зупиняє програвання анімації * @param aFrame Кадр, на який потрібно перейти */ public function gotoAndStop(aFrame:Number):void { currentFrame = (aFrame <= 0) ? 1 : (aFrame > totalFrames) ? totalFrames : aFrame; switchFrame(currentFrame); stop(); } /** * Переходить на вказаний кадр і запускає програвання анімації * @param aFrame Кадр, на який потрібно перейти */ public function gotoAndPlay(aFrame:Number):void { currentFrame = (aFrame <= 0) ? 1 : (aFrame > totalFrames) ? totalFrames : aFrame; switchFrame(currentFrame); play(); } /** * Метод, який дозволяє розпочати програвання анімації * з випадкового кадру */ public function playRandomFrame():void { gotoAndPlay(Math.floor(Math.random() * totalFrames)); } /** * Переходить на вказаний кадр * @param aFrame Кадр, на який потрібно перейти */ protected function switchFrame(aFrame:Number):void { currentFrame = Math.round(aFrame); bitmapData = frames[currentFrame - 1]; this.x = xPos + offsetX[currentFrame - 1]; this.y = yPos + offsetY[currentFrame - 1]; } /** * Подія, яка відбувається кожен кадр, якщо анімація * програється (_playing = true) */ public function enterFrame(e:Event):void { currentFrame++; if (currentFrame > totalFrames) currentFrame = 1; switchFrame(currentFrame); } //Getters and setters /** * Повертає загальну кількість кадрів у анімації */ public function get totalFrames():Number { return frames.length } } }
Клас AnimationManager.as
package com.jarofed { import flash.utils.getDefinitionByName; /** * Бібліотека анімацій, яка містить методи для додавання та * отримання анімацій. * @author jarofed */ public class AnimationManager { //Бібліотека всіх анімацій private var animations:Object = {}; public function AnimationManager() { //конструктор порожній, оскільки вся суть цього класу //полягає у методах addAnimation() та getAnimation() } /** * Додає нову анімацію до бібліотеки * @param id Ідентифікатор, за яким анімація буде доступна * @param identifier Назва кліпу, який потрібно растеризувати * @return Повертає растеризовану анімацію */ public function addAnimation(id:String, identifier:String):Animation { var animation:Animation = new Animation(); animation.rasterizeMovieClip(new (getDefinitionByName(identifier))()); animations[id] = animation; return animation; } /** * Метод, який дозволяє отримати анімацію з бібліотеки за * її ідентифікатором * @param id Ідентифікатор анімації * @return Повертає копію растеризованої анімації * (але зверніть увагу, що набір кадрів нової анімації є * посиланням на кадри старої анімації, таким чином * BitmapData не дублюється) */ public function getAnimation(id:String):Animation { if (!animations[id]) return null; var animation:Animation = new Animation(); animation.frames = animations[id].frames; animation.offsetX = animations[id].offsetX; animation.offsetY = animations[id].offsetY; animation.play(); return animation; } } }
Як бачите, код щедро наповнений коментарями. Тому, сподіваюся, він буде зрозумілим для більшості читачів. Якщо ж запитання все-таки залишилися – не соромтеся задавати їх.
Приклад використання
Давайте тепер розглянемо приклад використання наведених вище класів.
Передусім потрібно створити екземпляр бібліотеки анімацій, в якій будуть зберігатися всі растеризовані кліпи:
private var _animationsCache:AnimationManager; _animationsCache = new AnimationManager();
Додаємо в бібліотеку кліпи, які потрібно растеризувати (для цього у FlashIDE повинні бути заздалегідь підготовані кліпи blueCar_mc, redCar_mc і т. п.):
_animationsCache.addAnimation("blueCar", "blueCar_mc"); _animationsCache.addAnimation("redCar", "redCar_mc"); _animationsCache.addAnimation("greenCar", "greenCar_mc"); _animationsCache.addAnimation("purpleCar", "purpleCar_mc"); _animationsCache.addAnimation("yellowCar", "yellowCar_mc");
Тепер можна створити анімацію, наповнення якої буде растровим:
var clip:Animation = _animationsCache.getAnimation("blueCar"); /*Позиціонуємо анімацію на сцені з допомогою функції setPosition(), яка враховує зміщення кадрів за вісями X та Y Спроба позиціонувати анімацію безпосереднім присвоєнням значень параметрам X та Y може призвести до некоректного позиціонування, оскільки зміщення кадрів не будуть враховуватися*/ clip.setPosition(100, 100); //Запускаємо програвання анімації clip.play(); //І додаємо анімацію на сцену звичним способом addChild(clip);
Тест швидкодії растеризованих анімацій
Це все, звісно, чудово, але де ж відповідь на запитання, наскільки виграшними виявляються растеризовані анімації порівняно із традиційними векторними MovieClip-ами? Чи варта гра свічок?
Щоб дати відповідь на це запитання, я зробив тестову флешку, де можна перемикатися між векторними та растровими анімаціями, та додавати на сцену 50, 100 або 200 кліпів.
На моєму досить потужному комп’ютері (i7, 8Гб) цей тест видає стабільні 60 FPS при 50 кліпах (як векторних так і растрових). Але вже при 100 кліпах показники вектора падають до 30 FPS, а при 200 кліпах – до 12-15 FPS, тоді як растрові анімації видають 60 FPS і при 100 і при 200 кліпах.
Буду вдячний, якщо ви вирішите поділитися власними результатами тестування цієї флешки у коментарях.
Сподіваюся, стаття про растеризацію анімацій була для вас корисною і допоможе суттєво підвищити швидкодію ваших власних ігор.
Грудень 12, 2014 о 15:45
Привіт Ярославе!
Хочу поцікавитися чим зараз займаєтеся? Флеш-іграми чи вже переходите на щось інше?