Прошло некоторое время с тех пор, как я начал делать игры для 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
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 = "none"):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, "auto", 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, если интересно посмотреть такой подход в действии. К сожалению прямые ссылки на приложения оставлять нельзя.
Вот, собственно, и всё. Спасибо за внимание.
Автор: Arman11