Пишем Shoot 'em up на Alternativa3D - Часть 2: flash 3D изнутри.

Molehill.gif

Часть 2: flash 3D изнутри.

Chapter 1 :: Chapter 2 :: Chapter 3

Помните базовый код класса Main, который автоматические в него вписывается при создании проекта (смотрите предыдущую часть)? Там было задействовано неописанное свойство Stage, которое вот собственно цитата:

Класс Stage представляет основную область рисования.

Для SWF-содержимого, выполняемого в обозревателе (в Flash Player), Stage представляет собой всю область отображения содержимого Flash. 

К объекту Stage нет глобального доступа. Доступ к нему осуществляется через свойство stage экземпляра DisplayObject.

Как видите stage - это наша область рисования, которая занимает всю нашу область отображения, грубо говоря - сцена (одно из значений на английском).

В цитате есть название одного неизвестного нам класса DisplayObject, который является:

Класс DisplayObject является базовым классом для всех объектов, которые можно поместить в список отображения. Список отображения управляет всеми объектами, отображаемыми в средах выполнения Flash. Используйте класс DisplayObjectContainer, чтобы упорядочить экранные объекты в списке отображения. Объекты DisplayObjectContainer могут иметь дочерние экранные объекты, а другие экранные объекты, такие как Shape и TextField, являются конечными узлами, у которых есть только родительские элементы и элементы одного уровня, но нет дочерних.

Класс DisplayObject поддерживает базовые функции, такие как положение объекта по осям x и y, а также некоторые расширенные свойства объекта, такие как матрица преобразования.

DisplayObject — это абстрактный базовый класс, поэтому DisplayObject нельзя вызывать непосредственно. При вызове new DisplayObject() выдается исключение ArgumentError.

Все экранные объекты наследуют свойства и методы класса DisplayObject.

У нас он используется в одном самом важном месте - главный класс приложения всегда расширяет (наследует от) класс Sprite, который сам расширяет класс DisplayObject. Что мы собственно и видим в автоматически сгенерированном коде.

А Sprite в свою очередь это:

Класс Sprite является базовым стандартным блоком списка отображения: это узел списка отображения, который способен отображать графику, а также может содержать дочерние элементы.

Объект Sprite напоминает фрагмент ролика, но не имеет временной шкалы. Sprite — это подходящий базовый класс для объектов, не требующих временной шкалы. Например, будет логично использовать класс Sprite в качестве базы для компонентов пользовательского интерфейса, для которых обычно не используется временная шкала.

У класса Sprite есть метод addChild, с помощью которого мы можем добавить любой экземпляр класса-наследника DisplayObject в список отображения.

 

Теперь по порядку:

  1. Главный класс нашего приложения должен быть наследником Sprite.
  2. У главного класса есть унаследованное свойство stage, которое является всей нашей областью отображения.
  3. Методом addChild нашего главного класса, мы можем добавить к списку отображения любой экземпляр класса-наследника DisplayObject, после чего он появится у нас на экране.

Здесь все должно быть понятно.

MOLEHILL

Технология Molehill (далее "молехилл") появилась в 11-ой версии флеш-плеера и дала возможность трехмерной (да и двумерной тоже) графике использовать ресурсы видеокарты (о боги, наконец-то!!!).

Как же это все работает (картинка идентична заголовку, но кликабельна):

Molehill_7147.gif

  1. Как видим у Stage может быть до четырех свойств Stage3d. Та stage3d, что выше, перекрывает нижнюю, но нам понадобится только одна :).
  2. Stage3d всегда находятся позади любого DisplayObject, т.е. двумерная графика всегда находится на переднем плане (в голову сразу приходит HUD, и пользовательское меню).
  3. У каждой Stage3d есть свойство Context3D, куда загружаются все ресурсы, которые должны быть выведены на экран. Все ресурсы могут быть загружены только после того как создан контекст.
Теперь посмотрите на код из предыдущей части статьи (ссылка на исходник).
Сначала мы импортировали классы, это обыкновенное дело в программировании.
Затем идет объявление нашего класса Main, расширяющего (extends) класс Sprite.
Перед всеми функциями мы объявили пять переменных:
  1. stage3D:Stage3D; в AS операция присваивания (=) работает с объектами несколько необычно - вместо копирования объекта, она передает ссылку на него; другими словами в конструкторе класса мы присвоим этому свойству объект stage.stage3d[0], после чего изменяя свойство мы будем изменять и объект
  2. camera:Camera3D; это класс Альтернативы3д для камеры. камера работает по своей простой системе, разберем ее ниже.
  3. rootContainer:Object3D; Object3D это корневой класс для всех трехмерных объектов альтернативы. Самого его можно использовать в качестве контейнера, как именно вы увидите ниже.
  4. controller:SimpleObjectController; контроллер для камеры, считывает из stage различные события нажатия клавиш и движений мыши, в зависимости от чего поворачивает и двигает камеру.
  5. box:Box; тот самый кубик, который мы видим в демке.

