Adobe AIR + Starling + растеризування векторної графіки



Минув деякий час з тих пір, як я почав робити ігри для iOS і Android на Adobe AIR. Сьогодні хочу поділитися способом створення ігор під різні дозволи екранів — цей підхід я успішно застосовую в своїх проектах.

Як відомо, є кілька способів підготовки ігрової графіки для різних дозволів екранів:

Використовувати кілька паків з графікою
Самий популярний підхід для формування ігрової графіки. Дозволяє для кожного дозволу за своїм опрацювати графіком. Приміром, на маленьких екранах опрацювання і деталізація різних елементів зведена на мінімум, а деякі деталі і зовсім опущені. Але, такий набір досить багато важить, і не на всіх дозволах, після скейла текстур, виглядає добре. Після появи ретина дисплеїв зі звірячими дозволами екранів, розробникам довелося до вже наявних трьох пакам текстур додавати ще один.

Малювати піксель-арт
Дозволяє використовувати в грі пак атласів з маленькими текстурами тільки для одного дозволу екрану, який можна поскейлить на будь-який розмір. Квадрат він і є квадрат. Хоч на sd, хоч на xxxhd піксель-арт буде виглядати як піксель-арт. Плюс піксель-арт порівняно неважко малювати.

Векторна графіка
Дозволяє використовувати в грі один пак атласів для поточного дозволу екрану, практично нічого не важить, тягнеться на будь дозвіл без втрати якості, дуже добре виглядає і досить просто малюється. Саме цього мені й хотілося.

