Існують сотні, якщо не тисячі способів оптимізувати код та підвищити швидкодію програм, написаних на мові ActionScript 3. Ви можете знайти навіть офіційні рекомендації щодо підвищення продуктивності Flash-платформи. Втім, якщо говорити про Flash-ігри, то 99% проблем продуктивності будуть пов’язані зі специфікою роботи флеш, а саме – векторною графікою.
Інакше кажучи, ви можете скільки завгодно оптимізувати код, використовуючи ефективні математичні моделі, паттерни програмування, правильні масиви та цикли, але, якщо ви не знайдете спосіб оптимізувати графіку – ваші зусилля будуть марними. І навпаки, якщо ви вирішите проблему із графікою, то раз і назавжди забудете про проблеми зі швидкодією, навіть якщо ваш код залишатиметься далеким від ідеалу.
Будь-яка якісна сучасна гра (навіть якщо йдеться про невеликі портальні флеш-ігри) передбачає одночасне перебування на екрані та обробку кількох десятків чи навіть сотень візуальних об’єктів. Динамічний фон, тайлова графіка, анімовані спрайти – все це вимагає процесорного часу. А якщо йдеться про векторну графіку, яка використовується у флеш за замовчуванням – то кількість ресурсів на обробку зростає в рази.
Якщо ви читали одну з більш ранніх статей на цьому блозі про різницю між векторною та растровою графікою, то, безперечно, знаєте, що векторна графіка – сама по собі досить складна. Її основні переваги, як от можливість автоматичної анімації (Motion та Shape Tween) шляхом зміни параметрів кривих, перетворюються на недоліки, коли йдеться про швидкодію. Адже всі криві вираховуються складними математичними формулами. А якщо при цьому використовується анімація – то на віртуальну машину лягає ще й завдання пошуку проміжних положень точок та прямих між ключовими кадрами. І такі складні розрахунки повинні відбуватися 30, а то й усі 60 разів на секунду (в залежності від обраного нами framerate).
Проблем із продуктивністю можна до певного часу уникати, обмежуючи кількість ігрових об’єктів та штучно спрощуючи графіку. Але чи готові ви йти на такий компроміс із власною творчою натурою? Всі мої попередні ігри, по-суті, будувалися на такому компромісі. І це видно! Наприклад, у Symbiosis проявляються проблеми швидкодії, якщо запускати його на слабших комп’ютерах. А у грі Frogged саме через низьку продуктивність я був змушений повернутися до своїх стандартних 30 FPS, хоча мріяв поекспериментувати із фреймрейтом 60.
Думаю, що рано чи пізно практично будь-який програміст, який активно використовує векторну графіку (а серед флеш-розробників таких більшість), стикається з проблемою швидкодії. У грі це проявляється банальним “просіданням” FPS чи так званими “гальмами” графіки, і суттєво погіршує загальне враження від гри, перетворюючи ігровий процес на тортури, а іноді – повністю унеможливлюючи його.
На щастя, рішення існує, і його вже давно і успішно використовують бувалі флеш-розробники. Полягає воно в тому, щоб відмовитися від векторної графіки і перевести візуальні об’єкти в растр. Так уже склалося, що з растровою графікою комп’ютеру набагато простіше проводити розрахунки і виконувати рендер. Адже растрове зображення – це всього лише масив пікселів, які зберігають найпростішу інформацію.
Cache as Bitmap
Перший і найпростіший крок до подібної оптимізації – це виставлення властивості cacheAsBitmap статичним (неанімованим) графічним об’єктам вашої гри. Ви можете зробити це як у Flash IDE, поставивши галочку у відповідному полі, так і безпосередньо через код ActionScript. При цьому із векторного зображення робиться віртуальний “знімок” у вигляді растрової картинки, і криві вектора більше не обраховуються кожен кадр.
Cache as Bitmap особливо добре працює зі статичними фонами. Наприклад, у вже згадуваній мною грі Frogged одна стрічка коду:
background.cacheAsBitmap = true;
підвищила швидкодію майже вдвічі.
Втім, cacheAsBitmap має кілька недоліків, які суттєво обмежують використання цієї корисної властивості.
По-перше, такий спосіб растеризації абсолютно не підходить для роботи із анімованими зображеннями. При спробі ввімкнути cacheAsBitmap для MovieClip ми знизимо швидкодію, замість того, щоб підвищити її. Адже бітмапа буде по-новому створюватися кожен кадр, що додасть зайвої роботи віртуальній машині.
А по-друге, навіть при кешуванні статичних зображень, “гальма” частково залишаються. Це особливо помітно при роботі з кліпами великого розміру. Складається враження, що флеш час від часу “оновлює” дані та перераховує зображення.
Ручна растеризація
Що ж, якщо гора не йде до Магомета…
Єдиним очевидним рішенням залишається ручна растеризація. Адже використовуючи можливості ActionScript, можна зробити растрове зрображення із будь-якого масиву кліпів. Для цього треба лише “скласти” кліпи в потрібному порядку, а потім “сфотографувати” їх у бітмап, який буде використовуватися як фонове зображення у грі.
На допомогу нам йдуть два вбудовані класи Bitmap та BitmapData.
Клас Bitmap реалізує методи DisplayObject і за своєю суттю дуже схожий на Sprite, з тією лише відмінністю, що все його наповнення – растрове, а не векторне.
В свою чергу клас BitmapData – це і є наповнення класу Bitmap. Він несе інформацію про зображення та дозволяє працювати з даними (пікселями) зображення. Окрім того, методи класу BitmapData підтримують візуальні ефекти, не доступні для нерастрових екранних об’єктів.
Відповідно, щоб створити растрове зображення у ActionScript, нам доведеться спершу створити об’єкт BitmapData, скопіювати туди наше векторне зображення (наприклад фон, який використовується у грі) і зрештою, створити екранний об’єкт Bitmap, якому присвоїти створену нами BitmapData.
Конструктор BitmapData має такий узагальнений вигляд:
new BitmapData(width:int, height:int, transparent:Boolean = true, fillColor:uint = 0xFFFFFFFF)
він дає змогу створити новий об’єкт BitmapData із заданими шириною та висотою. Відповідно, перш, ніж створювати об’єкт BitmapData, нам потрібно дізнатися розміри зображення, яке ми плануємо растеризувати. Для цього використаємо метод getBounds(), який властивий усім екранним об’єктам.
Ось як виглядатиме код створення BitmapData:
/*векторне зображення фону, яке ми будемо растеризувати (передбачається, що у бібліотеці Flash IDE уже підготоване зображення фону background)*/ var vectorBackground:Sprite = new background(); /*допоміжний об'єкт Rectangle для визначення розмірів зображення та правильного позиціонування*/ var rect:Rectangle; var bitmapData:BitmapData; /*визначаємо розміри та координати векторного зображення за допомогою методу getBounds(). Ці дані зберігаються у об'єкті Rectangle.*/ rect = vectorBackground.getBounds(vectorBackground); /*округлюємо ширину та висоту отриманого чотирикутника (розміри растрового зображення, на відміну від векторного, можуть бути тільки цілими числами)*/ rect.width = Math.ceil(rect.width); rect.height = Math.ceil(rect.height); //створюємо BitmapData потрібного розміру bitmapData = new BitmapData(rect.width, rect.height, true, 0);
Наступним кроком буде копіювання зображення vectorBackground у щойно створений об’єкт BitmapData. Для цього використовується вбудований метод BitmapData draw(). Ось його узагальнений вигляд:
draw(source:IBitmapDrawable, matrix:Matrix = null, colorTransform:flash.geom:ColorTransform = null, blendMode:String = null, clipRect:Rectangle = null, smoothing:Boolean = false)
ми не будемо розбирати всі параметри, які передаються у метод, оскільки про них можна прочитати у офіційній документації Adobe, а зупинимося тільки на перших двох.
Параметр source – єдиний обов’язковий параметр – це і є посилання на зображення, яке ми хочемо растеризувати. У нашому випадку – vectorBackground. Іноді ним можна обмежитися (якщо точка реєстрації нашого зображення знаходиться чітко у лівому верхньому куті – точка 0, 0). Якщо ж зображення зміщено щодо точки 0, 0 – тоді нам в нагоді стане другий параметр – Matrix.
Об’єкт Matrix містить інформацію щодо масштабування, повороту та зміщення координат растрового зображення. Про використання матриці щодо об’єктів BitmapData, можна написати окрему статтю, але в даний момент нам буде потрібен тільки один метод – translate(), який дозволяє змістити зображення по вісях координат x та y.
Ось як виглядатиме код створення матриці та копіювання векторного зображення у растровий контейнер BitmapData:
//створюємо матрицю var mtx:Matrix = new Matrix(); /*виконуємо метод translate (відповідає за зміщення зображення за вісями x та y) з відомими координатами rect.x та rect.y*/ mtx.translate(-rect.x, -rect.y); /*копіюємо вміст векторного зображення vectorBackground у bitmapData*/ bitmapData.draw(vectorBackground, mtx);
Останнє, що нам залишилося – це присвоїти дані отриманої bitmapData екранному об’єктові Bitmap, який уже можна використовувати безпосередньо у грі. Ось останні стрічки коду ручної растеризації:
//створюємо об'єкт Bitmap – контейнер для растрового зображення var bitmapBackground:Bitmap; //передаємо йому дані із bitmapData bitmapBackground = new Bitmap(bitmapData); /*тепер можна додати векторне зображення на екран знайомим способом addChild()*/ addChild(bitmapBackground);
Єдине, про що ще варто пам’ятати – то це про зміщення вашого зображення за осями x та y. Якщо точка реєстрації знаходиться не у правому верхньому куті зображення, то потрібно компенсувати зміщення присвоївши зображенню координати, які зберігаються у Rectangle:
bitmapBackground.x = rect.x; bitmapBackground.y = rect.y;
Насамкінець – фінальний код ручної растеризації:
var vectorBackground:Sprite = new background(); var bitmapBackground:Bitmap; var rect:Rectangle; var bitmapData:BitmapData; var mtx:Matrix; rect = vectorBackground.getBounds(vectorBackground); rect.width = Math.ceil(rect.width); rect.height = Math.ceil(rect.height); bitmapData = new BitmapData(rect.width, rect.height, true, 0); mtx = new Matrix(); mtx.translate(-rect.x, -rect.y); bitmapData.draw(vectorBackground, mtx); bitmapBackground = new Bitmap(bitmapData); bitmapBackground.x = rect.x; bitmapBackground.y = rect.y; addChild(bitmapBackground);
Нижче наведено результат роботи цього коду. Ви можете переключатися між векторним та растровим зображенням фону.
На перший погляд може видатися, що це занадто складно і багато коду, як для заміни вбудованого cacheAsBitmap. Але повірте, швидкодія, яка проявляється в результаті використання цього методу, варта зусиль. Тим більше, що весь процес растеризації можна делегувати власноруч створеному утилітному класові.
Але цінність цього методу була б сумнівною, якби сфера його застосування обмежувалася фоновими зображеннями для ігор. Адже левову частку швидкодії у флеш з’їдають векторні анімації. Втім, як виявляється, завдяки ручній растеризації, їх також можна перевести у растр і зберігати у вигляді масиву зображень (кадрів). Таким чином можна досягнути тотальної растеризації графічного вмісту флеш-гри, і відповідно – дуже значного зростання швидкодії.
Про растеризацію анімацій я постараюся написати найближчим часом і опублікую статтю, де окрім всього іншого будуть тести швидкодії. Сподіваюся, ця тема є для вас настільки ж актуальною, як і для мене.
Грудень 3, 2014 о 14:32
Гарна стаття.
Ярославе, а як оптимізовувати ситуацію коли використовується чистий растр в ігровій анімації (png – сиквенції)? Мається на увазі те, що графіка трохи “дрижить” та якщо придивитись видно пікселі.