После объявления переменных идет функция-конструктор класса: public function Main(), тип у нее void и можно его не писать, т.к. конструктор. 
Конструктор класса Main.
Внутри функции мы записываем в переменную camera новый объект класса camera3D.

Параметры Camera3D(0.01, 10000)

  • 0.1 (camNearClipping) - минимальное расстояние, на котором рисуются полигоны;
  • 10000 (camFarClipping) - максимальное расстояние, на котором рисуются полигоны.

Сама по себе камера ничего не выводит на экран. Она занимается правильным отображением полигонов (т.е. определяет, что расположенный ближе к нам полигон перекрывает расположенный дальше), а также отсекает все полигоны, которые не входят в ее область видимости (область видимости камеры это неравносторонная пирамида, основанием которой является прямоугольник, повторяющий пропорции stage, вершиной являются координаты самой камеры минус camNearClipping, а высотой - camFarClipping; все полигоны что попадают в эту область и не перекрываются другими видимы камерой).

Далее мы задаем координаты камеры, тут все понятно, разве что стоит упомянуть, что все значения размерности в AS указываются в относительных условных единицах. Вы можете считать что 100 единиц соответствует 1 метру реального пространства, а можете брать 657 единиц за 2,43 километра, это ваше дело вкуса. Стоит лишь четко понимать разницу между пикселами в 2D, которые четко накладывают свои ограничения, и условными единицами размерности в 3D, которые ограничений практически не имеют. Как пример совместимых условных единиц измерения вспомните таковые в 3dsMax.

Следующий щагом мы добавляет контроллер класса SimpleObjectController. Этот класс подходит если вам нужен какой либо простой контроллер для тестирования приложения, широких возможностей от него не добиться, да нам в самой игре и не понадобится.

controller = new SimpleObjectController(stage, camera, 200);

В качестве параметров мы передаем ему:

  • stage (eventSource) - наш источник мышиных и клавиатурных событий, который должен быть объектом класса InteractiveObject;
  • camera (object3D) - контролируемый объект. На данный момент нам нужно управлять камерой.
  • 200 (speed) - скорость перемещения.

Сразу же заставляем его повернуть камеру в точку с координатами 0,0,0

 controller.lookAtXYZ(0, 0, 0);


Под конец этого блока кода создается вьюпорт для камеры

camera.view = new View(800, 600, false, 0xFFFFFF, 0, 4);


Именно во вьюпорт рендерится все, что видит камера, в качестве параметров передаем ему:

  • 800 и 600 - размеры вьюпорта;
  • false (renderToBitmap) - рендеринг в растровое изображение. Понадобится, если вы реализуете эффект зеркала (т.е. на месте зеркала стоит камера и смотрит на вас, ее вьюпорт рендерит все что видит в битмап, и в итоге вы накладываете полученную текстуру на само зеркало - ужасно ресурсоемко и медленно, так что где-то кто-то написал для этого спец шейдеры), нам оно не нужно, потому ставим "нет";
  • 0xFFFFFF (backgroundColor) - цвет фона;
  • 0 (backgroundAlpha) - прозрачность фона;
  • 4 (antiAlias) - антиалиасинг, то бишь сглаживание.
Если вы все еще не знаете, что такое рендеринг, шейдеры и полигоны, то что вы тут вообще делаете прочитайте эту статью.
И под конец добавляем вьюпорт к нашему главному классу. То есть вьюпорт добавляется к DisplayTree и с этого момента выводится на экран.
Создаем rootContainer, как видите конструктор класса Object3D вообще не имеет параметров. Как именно мы используем этот класс в качестве контейнера станет понятно ниже.
Добавляем в контейнер нашу камеру методом

rootContainer.addChild(camera);

Добавлять ее обязательно, иначе она не загрузится в контекст и фактически не будет существовать в нашем трехмерном пространстве.
Теперь поработаем с кубом. Для начала создадим его:

