Animator Назначение движка Движок дает предметно-ориентированное API для создания высокопроизводительных графических приложений. Основным его применением, вероятно, станут игры. Движок позволяет описать параметрическую модель игрового мира, посредством специализированных классов. Позволяет отобразить модель в кадре анимации. А также реализует часто встречающиеся элементы управления игрой (контроллеры). С другой стороны, движок не реализует (и не накладывает ограничений) на реализацию конкретной бизнес-логики приложения (для игры – это игровой AI). Общий подход к построению движка Следует выработать единый стиль создания компонент (code conventions). Можно поискать готовый, благо их много, в т.ч. и игровых. Следует стараться избегать свойств (особенно, геттеров). Но не следует так же заменять геттеры на функции. Нужно искать компромисс. Т.к. рассматривается интеграция с LibCanvas, можно попробовать придерживаться стиля, принятого в данной библиотеке. Так же имеет смысл плотно использовать AtomJS. В любой игре есть три взаимосвязанных сущности: Параметрическая объектная модель игрового мира. Алгоритм управления игрой - AI. Алгоритм отрисовки. Следует стремиться реализовать логику работы этих сущностей так, чтобы они делали только свою работу, а с остальными сущностями взаимодействовали через соответствующие интерфейсы. Например, параметрическая модель не должна обеспечивать рендер, а AI не должен рассчитывать коллизии (может только проверять их наличие). Выглядеть это может примерно так: С помощью объектной модели описывается игровой мир: помещения, декорации, персонажи, спецэффекты и т.д. С помощью Viewport-ов этот мир можно правильно отобразить в кадре анимации, согласно положению и состоянию всех его объектов (в т.ч. и самого Viewport-а). С помощью AI миром можно управлять. Для этого AI может менять параметры объектов, добавлять новые, удалять имеющиеся и т.д. Для облегчения задачи типового управления, часть алгоритмов AI можно оформить в виде контроллеров и применять их к объектам стандартным образом. Например, контроллер патрулирования территории, контроллер преследования, контроллер столкновений и т.д. Декларативное описание Общая базовая функциональность за счет унификации всех компонент. Основой являются графические примитивы и контейнеры – составные графические примитивы, объединяющие примитивы и другие контейнеры. Контейнеры могут добавлять специфическую функциональность к группе примитивов. Примитивы могут быть текстурированы с использованием растровых карт. К контейнерам и примитивам можно применять эффекты (например, прозрачность, масштабирование, освещение и т.п.). На свойства эффектов, контейнеров и примитивов можно навешивать контроллеры и триггеры. Контроллеры управляют изменением свойства по заданной программе, а триггеры срабатывают при достижении свойством установленного порога. Любой контейнер имеет собственную трехосную систему координат (не обязательно ортогональную) и любой примитив (в т.ч. вложенный контейнер) описывается положением верхнего левого угла в этой системе, а также наклонами относительно осей родительского контейнера вокруг произвольной точки (pivot). Размер контейнера определяется размерами его примитивов (т.е. явно не задается). Для вывода изображения используются специализированные объекты – Viewport-ы. Характер отображения будет зависеть от типа Viewport-а (планиметрия, перспектива), и от его настроек (угол обзора, фокусное расстояние и т.п.). Поясняющий пример: VP1 и VP2 - Viewport-ы с перспективной проекцией, VP3 – с планиметрической. Схему на приведенном рисунке можно сделать из 2-х простых контейнеров: один включает 4 прямоугольных полигона для стен, на которые наложены простые цветовые текстуры (solidColor), второй включается в состав первого и содержит 4 полигона центральной колонны. Состав движка Теоретически движок может работать с любым API рендеринга. Для начала следует реализовать поддержку 2D Canvas API (склоняемся в сторону LibCanvas и AtomJS). Движок создает главный контейнер, с бесконечными шириной, длинной и глубиной, к которому можно добавлять другие контейнеры верхнего уровня. Движок поддерживает несколько мультикастовых событий: - от клавиатуры - от мыши. Любой объект можно подписать на одно или несколько из этих событий. Пока на событие никто не подписан, оно не активно и не отслеживается в системе. Как правило на события клавиатуры и мыши подписывается AI. Библиотека растровых карт - текстуры (в т.ч. текстурные карты) - виртуальные текстуры - по сути кэш растеризации линий, фигур, текстов, заполнителей (solidColor, linearGradient, ...) и т.п. Статическая растровая карта может иметь только 1 элемент. Динамическая – несколько (в переделе – бесконечное число элементов для процедурных карт). Если динамическая структура накладывается на какой-либо полигон, можно задействовать его свойство frame для выбора части текстуры из предопределенного набора. Библиотека графических примитивов Все примитивы по умолчанию прозрачны и заполняются исключительно растровыми картами - Полигоны - треугольники, прямоугольники, многоугольники, овалы и т.п. Позволяют применить любую текстуру из библиотеки растровых карт либо как есть, либо с замощением, либо с растягиванием и т.п. Библиотека специализированных контейнеров Контейнеры позволяют объединять объекты и согласованно изменять их параметры групповым образом. А так же, добавляют специализированную функциональность (например, кнопка). Контейнеры имеют собственные оси координат и все объекты внутри контейнера описываются относительно них. Контейнеры могут включать другие контейнеры неограниченно. Т.о. образуется граф объектов. Примеры контейнеров: - простой контейнер – обладает только базовой функциональностью, - кнопка - может состоять из нескольких любых примитивов - фон, рамка, текст, позволяет регистрировать обработчик нажатия, - слайдер, - скроллбокс - и т.п. Библиотека Viewport-ов. Задача Viewport-а - отрисовка объектов в кадре анимации, согласно их координатам и трансформациям (повороты, эффекты и т.п.). Для этого алгоритм: - обходит граф всех объектов, - определяет какие объекты попадают в поле зрения (например, трассировкой) - строит граф отрисовки (скорее всего это будет простой список - надо еще подумать) - вызывает событие readyDrawing на которое можно подписать контроллеры и AI. - рисует объекты. Пока планируется ограничиться одним типом Viewport-а с планиметрической проекцией. Библиотека хелперов - куки (хранение/восстановление JSON сериализации), - случайные числа (различные распределения, целые / дробные, вероятности, условно случайные), - звук. Библиотека эффектов (например, эффект прозрачности, осветления/затемнения и т.п.). Эффекты рассчитываются исключительно для тех объектов, которые попали в граф отрисовки Viewport-а. Библиотека контроллеров и триггеров. Контроллеры и триггеры навешиваются на любой объект или эффект или на отдельный параметр объекта или эффекта. Задача контроллера - изменение одного или нескольких параметров по определенному алгоритму. Контроллер можно связать с любым событием. Например, контроллер патрулирования можно связать с событием таймера движка. Тогда с определенной периодичностью этот контроллер будет обновляться и изменять контролируемый параметр или объект. Задача триггера - вызов обработчика при срабатывании указанного условия для контролируемого параметра или объекта. Общая стратегия использования примерно такая: Для примера возьмем предыдущий рисунок и нарисуем пол и колонну - вначале создается движок на одной из поддерживаемых технологий. //создаем движок типа CanvasDriver, работающий с Canvas объектом var am = new Animachine.CanvasDriver('#my_canvas') - объявляются используемые текстуры: var bi = new Bitmap.Initializer(); var bg = bi.regTexture (“background.jpg”); //простая картинка, размером 200х500 var enemy = bi.regTextureMap(“sprite01.png”, [50, 80]); //текстурная карта с элементами 50x80 var faceColor = bi.regSolidColor(“#face”); //текстура для заливки цветом var waterFall = bi.regFunctional(functor); //функциональная текстура, управляемая функтором bi.init(onProgress); //здесь инициализируются все созданные тесктуры и отображается прогресс - затем структурами можно заполнить полигоны: var floor = new Primitive.Poligon(bg); //пол, размером с текстуру floor.y = -100; //добавляем эффект поворота вокруг верхнего левого угла на угол 90 градусов относительно оси x floor.effects.add(new Effect.Rotate(floor.pos, [Math.PI/2, 0, 0])); //4 прямоугольных стены для колонны var walls = [new Primitive.Polygon(faceColor, new Primitive.Polygon(waterFall, new Primitive.Polygon(faceColor, new Primitive.Polygon(waterFall, Primitive.Filling.Stretch, Primitive.Filling.Stretch, Primitive.Filling.Stretch, Primitive.Filling.Stretch, [50, [200, [50, [200, 100]), 100]), 100]), 100])]; /* можно было бы задавать координаты стен так walls[0].x = 50; walls[0].y = -100; walls[0].z = 350; walls[2].pos = [50, -100, 20]; //аналогично walls[2].x = 50; walls[2].y = -100; walls[2].z = 20; walls[1].pos = [100, -100, 350]; walls[3].pos = [50, -100, 20]; но это слабо переносимо, т.к. такую колонну ни передвинуть, ни повернуть нормально не получится. Самое время применить контейнеры */ - Делаем два контейнера для комнаты и колонны var column = new Container.Simple(); //простой контейнер с базовой функциональностью column.add(walls); //можно сделать и после размещения всех эелемнтов, но для примера сделаем тут column.beginUpdate(); //чтобы не персчитывались размеры при всяком изменении составляющих walls[0].pos = [0, 0, 200]; walls[1].pos = [50,0]; //x=50, y=z=0 walls[2].pos = [0]; //x=y=z=0 walls[3].pos = [0]; //нужно повернуть две боковые стены относительно оси y на 90 градусов по часовой var a = [0, Math.PI/2, 0]; walls[1].effects.add(new Effect.Rotate(walls[1].pos, a)); walls[3].effects.add(new Effect.Rotate(walls[3].pos, a)); column.endUpdate(); //закончили размещать стены, можно и пересчитать //помещаем пол и колонну в общий контйенер var room = new Container.Simple(); room.add([floor, column]); am.rootContainer.add(room); //размещаем колонну по центру пола сolumn.x = (room.width - column.width)/2; column.z = (room.depth – column.depth)/2; //контейнеры имеют свойство «глубина», а примитивы – нет Следует пояснить, что все координаты и размеры носят относительный характер. Если контейнер добавляется непосредственно в главный контейнер движка (как, например, для room), то все считается относительно экранной системы координат (оси x и y расположены как обычно, ось z – ортогональна плоскости монитора и направлена в сторону оператора). Иначе – все считается относительно координатных осей контейнера. Так в нашем примере, даже если мы повернем колонну по одной из осей, все координаты и углы поворота ее стен останутся такими же, как были. Поэтому можно договориться, что шириной контейнера считается проекция описывающего его прямоугольного параллелепипеда на ось X, высотой – аналогичная проекция на ось Y, глубиной – на ось Z. - теперь можно попробовать вывести картинку на экран //добавим камеру в координатном пространстве экрана var camera = new Viewport.Perspective(am); //Perspective projection viewport camera.focus = 0; //камера с нулевым гиперфокальным расстоянием и бесконечной глубиной резкости camera.a = 2 * Math.PI / 3; //Угол изображения объектива – 120 градусов camera.pos = [50, 50, 700]; //координаты относительно экрана, т.к. камера не включена в контейнер Helper.Interaction = new Helper.Interaction.Default(); //Задаем хэлпер интерактивности по умолчанию Helper.Interaction.fps = 30; Helper.Interaction.resume(); //теперь камера будет выводить изображение с частотой 30 fps Что дальше? После реализации минимальной базовой функциональности движка, можно предпринять попытку создания на его основе WYSIWYG редактор, который позволит в графическом виде конструировать простейшие карты. На первых порах, выхлопом этого редактора должен стать валидный JS код уровня (т.н. параметрическое описание карты) – весьма полезно для отладки, а в дальнейшем можно изменить это поведение