Але, не все так просто. Справа в тому, що вся векторна графіка обробляється на CPU, а значить гра з такою графікою на вашому телефоні приречена на гальма, та й сильно не разбежишься (об'єктів на екрані виходить мало та й ті повинні бути простими, без зайвої деталізації). Хоча перша версія моєї гри City 2048 була саме такою, і на подив працювала цілком собі пристойно, видавала 25-40 fps. Запускаючи тестову версію гри, я очікував що телефон прямо у мене в руках зависне і розплавиться від цього, але немає. Так само можу сказати, що ще одна моя гра Dots Tails до цих пір працює з використанням векторної графіки, є на те свої причини.

Щоб збільшити продуктивність, необхідно промалювати всю ігрову графіку на GPU, для цього будемо використовувати Stage3D і Starling. Виходить що з окремих векторних елементів, потрібно скласти растрові спрайтшиты відразу потрібного розміру в процесі виконання програми. Про те як це реалізувати ми і поговоримо.

Перед вживанням, векторну графіку необхідно розтягнути до потрібного розміру, розкласти на атлас і запекти. Для цих цілей я використовував злегка змінений клас від Emiliano Angelini «Dynamic Texture Atlas and Bitmap Font Generator», залишивши від нього тільки створення простого атласу текстур без анімацій.

Принцип роботи наступний:

1. Малюємо арт для гри в Adobe Flash Pro (або будь-якому іншому векторному редакторі і переносимо в Flash Pro)



2. Створюємо спрайт який буде містити в собі елементи графіки, робимо його доступним для AS. Саме з нього ми і будемо робити спрайтшит.



3. Запихаємо в цей спрайт потрібну нам графіком. Я намагався розмістити елементи так, щоб вони влазили в розмір 512х512. Це необхідно, так як при скейле розмір атласу не повинен бути більше 4к. Для дизайн макета я завжди використовую розмір 600х800, так намальовані і скомпоновані елементи добре виглядають і не вилазять за розмір 2к. Так-же елементи графіки варто компонувати за тематикою, наприклад у мене в іграх шар з GUI лежить над ігровою графікою, з цього я роблю два окремих атласу з GUI і з ігровими елементами + якщо в грі кілька візуально різних рівнів то краще розкидати ці елементи з різних атласами. Це допоможе скоротити кількість дроуколов.



4. Кожному елементу в атласі не забуваємо присвоїти ім'я.



5. Експортуємо .swc з ресурсами і підключаємо його до проекту.



6. Приступаємо до програмної частини. Для початку обчислюємо скеил, на який будемо тягнути ресурси:

// Розмір екрану нашого пристрої, наприклад iPad2
var _stageWidth:Number = 768;
var _stageHeight:Number = 1024;

// Розмір дизайн-макету
var defaultScreenWidth:Number = 600;
var defaultScreenHeight:Number = 800;

// Обчислюємо скейлы і беремо потрібний, в залежності від орієнтації екрану. В моєму випадку портретна
_scaleX = _stageWidth / defaultScreenWidth;
_scaleY = _stageHeight / defaultScreenHeight;
_minScale = Math.min(_scaleX, _scaleY);

7. Додаємо в проект клас TextureManager.as і прописуємо в ньому імена атласів з SWC

Вміст класу TextureManager
package com.Extension
{
import avmplus.getQualifiedClassName;

import com.Event.GameActiveEvent;
import com.Module.EventBus;
import com.greensock.TweenNano;

import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.display.Sprite;
import flash.display.StageQuality;
import flash.geom.Matrix;
import flash.geom.Rectangle;

import starling.display.Image;
import starling.display.Sprite;
import starling.textures.Texture;
import starling.textures.TextureAtlas;

public class TextureManager
{
// зберігає в собі координати на які потрібно зрушити спрайт, щоб зберегти PivotPoint об'єкта з SWC
private static var textureAdditionalData:Object = {};
// контейнер з готовими атласами
private static var textureAtlases:Vector.<TextureAtlas> = new <TextureAtlas>[];
// масив атласів які потрібно розпарсити 
// !!! (тут потрібно прописати імена атласів з SWC і скейл)
private static var toParse:Array = [
[guiAtlas, ScaleManager.minScale],
[gameAtlas, ScaleManager.minScale]
];

// повертає старлинговый спрайт з потрібної нам текстурою з атласу
public static function getSprite(textureName:String, smooth:String = "ні"):starling.display.Sprite
{
if (textureAdditionalData.hasOwnProperty(textureName))
{
var addition:Object = textureAdditionalData[textureName];
var image:Image = new Image(findTexture(textureName));
image.x = -addition["x"];
image.y = -addition["y"];
image.textureSmoothing = smooth;

var result:starling.display.Sprite = new starling.display.Sprite();
result.addChild(image);

return result;
}

throw new Error("[!!!] Texture '" + textureName + "' not found.");
}

// повертає текстуру з атласу
public static function getTexture(textureName:String):Texture
{
return findTexture(textureName);
}

// метот, який потрібно викликати при старті гри. Якщо атласів багато, то це може зайняти деякий час.
public static function createAtlases():void
{
if (!textureAtlases.length)
{
nextParseStep();
return;
}
throw new Error("[!!!] Texture atlases already.");
}

// почергово створюємо атласи
private static function nextParseStep():void
{
if (toParse.length)
{
var nextStep:Array = toParse.pop();
TweenNano.delayedCall(.15, TextureManager.createAtlas, nextStep);
}
else
{
// якщо всі, то відправляємо подія про старт гри.
EventBus.dispatcher.dispatchEvent(new GameActiveEvent(GameActiveEvent.GAME_START, true));
}
}

// пошук потрібної текстури в атласах
private static function findTexture(textureName:String):Texture
{
var result:Texture;
for each (var atlas:TextureAtlas in textureAtlases)
{
result = atlas.getTexture(textureName);
if (result)
{
return result;
}
}

throw new Error("[!!!] Texture '" + textureName + "' not found.");
}

// клас який парсити спрайт з SWC і створює атлас
private static function createAtlas(swcPack:Class, scaleFactor:Number):void
{
var pack:flash.display.Sprite = (new swcPack()) as flash.display.Sprite;
var itemsHolder:Array = [];
var canvas:flash.display.Sprite = new flash.display.Sprite();

var children:uint = pack.numChildren;
for (var i:uint = 0; i < children; i++)
{
var selected:DisplayObject = pack.getChildAt(i);
var realX:Number = selected.x;
var realY:Number = selected.y;
selected.scaleX *= scaleFactor;
selected.scaleY *= scaleFactor;

var bounds:Rectangle = selected.getBounds(selected.parent);
bounds.x = Math.floor(bounds.x - 1);
bounds.y = Math.floor(bounds.y - 1);
bounds.height = Math.round(bounds.height + 2);
bounds.width = Math.round(bounds.width + 2);
var drawRect:Rectangle = new Rectangle(0, 0, bounds.width, bounds.height);

var bData:BitmapData = new BitmapData(bounds.width, bounds.height, true, 0);
var mat:Matrix = selected.transform.matrix;
mat.translate(-bounds.x, -bounds.y);
bData.drawWithQuality(selected, mat, null, null, drawRect, false, StageQuality.BEST);

var pivotX:int = Math.round(realX - bounds.x);
var pivotY:int = Math.round(realY - bounds.y);

textureAdditionalData[selected.name] = {x:pivotX, y:pivotY};
var item:flash.display.Sprite = new flash.display.Sprite();
item.name = selected.name;
item.addChild(new Bitmap(bData, "авто", 'false'));
itemsHolder.push(item);
canvas.addChild(item);
}

layoutChildren();

var canvasData:BitmapData = new BitmapData(canvas.width, canvas.height, true, 0x000000);
canvasData.draw(canvas);

var xml:XML = new XML(<TextureAtlas></TextureAtlas>);
xml.@imagePath = (getQualifiedClassName(swcPack) + ".png");

var itemsLen:int = itemsHolder.length;
for (var k:uint = 0; k < itemsLen; k++)
{
var itm:flash.display.Sprite = itemsHolder[k];

var subText:XML = new XML(<SubTexture />);
subText.@name = itm.name;
subText.@x = itm.x;
subText.@y = itm.y;
subText.@width = itm.width;
subText.@height = itm.height;
xml.appendChild(subText);
}
var texture:Texture = Texture.fromBitmapData(canvasData);
var atlas:TextureAtlas = new TextureAtlas(texture, xml);
textureAtlases.push(atlas);

function layoutChildren():void
{
var xPos:Number = 0;
var yPos:Number = 0;
var maxY:Number = 0;
var maxW:uint = 512 * ScaleManager.atlasSize;
var len:int = itemsHolder.length;

var itm:flash.display.Sprite;

for (var i:uint = 0; i < len; i++)
{
itm = itemsHolder[i];
if ((xPos + itm.width) > maxW)
{
xPos = 0;
yPos += maxY;
maxY = 0;
}
if (itm.height + 1 > maxY)
{
maxY = itm.height + 1;
}
itm.x = xPos;
itm.y = yPos;
xPos += itm.width + 1;
}
}

nextParseStep();
}

public function TextureManager()
{
throw new Error("[!!!] Used private class.");
}
}
}

Трохи докладніше про те, що відбувається в методі createAtlas:

» 7.1. Кожен елемента в атласі з SWC скейлим, залишаємо координати для PivotPoint, промальовуємо в Bitmap і додаємо в контейнер canvas.

» 7.2. Розставляємо елементи в контейнері canvas один за одним, так щоб влізли в потрібний розмір атласу

» 7.3. Контейнер canvas малюємо в BitmapData і генера .XML

» 7.4. З отриманих BitmapData і .XML створюємо старлинговый TextureAtlas

» 7.5. Отриманий атлас додаємо в контейнер textureAtlases

8. При старті гри створюємо атласи для старлінга

TextureManager.createAtlases();

9. Додаємо потрібний нам спрайт на сцену

var tileView:starling.display.Sprite = TextureManager.getSprite("rotateView");
this.addChild(tileView);

Що отримуємо в результаті? Красиву графіку, яка практично нічого не важить, тягнеться на як завгодно великий розмір екрану без втрати якості. При цьому гра працює на стабільних 60fps. Ну і особисто для мене ще один плюс в тому що у векторі досить просто малювати, хоч я і не художник, але дещо у векторі можу.



Растеризацію векторної графіки я використовую в своїх іграх City 2048, Quadtris і Placid Place. Які можна знайти в Apple App Store і Google Play, якщо цікаво подивитися такий підхід в дії. На жаль прямі посилання на додатки залишати не можна.

Ось, власне, і все. Дякую за увагу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.