box = new Box();


Покроем его материалом со всех сторон:

box.setMaterialToAllSurfaces(new FillMaterial(0x0));


Я думаю перевод метода с английского очевиден. В качестве параметра ему передается материал. В AS многие объекты, которые используются очень редко удобнее не записывать в отдельную переменную, а создавать непосредственно там, где их нужно использовать, для чего и нужен оператор new (предварительно нужно импортировать класс FillMaterial), который вызывает конструктор нужного нам класса. Конструктор класса FillMaterial берет как параметр цвет будущего материала, мы ставим черный (могли бы написать 0х000000 в полной форме).
В итоге также добавляем куб в контейнер (к слову мы могли добавить куб в контейнер сразу после его создания, и уже потом покрыть его материалом, но так как-то эстетичнее):

rootContainer.addChild(box);

 

В следующем блоки последние строки инициализации.

stage3D = stage.stage3Ds[0];

 
Берем из вектора всех stage3D, нужную нам (под нулевым индексом) и записываем ее (а точнее ссылку на нее) в созданную нами переменную. Все это делается исключительно для удобства, ведь удобнее обращаься stage3D, чем постоянно прописывать stage.stage3Ds[0].
 

Подписываем нашу stage3D на событие входа в кадр:

stage3D.addEventListener(Event.CONTEXT3D_CREATE, init);

то есть каждый раз, когда stage3d пошлет событие о том, что у нее создался context3d, будет выполнена функция init.
Ну и наконец создаем сам контекст:

stage3D.requestContext3D();

 

На этом конструктор класса заканчивается. Напоминаю, что при запуске приложения первым делом создается экземпляр главного класса, то есть описанная выше функция конструктора выполнится самой первой.
Разберем функцию init.

Так как наша функция вызывается через слушатель события, она должна принимать объект класса Event или его наследников в качестве параметра. Тип у нее void.

private function init(event:Event):void


Теперь объясняю как устроен наш контейнер. У Object3D и его наследников есть свойства ресурсов, которые содержат массивы для вершинной программы в шейдере (массивы геометрии), массивы для пиксельной программы в шейдере и еще куча разных системных данных (мы также передаем сами шейдеры, которые хранятся в материалах). Чтобы все, что мы создали, было добавлено в наше трехмерное пространство, нужно загрузить все это в контекст. Для того, чтобы загрузить все ресурсы разом мы и создали наш rootContainer , добавляя ему в качестве детей нашу камеру и куб, мы тем самым передали ему и их ресурсы.

Дальше простая языковая конструкция загрузки всех ресурсов из контейнера в контекст:

for each (var resource:Resource in rootContainer.getResources(true))
{
resource.upload(stage3D.context3D);
}


На самом деле наше трехмерное пространство уже создано. Осталось только его отобразить, для этого подписываем экземпляр нашего главного класса на событие входа в новый кадр:

addEventListener(Event.ENTER_FRAME, enterFrameHandler);

Каждый раз, когда будет отсылася событие о том, что завершена отрисовка предыдущего кадра и началась рисовка нового, будет вызываться функция enterFrameHandler, из-за удобной однопоточности платформы Flash, новое событие не наступит, пока полностью не выполнится эта функция.

 

Функция enterFrameHandler.


Так как функция вызывается из слушателя события, то вы уже должны знать какого она типа и что в качестве параметра вызывает (идентично предыдущей функции - init).

Здесь всего две простых строчки:

controller.update();

Обновляем контроллер, т.е. пересчитываем координаты и углы наклона контролируемого объекта (нашей камеры) в зависимости от событий нажатия клавиш и движений мыши с зажатой левой кнопкой.

 

camera.render(stage3D);

Одним росчерком пера мы отрисовываем всю нашу иерархию объектов в переданную Stage3d. 

 

ВЫВОДЫ

Я надеюсь вы поняли всю прелесть движка Alternativa3D 8. Вместо того, чтобы самому  оперировать массивами вручную созданной геометрии, вместо того чтобы писать шейдеры на ассемблере под каждый вид текстуры, вместо того чтобы каждый кадр исполнять весь массив шейдеров, нам достаточно написать лишь camera.render(stage3D).


Также интуитивно понятно создаются объекты и добавляются в список отображения, но это все лишь основа движка (мы использовали по большей части классы из пакета Core) так что ждите следующей части, в которой мы приступим к прототипу нашего шмапа (в итоге в него можно будет даже "поиграть").