Позитивная новость для геймдива:
В полку сервисов всеми любимой Adobe прибыло. Точнее, прибудет.
Adobe анонсировала открытие Flash Platform Game Technology Center - ресурса для разработчиков игр на Flash Platform.
Для начала несколько слов прелюдии.
Примерно полтора года назад я накодил простенький передзагрузчик (аналог), стартующий до инициализации главного класса приложения, и периодически его дополнял и частично переписывал. Таким образом один класс вырос в целую систему, поддерживающую модульную архитектуру, полиморфизм, и предзагрузку необходимых ассетов.
Эту разработку я частенько сам использую в больших многомодульных проектах, где есть одна или несколько библиотек (swc) и огромное количество модулей, эти библиотеки использующие; Предзагрузчик у всех модулей - один, следовательно вынесен в библиотеку, а не клонируется по всей команде разработчиков от проекта к проекту. При этом (частая ситуация) хорошо было бы, что бы все модули приложения могли работать как самостоятельно, так и внутри любого другого модуля, - т.е. полиморфизм. Вот эту ситуацию и возьму за основу.
По-сути, мое решение представляет из себя неслабое универсализированное расширение уже давно описанных метод: тут (по-русски), тут и тут - настоятельно рекомендую ознакомиться перед продолжением.
мы получаем вот такой результат:
[Схема с нашим классом]
Но все было бы чудесно, если б не одна проблема: Наш предзагрузчик понятия не имеет об имени главного класса приложения, который ему надо инстанциировать, а заниматься жестким хардкодом в большом проекте - непозволительная роскошь. Первая мысль была такова:
В каждом проекте-модуле использовать в качестве предзагрузчика субкласс того основного, что находится в библиотеке и остается неизменным. В этом субклассе мы просто переопределяем значение protected переменной MAINCLASS_NAME, ранее заведенной в основном классе и далее используем ее при инстанциировании нужного нам главного класса приложения. Но это мне вскоре надоело (неудобно) и было придумано более интересное решение:
Предзагрузчик наш остается в библиотеке, не имеет никаких субклассов (можно даже сделать его final). После загрузки всей свифки он сам найдет нужное ему имя класса в метаданных, а в голове главного класса мы просто впишем свой кастомный метатэг:
[App(className='ИмяГлавногоКласса')]
Что бы компиллер "увидел" наш метатэг, в параметры компилятора (в свойствах проекта) впишем: -load-config cfg.xml
(про параметры компилятора подробности тут)
А в конфиге мы явно укажем, что на наш метатэг стоит обратить внимание:
package{importflash.display.FrameLabel;
importflash.display.MovieClip;
importflash.events.Event;
importflash.utils.*;
import ru.kozlovskij.core.Application;
import ru.kozlovskij.preload.Preloader;
/**
* SWF metadata
*/[SWF(backgroundColor='0xFFCC00', width='500', height='500')]/**
* The frameworks must be initialized by SystemManager.
* This factoryClass will be automatically subclassed by any
* MXML applications that don't explicitly specify a different
* factoryClass.
*
* NOTE: Minimize the non-Flash classes you import here.
* Any dependencies of SystemManager have to load in frame 1,
* before the preloader, or anything else, can be displayed.
*/[Frame(factoryClass='ru.kozlovskij.preload.Preloader')]//[Frame(factoryClass='exampleClasses.MyPreloader')]/**
* Custom metadata tags
*/[App(type='module', className='Main')][copyrights(firstName='Aleksandr', lastName='Kozlovskij', url='http://Kozlovskij.ru', blog='http://FlashImp.ru')]publicclass Main extends Application
{publicfunction Main(){super();
addEventListener(Preloader.PRELOADER_DONE, preloaderDoneHandler);
}privatefunction preloaderDoneHandler(e:Event):void{//initialize...for(var i:int; i < (parentasMovieClip).currentLabels.length; ++i)trace('FrameLabel:', '[', FrameLabel((parentasMovieClip).currentLabels[i]).name,
',', FrameLabel((parentasMovieClip).currentLabels[i]).frame, ']');
(parentasMovieClip).nextFrame();
parent.addChild(new(flash.utils.getDefinitionByName('exampleClasses.CustomFrame')asClass)());
}}}
Метод preloaderDoneHandler вызывается, когда от предзагрузчика мы получаем событие об окончании загрузки всего необходимого (+ассеты?). В preloaderDoneHandler мы, удовлетворения ради, выводим в трэйс все, имеющиеся в нашем распоряжении (в родителе-предзагрузчике) фрэймы. В последних двух строках мы переходим в третий кадр и инстанциируем класс, который был загружен последним - об этом чуток ниже.
Во многих моих реализациях данного подхода я либо создавал в Main классе метод, возвращающий массив урлов к тому, что еще необходимо загрузить, что было несколько криво и не гибко, либо записывал эту информацию в метаданные, что позволяло предзагрузчику все необходимое скачать еще до инициализации главного класса, а он в свою очередь рождался "на всем готовом". Нюансы реализацию такой инициализации приложения я покажу позже, а сейчас - простой пример того самого предзагрузчика:
Preloader.as - собственно, сам прелоудер:
package ru.kozlovskij.preload
{importflash.display.DisplayObject;
importflash.display.MovieClip;
importflash.display.StageAlign;
importflash.display.StageScaleMode;
importflash.events.Event;
importflash.system.ApplicationDomain;
importflash.utils.ByteArray;
import ru.etcs.utils.ClassExplorer;
[Event(name="preloaderDone", type="ru.kozlovskij.preload.Preloader")]publicclass Preloader extends MovieClip{public static const PRELOADER_DONE:String = 'preloaderDone';
protectedvar MAINCLASS_NAME:String;
protectedvar preloaderGraphics:IPreloaderGraphics;
privatevar Main:Class;
privatevar main:DisplayObject;
protectedvar _standalone:Boolean = true;
protectedfunctionget standalone():Boolean{return _standalone;}publicfunction Preloader(preloaderGraphics:IPreloaderGraphics = null){stop();
this.preloaderGraphics = preloaderGraphics ? preloaderGraphics :new PreloaderGraphics();
if(stage)stage.scaleMode = StageScaleMode.NO_SCALE,
stage.align = StageAlign.TOP_LEFT,
stage.frameRate = 31,
createGraphics(),
stage.addEventListener(Event.RESIZE, resizeHandler);
else
_standalone = false;
addEventListener(Event.ENTER_FRAME, enterFrameHandler);
resizeHandler();
}protectedfunction createGraphics():void{addChild(preloaderGraphics asDisplayObject);
}privatefunction enterFrameHandler(e:Event):void{//percent = current / totalvar percent:Number = root.loaderInfo.bytesLoaded/root.loaderInfo.bytesTotal;
preloaderGraphics.percent = percent *100;
//trace(Math.round(100 * percent) + '%');if(framesLoaded == totalFrames){removeEventListener(Event.ENTER_FRAME, enterFrameHandler);
//Move to the next frame when we're done//Moving to frame 1(!)nextFrame();
preinit();
}}protectedfunction resizeHandler(e:Event = null):void{
preloaderGraphics.x = stage?stage.stageWidth/2:root.width/2,
preloaderGraphics.y = stage?stage.stageHeight/2:root.height/2;
}privatefunction preinit(mainclass_name_from_metadata:String = null):void{var needThrow:Boolean = true;
MAINCLASS_NAME = mainclass_name_from_metadata ? mainclass_name_from_metadata :
MAINCLASS_NAME ? MAINCLASS_NAME :null;
MAINCLASS_NAME ?null:(needThrow = false, getMETAMainClassName());
if(needThrow && !MAINCLASS_NAME){clear();
thrownewError(this.toString()+': Main Class Definition eq null');
}var has:Boolean = ApplicationDomain.currentDomain.hasDefinition(MAINCLASS_NAME);
if(has)//if main app
Main = ApplicationDomain.currentDomain.getDefinition(MAINCLASS_NAME)asClass;
else{
has = loaderInfo.applicationDomain.hasDefinition(MAINCLASS_NAME);
if(has)//if module app
Main = loaderInfo.applicationDomain.getDefinition(MAINCLASS_NAME)asClass;
}if(has){//and instantiate our Main class
main = new Main();
//get urls, whot need to additional preloading before starting application (in next post..)init();
clear();
}elseif(needThrow){clear();
thrownewError(this.toString()+': Main Class Definition NOT FOUND');
}}privatefunctioninit():void{//finally we add main class to the DisplayList and dispatching eventaddChild(main asDisplayObject).dispatchEvent(newEvent(PRELOADER_DONE));
//stage.addChild(main as DisplayObject) - only if we make a standalone app.}publicfunctionclear():void{//killing & clearing preloaderGraphics :)
IPreloaderGraphics(removeChild(preloaderGraphics asDisplayObject)).clear();
standalone ?(stage.removeEventListener(Event.RESIZE, resizeHandler), stage.removeChild(this)):null;
}//-----------------------------------------------------//privatefunction getMETAMainClassName():void{var bytes:ByteArray = newByteArray();
bytes.objectEncoding = 3;
bytes.writeBytes(loaderInfo.bytes);
bytes.position = 0;
var explorer:ClassExplorer = new ClassExplorer();
explorer.loadBytes(bytes);
explorer.addEventListener(Event.COMPLETE, explorerWorkCompleteHandler);
}privatefunction explorerWorkCompleteHandler(e:Event):void{var explorer:ClassExplorer = e.targetas ClassExplorer;
var xml:XML = explorer.getDefinitionInfo();
//trace(xml.toXMLString());var className:String;
className = xml.definition/*.(@name == 'frame2')*/.script.classTrait.metadata.(@name == 'App').arg.(@key == 'className').@value.toString();
preinit(className);
}}}
Как видно, в данной реализации я оставил возможность наследовать этот класс и переопределять значение переменной MAINCLASS_NAME. Так же в конструктор можно передать экземпляр визуализатора процесса загрузки, реализующий интерфейс IPreloaderGraphics. По умолчанию создается инстанс PreloaderGraphics - простейший субкласс DisplayObject'a, содержащий в себе текстовое поле для отображения процентов - это я тоже упростил:)
Вернемся к ru.kozlovskij.preload.Preloader. Как это все работает?
Запускается конструктор, подписываемся на событие Event.ENTER_FRAME и постоянно проверяем, не завершилась ли загрузка; Загрузка завершена, находим в метаданные и в них - имя главного класса (preinit->getMETAMainClassName->explorerWorkCompleteHandler->preinit), инициализируем главный класс, не забыв перед этим перескочить на второй кадр, - вот такая последовательность.
В демонстрируемой реализации в качестве "поисковика метадаты" я использовал ClassExplorer, за что отдельное спасибо автору.
Для наших целей пришлось немного его подправить. Не помню уже где и что исправлял - интересующимся поможет любой мержер, например, Araxis Merge, который я считаю очень достойным.
Единственный минус - это то, что при каждом добавлении нового метатэга, необходимо прописывать его в конфиге компиляции.
Теперь от третьим кадре:
Для того, чтобы в нашем рутовом классе (рутовый он только в случае, если приложение самостоятельное, не модуль), являющимся по-совместительству предзагрузчиком, при компиляции создался третий кадр, конфигурационный файл необходимо дополнить следующим образом:
Класс exampleClasses.CustomFrame будет экспортирован во фрэйм "newFrameName". Третьим этот фрэйм будет потому, что два уже есть.
По непонятной мне причине, добавлять ноды для четвертого, пятого и т.д. фрэймов - бесполезно.
Теперь смотрим в Main.as на метод preloaderDoneHandler. Последние два строки - по-сути, тоже самое, что и в ru.kozlovskij.preload::Preloader, т.е. переход на следующий кадр и инициализация соответствующего класса.
Написал VBS скриптец для пакетной компиляции .pbk файлов. Работает только под окнами.
Как работает: Принимает пачку входящих параметров - [fullUrlFile.pbk ...], выбрасывает все, что не .pbk, для каждого вызывает компилятор.
Принимает необязательные параметры:
-pb-C:\url\to\pbutil.exe
-bin-C:\url\to\
Написано кривовато, ибо в VBS я на момент написания абсолютно ничего не смыслел.
Применять эту штуку удобно в случае, если мы пишем и отлаживаем код в Pixel Bender Toolkit, а самих кернелов (.pbk) в проекте много. Приходится либо вызывать компиляцию для каждого обновленного ручками из Pixel Bender Toolkit-а, либо, если пользуемся плагином для Eclipse под названием PBDT, то сохранять каждый pbk, не, что тоже долго и неприятно.
Решение для Eclipse/FB: Просто подключаем этот vbs-скрипт как екстернал-компилятор и указываем в параметрах папку, куда следует складывать готовые .pbj. И все обновившиеся кернелы скомпилятся при следующем билде.
Конкретика:
Создаем в Project > Properties > Builders > .. новый билдер, называем его PBBCR;
Заменяем свой .externalToolBuilders\PBBCR.launch на аналог из скачанного архива;
Снова открываем Project > Properties > Builders > PBBCR > в поле Arguments первым параметром пишем/правим путь к PBBCR.vbs;
(опционально) Там же в поле Arguments пишем/правим путь к pbutil.exe (он есть в папке Pixel Bender-a начиная с 1.5.1 версии) и путь к папке, куда складывать бинарники .pbj.
Не забываем в списке билдеров Project > Properties > Builders > поставить наш PBBCR на самый верх, дабы он срабатывал первый.
Сам скрипт, пример .project, пример .launch в архиве.
Единственный нюанс: Если сами указываем output-папку и она реально не существует, ее необходимо создать самостоятельно - скрипт сам не умеет творить такие чудеса.