Готовый перевод Adobe Flash 11 Stage3D (Molehill) game programming (Chapter 6 - 10) / Программирование игр в Adobe Flash 11 Stage3D (Molehill) (Глава 6 - 10): Таймеры, ввод и сущности: Великолепный геймплей!

Теперь вы вступаете на уровень 7.
Следующий шаг в нашем грандиозном приключении – реализовать управление игрока, так чтобы действительно чувствовалось, что вы что-то делаете! Прекрасной графики и фантастической частоты кадров уже не достаточно – пришло время взять узду и укротить это животное, добавив таймеры и класс игровых сущностей для более простого контроля действий.

Интерактивность – отличительный признак игры. Геймплей - это результат обратной связи входных данных, которые приводят к интересным выходным данным. Действие и реакция. Нажмите кнопку и выстрелите пулей. Выстрелите ракетницей и разнесите пришельца в щепки. Уничтожьте босса и перейдите на следующий уровень. Всё это – цикл обратной связи, который превращает демо в игру. Причина и следствие, которые комбинируясь приводят к геймплею.

В этой главе мы добавим возможность перемещаться по нашему игровому миру. Без возможности управлять вещами, наша игра до сих пор больше была похожа на графическое демо. Кроме того, мы хотим иметь возможность управлять многими видами игровых лиц от погони камер за космическим кораблём до пуль и прочего.
В этой главе мы создадим:
Класс таймера, который измеряет время для использования в анимации
Класс входных данных, который обнаруживает ввод игроком данных с клавиатуры и мыши
Класс игровых лиц для использования всеми игровыми объектами
Наложение хедз-ап дисплея
Более детализированный игровой мир.

Наш текущий квест.
Мы объединим данные, вводимые игроком, основанную на таймере анимацию и отследим камеру, для того, чтобы реализовать новую и улучшить сцену, которая основана на работе, которую мы сделали в предыдущих главах. Мы усовершенствуем наш игровой прототип, так чтобы он стал действительно интерактивным: вы будете летать по детализированной вселенной, где вещи движутся по кругу.

Ваша скорость будет зависеть от частоты кадров, это значит, что она будет в зависимости от времени и не будет ускоряться или замедляться, если FPS изменится. Игровой мир будет увеличиваться и сложности с дополнительными астероидами и небом на заднем фоне. GUI игрового движка будет обновлено благодаря добавлению HUD (хэдз-ап-дисплей) графики, которая накладывается поверх 3D-графики. Наконец, ваш космический корабль будет иметь светящийся анимированный двигатель, чтобы быть немного более привлекательным.

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

Сохраняем простоту.
Как и во многих приключениях путь к нашей цели ведёт нас через искушения. Мы можем поддаться искушению добавить так много геймплея и так много характеристик, что мы можем упустить ошибки в чрезмерно сложной игре.
Мы начнём с более базового геймплея, цикла обратной связи: простота действий, которая складывается в ответную реакцию геймплея. Мы создадим основной фреймворк общих игровых классов, который, который просто надстроить.

Вместо стремления совершать миллионы различных вещей, мы должны сначала сделать одну или две вещи хорошо и тогда шлифовать и углублять набор функций. Имея это ввиду, давайте запланируем первую реализацию простейших элементов геймплея и медленно улучшим их до «приносящих удовольствие».
Мы можем добавить сложные характеристики, такие как искусственный интеллект или определение столкновений позже. Вместо попыток запрограммировать каждую возможность подобно игровой интерактивности, мудрый воин выберет более прямой маршрут до финишной прямой.

Делаем это повторно используемым.
Сейчас, чтобы мы погрузились в разработку нашей игры, пришло время получить несколько процедур, которые мы сложим вместе и отправим их в повторно используемые классы. Пока мы на этом, некоторые основные функции, используемые во всей игре, могут быть абстрагированы и сделаны готовыми для использования различными способами.

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

Для того, чтобы сделать это, мы должны создать повторно используемый игровой класс таймера, который отслеживает сколько времени прошло до сих пор, а также сколько точно времени прошло с момента предыдущего кадра. Это важно, потому что FPS будет постоянно изменяться ( без потерь пользователь играет на супер-мощном компьютере, и частота кадров никогда не падает ниже максимальной). Каждый кадр в игре будет занимать различный период времени и изредка будут большие расхождения между частотой кадров и периодом времени в ходе игровой сессии. Когда много всего происходит или есть сразу много мешей на экране, требуется длительное время, чтобы отобразить экран и обновить модели.

В совершенном мире наша игра никогда не должна опускаться ниже 60 fps, но в реальном мире будут случаи, когда частота кадров снижается (особенно на старых компьютерах) и мы не хотим, чтобы наша игра работала различно в зависимости от того, какой компьютер используется или сколько врагов видно.

Основанное на времени моделирование необходимо, чтобы обеспечивать отсутствие «икоты» во время игры, и чтобы все пользователи получили одинаковый опыт несмотря на аппаратные возможности их компьютеров. Эта информация «время, прошедшее за кадр» будет использоваться зачастую в типичных играх, чтобы управлять перемещениями вашего игрока, пулями и врагами, так, чтобы их скорость оставалась постоянной, несмотря на сложность сцены.

Наконец, вместо заполнения одного исходного файла Action Script с тысячами линий связанного кода, чтобы создавать и анимировать различные игровые объекты, такие как космические корабли, рельеф, будет мудро создать абстрактный класс игровых сущностей. Этот класс сущностей будет отвечать за инициализацию объектов , расчёт матриц трансформации каждого объекта, рендеринг себя, и т.д.

Вместо всего этого кода, размещённого в разных местах, и части которого разбросаны по основному базовому коду нашей игры, он должен быть собран в одно место и сделан повторно используемым. В этом случае один класс сможет управлять игроками, врагами, пулями, рельефом, спецэффектами и многим другим.

Делаем нашу игру более интерактивной.
Для того, чтобы успешно завершить квест, намеченный ранее, мы должны совершить следующее:
Добавить HUD-дисплей поверх графики
Запрограммировать класс игрового таймера
Создать простой класс входящих игровых данных
Реализовать абстрактный класс сущностей
Спроектировать оформление для нашего нового, улучшенного игрового мира
Обновить наш игровой движок, чтобы управлять всем этим новым хламом

Предыдущие шаги кратко намечают задачу, требуемую, чтобы обновить нашу игру до поддержки истинной интерактивности игрока, так же как и создание инфраструктуры, требующейся для будущих игровых прелестей, таких как враги, система частиц и другие спецэффекты. Начнём с самого простого из предыдущей задачи – создания модно выглядящего хэдз-ап-дисплея. Это может действительно добавить общий вид в вашу игру. Он заставит её чувствовать себя похожей на игру и будет большим вкладом.

Добавляем HUD-дисплей поверх графики.
Мы наложим изображение поверх нашего Stage3D вида, которое действует в качестве привлекательного окна для нашей 3D-графики. Создайте .PNG файл в вашем основном редакторе изображений, таком как Photoshop. Убедитесь, что большая часть этого изображения прозрачная. Вы можете добавить ориентационную сетку (прицел), или стекло кубрика корабля, или маленький компьютерный терминал. Для нашего примера игры-шутера с космическим кораблём, мы используем это изображение:

Немного техно-метала и пара компьютерных экранов – это всё, что требуется, чтобы получить немного стиля в нашей игре. Вы можете использовать множество изображений или только одно изображение (как выше), которое точно такого же размера как и ваша игровая сцена. Импортируйте это изображение во Flash, перетащив его на палитру библиотеки и убедитесь, что оно использует корректное имя и доступно в вашем Action Script. Если вы используете чистый AS3 или Flex, тогда просто добавьте его в код тегом [Embed].

Время для действий: накладываем GUI.
Создайте ссылку на ваше новое наложенное изображение HUD в коде, лучше на верху вашего класса Stage3dGame.as, где-нибудь под определением класса, например:
public class Stage3dGame extends Sprite {
// наложенное изображение HUD:
// Версия для чистого AS3:
[Embed (source = "hud_overlay.png")]
private var hudOverlayData:Class;
private var hudOverlay:Bitmap = new hudOverlayData();
// Версия Adobe CS5:
private var hudOverlay:Bitmap =
new Bitmap(new hudTextureBitmapData(640,4 80));

Мы только встроили AS3 код или версию для CS5 в зависимости от вашей рабочей среды. Сейчас, всё, что мы должны сделать – это добавить этот новый hudOverlay битмап на сцену во время инициализации вашей игры. Добавьте следующие строчки наверх вашей существующей функции initGUI() следующим образом:
private function initGUI():void {
// накладываем хэдз-ап-дисплей
addChild(hudOverlay);

Что же произойдёт?
Теперь, когда вы соберёте и запустите ваш SWF, вы должны увидеть наложение поверх вашего Stage3D вида. Помните, что причина, по которой мы можем добавить его на сцену в любое время (перед или после создания Stage3D) в том, что 3D графика всегда рендерится первой, ниже всех обычных Flash спрайтов.

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

Один из очень удобных приёмов, который может быть использован, чтобы сохранить вашу игровую частоту кадров невероятно быстрой, это совершать сложные вычисления каждое время от времени, а не каждый отдельный кадр. Хотя многое в вашей игровой логике должно делаться каждый кадр как часть цикла рендеринга, большинство мастеров программирования игр полагаются на замечательную технику оптимизации, которую иногда называют функцией «сердцебиения». Это техника оптимизации, которую большинство ААА игр используют, чтобы сохранить FPS таким высоким, как это возможно.

Эта функция сердцебиения вызывается только время от времени. В нашем примере игры будем вызывать её только один раз в секунду. На данный момент мы не будем делать многого во время этой фазы, но это хорошая основа для медленных или сложных процедур, которые должны совершаться только иногда. К ним относится вычисление видимости объектов, когда вы имеете гигантский игровой мир: вместо прохода через каждый объект, возможно десятки тысяч каждый отдельный кадр, просто обновляйте видимость в зависимости от расстояния от камеры один раз в секунду.

В будущих доработках нашей игры мы сможем выбирать использовать её как идеальное место для LOD (уровня детализации), ИИ (искусственного интеллекта) и игровые события, такие как таймеры обратного отсчёта, которые могут быть использованы во времени бомб или показывать конец уровня, или срабатывание закрытия дверей или выключение света по истечении установленного интервала. Возможно, во время сердцебиения вы вызовете эффекты окружения, такие как вспышки молнии и звук грома, или запустите следующий кусок музыки. Процедуры подобно этим не должны запускаться каждый отдельный кадр.

На данный момент наша функция сердцебиения будет пустой оболочкой, готовой для будущего использования. Просто запомните, что всякий раз года страдает ваша частота кадров (должно быть слишком много вычислений происходит каждый отдельный кадр), вы можете рассмотреть возможность перемещения некоторого из этого кода в вашу функцию сердцебиения, так как она выполняется только время от времени. Если у вас есть код, который не должен запускаться каждый отдельный кадр, поместите его в функцию сердцебиения и запускайте её вместо этого только раз.

Примеры вещей, которые вы можете сделать в функции сердцебиения:
Проверять завершилась ли загрузка
Отправлять информацию позиционирования на сервер мультиплеера
Проверять на видимость рудиментарное PVS (потенциально видимые установки) решение
Запускать чрезвычайно сложные процедуры искусственного интеллекта, такие как пути обнаружения, что занимает больше чем один кадр на вычисления.

Для того, чтобы успешно реализовать предыдущие особенности, вы должны совершить следующее:
1. Создать новый класс с переменными для записи времени и счётчиков.
2. Создать конструктор, занимаемый функцией указателем для использования как функция сердцебиения.
3. Запрограммировать функцию счёта, которая выполняется каждый кадр и измеряет интервал времени.
4. Запускать функцию сердцебиения только когда это нужно.

Время действовать - создаем класс таймер для игры
Описанные выше шаги кратко излагают то, что должно быть запрограммировано в классе GameTimer, что бы быть готовым к использованию в нашей игре. Для начала, давайте создадим новый класс. Создайте новый файл в папке проекта, и назовите его GameTimer.as и опишите таймер следующим образом:
// Stage3d game timer routines version 1.0
package
{
import flash.utils.*;
public class GameTimer {
// когда игра начата
public var gameStartTime:Number = 0.0;
// метка: предыдущий фрейм
public var lastFrameTime:Number = 0.0;
// метка: текущий фрейм
public var currentFrameTime:Number = 0.0;
// сколько милисекунд назад был вызван предыдущий кадр
public var frameMs:Number = 0.0;
// колличество фреймов в текущей игре
public var frameCount:uint = 0;
// когда показывать следующий
public var nextHeartbeatTime:uint = 0;
// сколько милисекунд прошло
public var gameElapsedTime:uint = 0;
// как часто происходит сердцебиение
public var heartbeatIntervalMs:uint = 1000;
// функция для запуска удара сердца
public var heartbeatFunction:Function;
}

Что же произойдёт?
В предыдущем коде всё, что мы сделали, это создали новый класс по имени GameTamer, который сохраняет несколько удобных публичных переменных, которые могут быть необходимы нашей игре. Во время нашего игрового цикла рендеринга мы будем обновлять наш таймер, и эти значения будут меняться.

Время для действий: добавляем конструктор класса GameTimer.
Теперь мы запрограммируем конструктор таймера. Наша игра захочет передать ссылку на функцию сердцебиения, которой мы хотим вызывать таймер, и как часто мы хотим запускать его:
//конструктор класса
public function GameTimer(
heartbeatFunc:Function = null, heartbeatMs:uint = 1000)
{
if (heartbeatFunc != null)
heartbeatFunction = heartbeatFunc;
heartbeatIntervalMs = heartbeatMs;
}

Что же произойдет?
Предыдущий код просто инициализирует наш новый класс GameTimer , объявляя ссылки на функцию обратного вызова, которую наша игра, возможно, должна запускать через определённые интервалы.

Время для действий: реализуем функцию отсчёта.
Нашей игре нужно обновлять таймер каждый кадр, так что она получает возможность измерять время, прошедшее с предыдущего кадра и, возможно, запускать функцию сердцебиения если нужно. Следующая функция отсчёта будет делать для нас эту работу.
public function tick():void {
currentFrameTime = getTimer();
if (frameCount == 0)
// первый кадр?
{
gameStartTime = currentFrameTime;
trace("First frame happened after "
+ gameStartTime + "ms");
frameMs = 0;
gameElapsedTime = 0;
}
else
{
// сколько времени прошло с последнего кадра?
frameMs = currentFrameTime - lastFrameTime;
gameElapsedTime += frameMs;
}
if (heartbeatFunction != null)
{
if (currentFrameTime >= nextHeartbeatTime)
{
heartbeatFunction();
nextHeartbeatTime = currentFrameTime + heartbeatIntervalMs;
}
}
lastFrameTime = currentFrameTime;
frameCount++;
}
} // конец класса
} // конец пакета

Что же произойдет?
На этом мы закончим наш воображаемый новый класс GameTimer! Как вы можете видеть, это очень простой кусок кода. Тем не менее, он тоже очень полезен и достаточно мощен: фактически, каждая ига, даже готовая, использует что-то вроде этого, чтобы следить за временем, и вызывает события в зависимости от прошедшего времени.

Функция getTimer возвращает значение в миллисекундах. Запомнив предыдущее значение, наш класс таймера может рассчитать, сколько мс прошло с начала игры или с последнего раза, когда вызывалась функция отсчёта.

Зачем измерять время постоянно? Разве не проще обновлять локации и анимации каждый кадр? Причина этого в том, что мы никогда не можем быть уверены, что наша частота кадров не будет колебаться. Человек, запустивший вашу игру на менее мощной машине, будет иметь более низкую частоту кадров, чем некто, запустивший игру на мощном игровом устройстве. Используя таймеры, ваша игра будет запускаться с одинаковой скоростью, несмотря на FPS, и по мере роста, она будет запускаться с точно такой же скоростью, независимо от того, насколько мощная машина.

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

Измерение времени – это первый шаг в создании интерактивности в игре. Анимации должны знать, сколько времени прошло, и игровые события часто происходят только в определённые интервалы. Некоторые объекты в нашей игре, возможно, должны знать свой «возраст», для того, чтобы взрываться после определённого периода времени. На данный момент это всё, что нам нужно, для того, чтобы перейти к следующему шагу.

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

Так как многие игры могут вместо этого использовать мышь для перемещения или чтобы осмотреться, что должно быть удобно в шутерах от первого лица, мы должны также отслеживать местоположение мыши и обнаруживать нажатия кнопки и перетаскивания. Наконец, обычно многие игры используют клавиши клавиатуры W, A, S, D для перемещения, как альтернатива использованию клавиш со стрелками. Эта схема ввода данных очень распространена в шутерах и также является удобной , что обеспечит пользователям запуск вашей игры на устройствах без клавиш со стрелками (таких как некоторые телефоны), и они всё ещё смогут управлять вашей игрой.

Для того, чтобы запрограммировать предыдущие характеристики, мы должны совершить следующее:
1. Создать новый класс по имени GameInput
2. Добавить переменные, используемые для отслеживания текущих состояний ввода.
3. Запрограммировать конструктор, который инструктирует Flash отправлять события, когда входящие данные изменились.
4. Обработать входящие события, обновляя текущее состояние входных данных.

Время для действий: создаём класс GameInput.
Принимая во внимание предыдущие шаги, создадим новый файла класса по имени GameInput.as в вашей папке проекта:
//алгоритм ввода данных Stage3D игры, версия 1.0
//
package
{
import flash.display.Stage;
import flash.ui.Keyboard;
import flash.events.*;
public class GameInput {
// текущее состояние мыши
public var mouseIsDown:Boolean = false;
public var mouseClickX:int = 0;
public var mouseClickY:int = 0;
public var mouseX:int = 0;
public var mouseY:int = 0;
// текущее состояние управления с клавиатуры
public var pressing:Object =
{ up:0, down:0, left:0, right:0, fire:0 };
// если обзор мышью включён, он добавляется вслед за камерой
public var cameraAngleX:Number = 0;
public var cameraAngleY:Number = 0;
public var cameraAngleZ:Number = 0;
// если trueперетаскивая мышь, изменяем угол камеры
public var mouseLookMode:Boolean = true;
// основная сцена игры
public var stage:Stage;

Что же произойдёт?
В предыдущем коде мы импортировали во Flash функциональность, требующуюся для мыши и клавиатурных событий. Затем мы определяем на ш класс GameInput и сохраняем переменные, которые будут использоваться в нашей игре, чтобы запрашивать текущее состояние ввода. Например, в нашей игре мы сможем, в конечном счёте, проверить, если нашклассввода.нажатие.левое истинно или нет, и тогда действовать соответственно.

Время для действий: программируем конструктор класса Gamelnput.
Непосредственно под объявлением переменных, которые мы только что добавили, добавьте конструктор класса, который будет выполняться всякий раз, когда мы создаём новый объект GameInput в нашей игре. Мы сделаем это следующим образом:
//конструктор класса
public function GameInput(theStage:Stage)
{
stage = theStage;
// получаем нажатия клавиш и обнаруживаем потерю фокуса игры
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed); stage.addEventListener(KeyboardEvent.KEY_UP, keyReleased);
stage.addEventListener(Event.DEACTIVATE, lostFocus);
stage.addEventListener(Event.ACTIVATE, gainFocus);
stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown); stage.addEventListener(MouseEvent.MOUSE_UP, mouseUp); stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);
}

Что же произойдет?
Так как процедуре ввода требуется ссылка на сцену игры, мы должны отправлять её этой процедуре. Затем мы инструктируем Flash информировать нас, когда происходит любое из событий, связанных с вводом игрока. Они включают события отпускания и нажатия клавиши, события мыши и события, которые сообщают игре, когда теряется фокус (такие как когда пользователь щёлкает другое окно браузера и больше не глядит на игру). Это важно, потому что когда игра теряет фокус клавиатуры, мы, возможно, захотим приостановить игру или предположить, что клавиши движения, нажатые ранее, уже не в таком состоянии: мы можем никогда не получить события KEY_UP, если клавиша была отпущена после того, как игра потеряла фокус ввода.

Время для действий: определяем движения мыши.
Следующий шаг это запрограммировать значения, чтобы определять местоположение мыши, нажата ли кнопка или нет, и запоминать начало клика с перетаскиванием, так чтобы мы могли измерить расстояние, которое преодолела мышь, перед тем, как была отпущена кнопка. Мы сделаем это следующим образом:
private function mouseMove(e:MouseEvent):void {
mouseX = e.stageX;
mouseY = e.stageY;
if (mouseIsDown && mouseLookMode)
{
cameraAngleY = 90 * ((mouseX - mouseClickX) / stage.width);
cameraAngleX = 90 * ((mouseY - mouseClickY) / stage.height);
}
}

Следующая функция будет запускаться каждый раз, когда игрок перемещает мышь. В ситуациях, когда кнопка мыши удерживается нажатой, мы хотим отслеживать пройденное расстояние с момента, когда кнопка была нажата первый раз, для того чтобы надлежащим образом осуществлять «обзор мышью» в нашей игре.
private function mouseDown(e:MouseEvent):void {
trace('mouseDown at '+e.stageX+','+e.stageY);
mouseClickX = e.stageX;
mouseClickY = e.stageY;
mouseIsDown = true;
}

Предыдущая функция срабатывает, когда впервые нажимается кнопка мыши. Текущее положение курсора мыши записывается для использования в предыдущей функции mouseMove, так что мы знаем, где «перетаскивание мышью» началось впервые, что позволяет нам измерять расстояние, которая она прошла по экрану.
private function mouseUp(e:MouseEvent):void {
trace('mouseUp at '+e.stageX+','+e.stageY+' drag distance:'+ (e.stageX-mouseClickX)+','+(e.stageY-mouseClickY));
mouseIsDown = false;
if (mouseLookMode)
{
// reset camera angle
cameraAngleX = cameraAngleY = cameraAngleZ = 0;
}
}

Что же произойдёт?
В предыдущем коде, каждый раз, когда срабатывает новое событие мыши, мы сохраняем информацию для использования в нашей игре. Для функциональности обзора мышью мы измеряем расстояние, пройденное мышью, и вычисляем подходящий угол камеры путём умножения 90 градусов на число процентов от размера сцены, который прошла мышь.

В любое время в цикле рендеринга нашей игры мы можем видеть, где мышь проверяла нашобъектданных. mouseX и Y, и мы можем выяснить местоположения клика по ссылке в переменных mouseClickX и Y класса. Если нам нужно стрелять пулями каждый раз, когда мышь нажата, мы можем просто проверить свойство mouseIsDown.

Время для действий: обнаруживаем события ввода с клавиатуры.
Наконец, в дополнении к управлению мышью, мы хотим иметь возможность обрабатывать распространённые клавиатурные вводу для нашей игры. Эти процедуры могут быть расширены дополнительными клавишами когда потребуется, но сейчас простые вводы направлений плюс пробел для выстрела это всё, что нам нужно.
private function keyPressed(event:KeyboardEvent):void {
// qwer 81 87 69 82
// asdf 65 83 68 70
// влево вправо 37 39
// вверх вниз 38 40
//trace("keyPressed " + event.keyCode);
switch(event.keyCode)
{
case Keyboard.UP:
case 87: // клавиша W
pressing.up = true;
break;
case Keyboard.DOWN:
case 83: // клавиша S
pressing.down = true;
break;
case Keyboard.LEFT:
case 65: // клавиша A
pressing.left = true;
break;
case Keyboard.RIGHT:
case 68: //клавиша D
pressing.right = true;
break;
case Keyboard.SPACE:
pressing.fire = true; break;
}

Что же произойдёт?
Предыдущая функция keyPressed вызывается Flash всякий раз, когда игрок нажимает клавишу. Если пробел, клавиши стрелок (или W, A, S, D как альтернатива использованию стрелок) нажаты, мы обновляем публичную переменную класса под именем pressing, чтобы отразить это состояние, что наша игра может реагировать на него.

Время для действий: обнаруживаем события отпускания клавиш.
Нам также необходимо выключить флаги ввода, когда игрок отпускает клавиши движения. Мы делаем это следующим образом:
private function keyReleased(event:KeyboardEvent):void {
switch(event.keyCode)
{
case Keyboard.UP:
case 87:
pressing.up = false;
break;
case Keyboard.DOWN:
case 83:
pressing.down = false;
break;
case Keyboard.LEFT:
case 65:
pressing.left = false;
break;
case Keyboard.RIGHT:
case 68:
pressing.right = false;
break;
case Keyboard.SPACE: pressing.fire = false;
break;
}

Что же произойдёт?
Тем же способом, что мы обнаруживаем, когда клавиши нажимаются, мы должны действовать, когда они отпускаются. Этим способом, например, корабль будет двигаться только влево, пока клавиша левой стрелки (или A) удерживается нажатой, и движение будет остановлено, когда она будет отпущена.

Время для действий: обнаруживаем ввод фокуса.
События фокуса вызываются когда пользователь кликает игру (gainFocus) или покидает игру(либо они кликают другую программу, либо начинают обзор другой вкладки). По причинам безопасности, Flash только «слушает» клавиатуру, когда она имеет фокус. В противном случае, Flash мог бы записывать людей, вводящих электронную почту в другой программе.
private function gainFocus(event:Event):void {
trace("Game received keyboard focus.");
}
// если игра потеряла фокус, не держим клавишу нажатой
private function lostFocus(event:Event):void {
trace("Game lost keyboard focus.");
pressing.up = false;
pressing.down = false;
pressing.left = false;
pressing.right = false;
pressing.fire = false;
}
} // конец класса
} // конец пакета

Что же произойдёт?
В предыдущем коде мы обнаруживаем, когда игра получает и теряет фокус клавиатуры. Это может случиться, например, тогда, когда пользователь кликает другую вкладку браузера или сворачивает своё окно браузера. Причина, по которой мы хотим получать уведомления об этих событиях, это то, что когда теряется фокус, события отпускания клавиш не будет срабатывать, и игра продолжит думать, что клавиша нажата.

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

В функции lostFocus мы уверены, что все ключи, как предполагается, «вверх», так что корабль нашего игрока не продолжит стрелять или двигаться, когда игра не контролируется пользователем.

Это всё, что нам нужно, для того, чтобы контролировать нашу игру. Однажды мы обновим нашу игру, чтобы создать объект этого нового класса, тогда мы сможем запрашивать его, чтобы определять в любое время, нажаты клавиши стрелок или нет, и мы также сможем таким же образом обновлять при обзоре мышью ракурс камеры.

Абстрактный класс сущностей.
В стремлении к цели этой главы, следующий шаг это большой папочка из них всех – «битва с боссом». Это также наиболее важная часть любого игрового движка. Готовьтесь испить кружку предпочитаемого вами зелья силы (чашку кофе, например) и приготовьтесь к мега-хардкорному игровому кодингу.

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

Для того чтобы сохранить вещи простыми, мы запрограммируем самые основы класса сущностей, которые предоставляют только простейшую функциональность. Функциональность, которая используется практически во всех возможных игровых объектах, такая как отслеживание их позиции, поворот, масштабирование и рендеринг мешей в нужном месте.

В будущем вы можете действительно пойти в город, добавив всевозможные интересные свойства и функции. Лучшим путём будет создать ваши собственные пользовательские классы, которые получаются из одного, путём создания новых пользовательских классов, использующих код подобно:
public class purpleDragon extends Stage3dEntity
Тогда мы можем добавить свойства и методы специфичные для поведения фиолетового дракона.

В данный момент мы используем основной класс, чтобы управлять объектами в нашем игровом мире, так как с этого момента, всё, что мы хотим в нашем игровом мире, будет иметь только преобразование (позиция, поворот, и т.д.) и отдельный меш, который рендерится, используя особый шейдер.

Для того, чтобы создать новый класс, который инкапсулирует виды внутриигровых сущностей, как описано ранее, мы должны выполнить следующие основные шаги:
Создать новый класс с именем Stage3dEntity.
Определить переменные для всех частей преобразований, таких как меши, текстуры и так далее.
Запрограммировать конструктор класса, инициализирующий функцию, которая записывает Stage3D-специфичные значения по мере необходимости.
Запрограммировать get и set-функции для обновления частей программы.
Создать функции, которые обновляют трансформации или значения, когда другие изменяются.
Разработать несколько удобных служебных функций, для выполнения для нас некоторых математических матриц.
Запрограммировать механизм от одной сущности к другой автоматически.
Реализовать функцию клонирования, что позволит использовать один и тот же меш во многих местах.
Запрограммировать функцию рендеринга, которая рисует меш, используя Stage3D.

Время для действий: создаём класс Stage3dEntity.
Начнём с создания нового файла с именем Stage3dEntity.as в вашей папке проекта, определим свойства и импортируем для нашего нового класса следующее:
//класс игровых сущностей Stage3d, версия 1.3
//адаптирован благодаря работе Алехандро Сантандера
package {
import com.adobe.utils.*;
import flash.display.Stage3D;
import flash.display3D.Context3D;
import flash.display3D.Context3DProgramType;
import flash.display3D.Context3DTriangleFace;
import flash.display3D.Context3DVertexBufferFormat;
import flash.display3D.IndexBuffer3D;
import flash.display3D.Program3D;
import flash.display3D.VertexBuffer3D;
import flash.display3D.*;
import flash.display3D.textures.*;
import flash.geom.Matrix;
import flash.geom.Matrix3D;
import flash.geom.Vector3D;

Предыдущий код просто импортирует функциональность, которая потребуется нашему классу сущностей. Мы будем делать много ы будем делать много 3D матриц и векторных операций, так как мы должны затем определить некоторые переменные класса, чтобы сохранять все виды установок положения, поворота и масштабирования, также как различные Stage3D объекты, с которыми мы будем взаимодействовать следующим образом:
public class Stage3dEntity {
// переменные матрицы (позиция, поворот, и т.д.)
private var transform:Matrix3D;
private var inverseTransform:Matrix3D;
private var transformNeedsUpdate:Boolean;
private var valuesNeedUpdate:Boolean;
private var x:Number = 0;
private var y:Number = 0;
private var z:Number = 0;
private var rotationDegreesX:Number = 0;
private var _rotationDegreesY:Number = 0;
private var rotationDegreesZ:Number = 0;
private var scaleX:Number = 1;
private var scaleY:Number = 1;
private var scaleZ:Number = 1;
private const RAD TO DEG:Number = 180/Math
// Stage3d объекты(публичные, так как могут быть унаследованы)
public var context:Context3D;
public var vertexBuffer:VertexBuffer3D;
public var indexBuffer:IndexBuffer3D;
public var shader:Program3D;
public var texture:Texture;
public var mesh:Stage3dObjParser;
// режимы рендеринга:
public var cullingMode:String = Context3DTriangleFace.FRONT;
public var blendSrc:String = Context3DBlendFactor.ONE;
public var blendDst:String = Context3DBlendFactor.ZERO;
public var depthTestMode:String = Context3DCompareMode.LESS;
public var depthTest:Boolean = true;
public var depthDraw:Boolean = true;
// используются только для установки
public var polycount:uint = 0;
// если этот параметр установлен, сущность «застряла» в другой
public var following:Stage3dEntity;
// оптимизируем, чтобы данные, которые нам нужны, отправлялись в Stage3D
// это зависит от того, что требуют шейдеры
public var shaderUsesUV:Boolean = true;
public var shaderUsesRgba:Boolean = true;
public var shaderUsesNormals:Boolean = false;

Что же произойдёт?
В предыдущем коде мы импортировали функциональность Stage3D, которая нам будет нужна, и затем определили кучу удобных переменных. Публичные только состояния рендеринга ( так что мы можем изменять их на лету в любое время в нашем цикле рендеринга), в то время как остальные будут, для многих частей, управляться get- и set-функциями, и следовательно, лучше оставить их приватными переменными, недоступными коду вне этого класса.

Время для действий: создаём конструктор класса Stage3dEntity.
Мы начнём с конструктора нашего класса и затем реализуем все эти матричные математические алгоритмы - «помощники». Однажды запрограммированные, они будут делать вашу жизнь настолько проще, что вы будете рады, что встретили их на своём пути.
//Конструктор класса
public function Stage3dEntity( mydata:Class = null, mycontext:Context3D = null, myshader:Program3D = null, mytexture:Texture = null, modelscale:Number = 1, flipAxis:Boolean = true, flipTexture: Boolean = true)
{
_transform = new Matrix3D();
context = mycontext;
shader = myshader;
texture = mytexture;
if (mydata && context)
{
mesh = new Stage3dObjParser(
mydata, context, modelscale, flipAxis, flipTexture);
polycount = mesh.indexBufferCount;
trace("Mesh has " + polycount + " polygons.");
}
}

Что же произойдёт?
В короткой функции-конструкторе выше мы сохраняем ссылки на Context3D игры, какой бы шейдер или текстуру наша игра ни захотела использовать для этой сущности. Мы также парсим меш, используя класс Stage3dObjParser, который мы запрограммировали в предыдущей главе.

Скрываем сложный код, используя get- и set-функции.
get- и set-функции позволяют классу перехватывать изменения свойств. Они очень удобны тем, что однажды определённые, они «действуют» прямо как обычные публичные переменные. Например, мы запрограммируем get- и set-функцию для «x», которая будет автоматически обновлять трансформацию нашего объекта Matrix3D.

В нашем игровом коде мы сможем изменять x координату игровой сущности, просто устанавливая, например, myEntity.x = 10. Внутри изменение значения x будет вызывать функцию "set x", так что мы сможем обновлять и другие приватные переменные, когда это необходимо.

Причина, по которой мы делаем это таким образом это уберечь себя от необходимости выполнять все виды сложнейших и нелогичных математических матриц, когда мы хотим сделать что-то вроде вращающейся модели. Чтобы не пришлось добавлять векторы, матрицы и т.п., ваш игровой код будет чище и проще для отладки, если «спрятано» всё математическое и делает тяжёлую работу за вас под капотом.

Время для действий: получение и установка преобразования.
Помните, что объект «преобразования» это слово, используемое для описания Matrix3D, который хранит настройки позиции, поворота и масштаба. Когда мы запрограммируем возможность изменять части преобразований без необходимости возиться с нелогичными математическими матрицами, мы должны сделать функцию, а не публичную переменную, т.к. мы сможем выполнять дополнительный код перед возвращением значения.
public function get transform():Matrix3D {
if(_transformNeedsUpdate)
updateTransformFromValues();
return _transform;
}
public function set transform(value:Matrix3D):void {
_transform = value;
_transformNeedsUpdate = false;
_valuesNeedUpdate = true;
}

Что же произойдёт?
В предыдущем коде мы реализовали способ устанавливать преобразование напрямую, таким образом, мы сможем просто отправлять полностью сконфигурированный Matrix3D прямиком в наши сущности. Get-функции необходимо сначала определить, содержит ли приватная переменная самые последние данные: если раньше мы установили только один компонент нашего преобразования, такой как координата x, мы установим _transformNeedsUpdate в true, так наш класс будет знать, что преобразование нуждается в перерасчёте, чтобы отразить самые последние изменения.

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

Что такое «грязный» флаг?
Переменная, которая устанавливается в true, чтобы указать, когда что-то ещё недавно изменилось, и требуется обновление других вещей. Говорится, что речь идёт о «грязном» деле и требуется обновление, которое должно быть выполнено в текущий момент. Это технология оптимизации, которая помогает обновлениям стоять в очереди до следующего раза, когда они потребуются, а не выполнять обновление чрезмерно часто. Если вы хотите, чтобы можно было использовать различные прилагательные, такие как isClean, тогда флаг должен сохранять противоположное значение, но иметь такой же эффект. Главное помнить, что мы записываем, и нет ли дорогих вычислений, которые должны быть запущены, так как оптимизации направлены на повышение производительности, путём избегания выполнения трудоёмкого кода чаще, чем необходимо.

В текущем примере эта техника создания преобразований игровых сущностей больше не экономит на ненужных вычислениях, ожидая до последней минуты, чтобы создать новую матрицу. Представьте, установлено mySpaceship.x=1 и mySpaceship.z = 2 и т.д. Мы не хотим обновлять TransformFromValues() , чтобы запускать каждый раз, когда мы изменяем какое-нибудь из многих значений, так как мы будем часто менять некоторые, обновляемые по необходимости, преобразования, когда приходит время для рендера. Более эффективно запускать эту функцию только один раз, когда мы следующий раз запрашиваем преобразования.

Время для действий: получаем и задаём позицию сущности.
Начнём с get- и set-функций просто для установки x, y, z позиции преобразований частей нашей сущности. Этим способом мы можем изменить местоположение чего-нибудь, не беспокоясь о повороте и масштабе.
public function set position(value:Vector3D):void {
_x = value.x;
_y = value.y;
_z = value.z;
_transformNeedsUpdate = true;
}
private var _posvec:Vector3D = new Vector3D();
public function get position():Vector3D {
if(_valuesNeedUpdate)
updateValuesFromTransform();
// оптимизация: избегаем создания временных переменных
// например return new Vector3D(_x, _y, _z);
_posvec.setTo(_x, _y, _z); return _posvec;
}
public function set x(value:Number):void {
_x = value;
_transformNeedsUpdate = true;
}
public function get x():Number {
if(_valuesNeedUpdate)
updateValuesFromTransform();
return _x;
}
public function set y(value:Number):void {
_y = value;
_transformNeedsUpdate = true;
}
public function get y():Number {
if(_valuesNeedUpdate)
updateValuesFromTransform();
return _y;
}
public function set z(value:Number):void {
_z = value;
_transformNeedsUpdate = true;
public function get z():Number {
if(_valuesNeedUpdate)
updateValuesFromTransform( return _z;
}

Что же произойдёт?
Мы просто написали серию функций, которые будут вызываться автоматически, всякий раз, когда наша игра устанавливает или получает переменную класса. Например, наша игра сможет теперь исполнять строчки кода, такие как spaceship.z = 10; и за кулисами устанавливать z будет вызывающаяся функция (обновляя приватную переменную z и делая преобразование космического корабля «грязным»), так что следующий раз мы будем иметь доступ к нему, z-компонента матрицы трансформации сущности также будет 10. И наоборот, если мы запустим код наподобие temp=spaceship.z;, получающая z функция будет, работая, возвращать приватную переменную _z, необязательно один раз извлекая её из «грязных» преобразований, если такие имеются.

Время для действий: получение и установка поворота сущности.
Эти get- и set-функции все практически идентичные. Они обновляют или поворачивают отдельный компонент матрицы трансформации вашего игрового объекта.
public function set rotationDegreesX(value:Number):void {
_rotationDegreesX = value;
_transformNeedsUpdate = true;
}
public function get rotationDegreesX():Number
if(_valuesNeedUpdate)
updateValuesFromTransform();
return _rotationDegreesX;
public function set rotationDegreesY(value:Number):void {
_rotationDegreesY = value;
_transformNeedsUpdate = true;
}
public function get rotationDegreesY():Number
if(_valuesNeedUpdate)
updateValuesFromTransform();
return _rotationDegreesY;
public function set rotationDegreesZ(value:Number):void {
_rotationDegreesZ = value;
_transformNeedsUpdate = true;
}
public function get rotationDegreesZ():Number {
if(_valuesNeedUpdate)
updateValuesFromTransform();
return _rotationDegreesZ;
}

Время для действий: получение и установка масштаба сущности.
Прямо как и с поворотами в предыдущем коде, следующие get- и set-функции выполняют масштабирование и не нуждаются в разъяснении:
public function set scale(vec:Vector3D):void {
_scaleX = vec.x;
_scaleY = vec.y;
_scaleZ = vec.z;
_transformNeedsUpdate = true;
}
private var _scalevec:Vector3D = new Vector3D();
public function get scale():Vector3D {
if(_valuesNeedUpdate)
updateValuesFromTransform();
//return new Vector3D(_scaleX, _scaleY, _scaleZ, 1.0);
// оптимизация: избегаем создания временных переменных
_scalevec.setTo(_scaleX, _scaleX, _scaleZ);
_scalevec.w = 1.0; return _scalevec;
}
public function set scaleXYZ(value:Number):void {
_scaleX = value;
_scaleY = value;
_scaleZ = value;
_transformNeedsUpdate = true;
}
public function get scaleXYZ():Number {
if(_valuesNeedUpdate)
updateValuesFromTransform();
return _scaleX; // невозможно определить
_transformNeedsUpdate = true;
}

Предыдущие функции относятся ко всему масштабу, который имеет x, y и z компоненты. Возможно, вы захотите работать только с одним из них; например, сделать что-нибудь толще, но не выше. Мы создадим все геттеры и сеттеры, чтобы позволить вам сделать такое с лёгкостью.
public function set scaleX(value:Number):void {
_scaleX = value;
_transformNeedsUpdate = true;
}
public function get scaleX():Number {
if(_valuesNeedUpdate)
updateValuesFromTransform();
return _scaleX;
}
public function set scaleY(value:Number):void {
_scaleY = value;
_transformNeedsUpdate = true;
}
public function get scaleY():Number {
if(_valuesNeedUpdate)
updateValuesFromTransform();
return _scaleY;
}
public function set scaleZ(value:Number):void {
_scaleZ = value;
_transformNeedsUpdate = true;
}
public function get scaleZ():Number {
if(_valuesNeedUpdate)
updateValuesFromTransform();
return _scaleZ;
}

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

Это то место, где происходит волшебство, касающееся обработки get/set-функций. Всякий раз, когда мы изменяем, например, x координату, преобразования должны быть восстановлены при очередном обращении к ней (например, перед рендерингом).
Обратное также справедливо: если мы имеем недавние изменения только матрицы трансформации, следующий раз мы запросим только x координату, это должно будет быть «вытаскиванием» трансформации.

Время для действий: обновляем преобразования или значения по вызову.
Эта техника «обновлять только когда требуется» сохраняет миллионы вычислений в ходе игры и поможет сохранить вашу частоту кадров высокой. Сейчас мы создадим эти две функции.
public function updateTransformFromValues():void {
_transform.identity();
_transform.appendRotation(
_rotationDegreesX, Vector3D.X_AXIS);
_transform.appendRotation(
_rotationDegreesY, Vector3D.Y_AXIS);
_transform.appendRotation(
_rotationDegreesZ, Vector3D.Z_AXIS);
// избегаем матричной ошибки #2183:
// значения масштаба не должны быть нулевыми
if (_scaleX == 0) _scaleX = 0.0000001;
if (_scaleY == 0) _scaleY = 0.0000001;
if (_scaleZ == 0) _scaleZ = 0.0000001;
_transform.appendScale(_scaleX, _scaleY, _scaleZ);
_transform.appendTranslation(_x, _y, _z);
_transformNeedsUpdate = false;
}
public function updateValuesFromTransform():void {
var d:Vector.<Vector3D> = _transform.decompose();
var position:Vector3D = d[0];
_x = position.x;
_y = position.y;
_z = position.z;
var rotation:Vector3D = d[1];
_rotationDegreesX = rotation.x*RAD_TO_DEG;
_rotationDegreesY = rotation.y*RAD_TO_DEG;
_rotationDegreesZ = rotation.z*RAD_TO_DEG;
var scale:Vector3D = d[2];
_scaleX = scale.x;
_scaleY = scale.y;
_scaleZ = scale.z;
_valuesNeedUpdate = false;
}

Что же произойдёт?
Две предыдущие функции запускаются всякий раз, когда get- или set-функция запрашивает данные, чтобы обновиться в первую очередь. Если ваша игра должна была установить преобразования вашей сущности, например, флаг _valuesNeedUpdate должен быть установлен в true. Если бы, в последствии, вы должны были получить только x позицию этой сущности, get-функция из предыдущего кода увидела бы, что текущее внутреннее значение для x в первую очередь нуждается в обновлении, запустив функцию updateValuesFromTransform(). Если мы, сразу же после этого, разрешим y компоненту, тогда значения будут всё ещё оставаться текущими, и грязный флаг будет false, так как функция установки y сможет быстро вернуть ответ без запуска функции обновления во второй раз.

Время для действий: создаём вспомогательную функцию движения.
Следующие функции это полезные утилиты, которые используются при программировании игр очень часто. Иногда мы хотим получить вектор, который показывает, в каком направлении повёрнута сущность. Например, вы можете ориентировать ракету в направлении 45 градусов влево и 5 градусов вверх. Тогда мы могли бы быстро спросить её, какой у неё «фронтальный вектор», и этот класс вернул бы направление, в котором сущность повёрнута. Этот вектор мог бы перемножаться со значением скорости, чтобы двигать ракету «вперёд». Также как и с многими алгоритмами, представленными выше, эти функции помогут вам избежать работы со сложными математическими Matrix3D в вашей игре.
//Утилиты движения:
//движемся в соответствии с направлением, в котором мы повёрнуты:
public function moveForward(amt:Number):void {
if (_transformNeedsUpdate) updateTransformFromValues();
var v:Vector3D = frontvector;
v.scaleBy(-amt)
transform.appendTranslation(v.x, v.y, v.z);
_valuesNeedUpdate = true;
}
public function moveBackward(amt:Number):void {
if (_transformNeedsUpdate) updateTransformFromValues();
var v:Vector3D = backvector;
v.scaleBy(-amt)
transform.appendTranslation(v.x, v.y, v.z);
_valuesNeedUpdate = true;
}
public function moveUp(amt:Number):void {
if (_transformNeedsUpdate) updateTransformFromValues();
var v:Vector3D = upvector;
v.scaleBy(amt)
transform.appendTranslation(v.x, v.y, v.z);
_valuesNeedUpdate = true;
}
public function moveDown(amt:Number):void {
if (_transformNeedsUpdate) updateTransformFromValues();
var v:Vector3D = downvector;
v.scaleBy(amt)
transform.appendTranslation(v.x, v.y, v.z);
_valuesNeedUpdate = true;
}
public function moveLeft(amt:Number):void {
if (_transformNeedsUpdate) updateTransformFromValues();
var v:Vector3D = leftvector;
v.scaleBy(amt)
transform.appendTranslation(v.x, v.y, v.z);
_valuesNeedUpdate = true;
}
public function moveRight(amt:Number):void {
if (_transformNeedsUpdate) updateTransformFromValues();
var v:Vector3D = rightvector;
v.scaleBy(amt)
transform.appendTranslation(v.x, v.y, v.z);
_valuesNeedUpdate = true;
}

Что же произойдёт?
Функции движения выше это ярлыки, чтобы помогать делать работу с игровыми сущностями нагляднее. Вместо того, чтобы выполнять всевозможные сложные математические операции с синусом, косинусом точкой и векторами, просто сдвигаем ракету в каком бы направлении она не была повёрнута в настоящее время, вы можете просто запустить функцию moveForward.

Время для действий: реализуем вспомогательную функцию вектора.
Для того, чтобы определить, например, что вектор впереди или справа от ракеты, мы должны вычислить его в зависимости от направления её поворота.
//оптимизация: эти векторы определяются как константы
//чтобы избежать создания временных переменных каждый кадр
private static const vecft:Vector3D = new Vector3D(0, 0, 1);
private static const vecbk:Vector3D = new Vector3D(0, 0, -1);
private static const veclf:Vector3D = new Vector3D(-1, 0, 0);
private static const vecrt:Vector3D = new Vector3D(1, 0, 0);
private static const vecup:Vector3D = new Vector3D(0, 1, 0);
private static const vecdn:Vector3D = new Vector3D(0, -1, 0);
public function get frontvector():Vector3D {
if(_transformNeedsUpdate)
updateTransformFromValues();
return transform.deltaTransformVector(vecft);
}
public function get backvector():Vector3D {
if(_transformNeedsUpdate)
updateTransformFromValues();
return transform.deltaTransformVector(vecbk);
}
public function get leftvector():Vector3D {
if(_transformNeedsUpdate)
updateTransformFromValues();
return transform.deltaTransformVector(veclf);
}
public function get rightvector():Vector3D {
if(_transformNeedsUpdate)
updateTransformFromValues();
return transform.deltaTransformVector(vecrt);
}
public function get upvector():Vector3D {
if(_transformNeedsUpdate)
updateTransformFromValues();
return transform.deltaTransformVector(vecup);
}
public function get downvector():Vector3D {
if(_transformNeedsUpdate)
updateTransformFromValues();
return transform.deltaTransformVector(vecdn);
}
Предыдущая функция это быстрый способ спросить сущность, например, «какой вектор у тебя слева?», не рассчитывая этого вручную. Так как каждая сущность может быть повёрнута в совершенно различном направлении, «лево» это относительно угла, на который она повёрнута.

Время для действий: добавляем несколько удобных вспомогательных функций сущностей.
Наконец, без тщательных объяснений, установим здесь удобные вспомогательные функции, которые могут быть полезными в дальнейшем.
Некоторые полезны для отладки, тогда как другие – просто ярлыки для общеигровых задач. Там может быть несколько процедур, которые вы никогда не должны использовать, но они включены, чтобы сохранить ваше время и усилия, если они вам когда-нибудь понадобятся.
//Удобные Утилиты
public function get rotationTransform():Matrix3D {
var d:Vector.<Vector3D> = transform.decompose();
d[0] = new Vector3D();
d[1] = new Vector3D(1, 1, 1);
var t:Matrix3D = new Matrix3D();
t.recompose(d);
return t;
}
public function get reducedTransform():Matrix3D {
var raw:Vector.<Number> = transform.rawData;
raw[3] = 0; // Remove translation.
raw[7] = 0;
raw[11] = 0;
raw[15] = 1;
raw[12] = 0;
raw[13] = 0;
raw[14] = 0;
var reducedTransform:Matrix3D = new Matrix3D();
reducedTransform.copyRawDataFrom(raw);
return reducedTransform;
}
public function get invRotationTransform():Matrix3D {
var t:Matrix3D = rotationTransform;
t.invert();
return t;
}
public function get inverseTransform():Matrix3D {
inverseTransform = transform.clone();
inverseTransform.invert();
return _inverseTransform;
}

Предыдущие функции используются редко, но могут пригодиться, если вы захотите преобразовать матрицу сущностей, которая уже лишена трансляции (данные о местоположении) или если вам нужно инвертировать повороты, например, для «отражения» или «освящения» шейдера.
public function get positionVector():Vector.<Number>
{
return Vector.<Number>([_x, _y, _z, 1.0]);
}
public function posString():String {
if (_valuesNeedUpdate)
updateValuesFromTransform();
return _x.toFixed(2) + ','
+ _y.toFixed(2) + ','
+ _z.toFixed(2);
}
public function rotString():String {
if (_valuesNeedUpdate)
updateValuesFromTransform();
return _rotationDegreesX.toFixed(2) + ','
+ _rotationDegreesY.toFixed(2) + ','
+ _rotationDegreesZ.toFixed(2);
}
public function follow(thisentity:Stage3dEntity):void {
following = thisentity;
}

Что же произойдёт?
Наряду со специализированными функциями, возвращающими модифицированные преобразования для специальных нужд, последние две функции в предыдущем коде это прекрасно подходят для отладки (или написания уровней!), потому что они, всего лишь, возвращают текстовую версию позиции и поворота сущности. Они могут быть использованы только для отладки вызовом функции trace(), чтобы проверить, если что-то не в том месте, что вы ожидали. Наконец, мы сделали простую функцию, которая сохраняет путь «родительской» сущности, так чтобы что-нибудь могло точно следовать за чем-то ещё.

Было бы действительно удобно иметь возможность «склеить» сущности вместе таким способом. Например, вы можете захотеть присоединить пушку с модели космического корабля, или шляпу на аватар, или спецэффект подобно «свечению» фонарного столба. Сделать так очень просто, и эта функция позволяет нашей игре сообщать нашим сущностям, что она должна следовать за другой сущностью. Когда она визуализируется, она добавляет преобразование сущности согласно своей собственной. Этим способом мы можем установить локальные x, y или z координаты, которые относятся к сущностям, за которыми мы следуем. Мы сможем подтолкнуть пушку или свечение к правильному местоположению относительно своего «родителя», и когда этот родитель будет перемещаться или поворачиваться, последуют туда же. В данный момент функция follow() просто устанавливает переменные, но для определённой игры вы, возможно, захотите добавить сюда больше строк кода, чтобы играла музыка или обновлялись очки, когда что-нибудь , например, добавляется игроку.

То есть польза всех этих удобных-приудобных матриц и векторов в функциональности скрытого разнообразия. Последняя вспомогательная функция, которую мы напишем, это та, которую вы обязательно будете часто использовать в ваших играх.
Это функция клонирования. Сегодня в играх клоны очень распространены. Например, в играх следующего поколения, которые имеют обширные пышные леса для исследования, есть только несколько уникальных моделей деревьев, и каждое клонируется множество раз, чтобы конкретизировать сцену. После всего этого, вы, возможно, не захотите ваять бесконечное число деревьев, дверей, скал или пуль, когда вы могли бы вместо этого повторно использовать эти модели во множестве локаций.

Использование клонов моделей необходимо, чтобы сохранить размер вашего файла небольшим и ваш труд по созданию уровня будет управляемым. Это также имеет смысл, чтобы иметь возможность разместить аналогичные ресурсы в нескольких местах в вашем игровом мире без парсинга мешей сущностей .OBJ или создания уникального вершинного буфера Stage3D для каждого раза. Это ускоряет время инициализации, снижает потребление RAM и позволяет гигантским сценам включать сотни тысяч полигонов.
Ещё одной большой новостью является то, что можно клонировать меш, но поместить его несколько в другой ориентации или дать ему другой масштаб, чтобы он выглядел отличным от других в вашей сцене.

Время для действий: клонируем сущности.
Когда мы хотим клонировать меш, мы можем использовать эту функцию вместо создания новой Stage3d сущности каждый раз.
//создаём точный дубликат в игровом мире
//повторно используем все Stage3d объекты
public function clone():Stage3dEntity {
if(_transformNeedsUpdate)
updateTransformFromValues();
var myclone:Stage3dEntity = new Stage3dEntity();
myclone.transform = this.transform.clone();
myclone.mesh = this.mesh;
myclone.texture = this.texture;
myclone.shader = this.shader;
myclone.vertexBuffer = this.vertexBuffer;
myclone.indexBuffer = this.indexBuffer;
myclone.context = this.context;
myclone.polycount = this.polycount;
myclone.shaderUsesNormals = this.shaderUsesNormals;
myclone.shaderUsesRgba = this.shaderUsesRgba;
myclone.shaderUsesUV = this.shaderUsesUV;
myclone.updateValuesFromTransform();
return myclone;
}

Что же произойдёт?
Функция clone() в предыдущем коде возвращает новую Stage3d сущность, которая является копией другой сущности, которая была инициализирована ранее. В этом способе только одна копия вершинного буфера Stage3D, текстуры или шейдера, нуждающегося в визуализации нескольких копий такого же вида объекта.
Самый последний шаг в создании нашего абстрактного класса сущностей это функция визуализации. Это то место, где мы инструктируем Stage3D как рисовать вашу модель на экране в правильном местоположении и с правильными текстурами и шейдерами.

Время для действий: визуализируем сущности.
Эта функция будет вызываться для каждого отдельного кадра в вашем игровом цикле рендеринга, каждый раз, когда камера перемещается. Поэтому мы должны генерировать правильную матрицу, чтобы отправлять во Flash, чтобы инкапсулировать преобразования сущностей, матрицы обзора камеры, плюс матрицу проекции (которая определяет пропорции сторон, область обзора, а также ближний и дальний план для нашей камеры).
//оптимизация: повторно используем такую же временную матрицу
private var _rendermatrix:Matrix3D = new Matrix3D();
В целях оптимизации эта матрица является частной временной переменной, чтобы, будучи созданной, однажды, за пределами этой функции, избегать выделения RAM во время рендеринга. Так как она будет вызываться миллионы раз во время игры, она может помочь избежать проблем, связанных с частотой кадров, когда GC (сборщик мусора), когда придёт время, решит очистить память, устранив неиспользуемые переменные.
//визуализируем сущность, изменяем состояние, только если требуется
public function render( view:Matrix3D, projection:Matrix3D, statechanged:Boolean = true):void
{
// используется только для отладки:
if (!mesh) trace("Missing mesh!");
if (!context) trace("Missing context!");
if (!shader) trace("Missing shader!");
// только визуализируем, если установлено
if (!mesh) return;
if (!context) return;
if (!shader) return;
//перезагружаем матрицу
_rendermatrix.identity();
_rendermatrix.append(transform);
if (following) _rendermatrix.append(following.transform);

Предыдущий код проверяет, чтобы убедиться, что меш, Context3D и шейдер установлены, и существует функция, если любое из этих данных потерян. Это может случиться, например, когда класс сущностей используется, чтобы инкапсулировать невидимые вспомогательные сущности, которые не должны быть нарисованы. Затем создаётся матрица трансформации текущей сущности. Если мы, бывает, находимся за другой сущностью, тогда мы также добавляем трансформацию этой сущности к нашей собственной.
_rendermatrix.append(view);
_rendermatrix.append(projection);
// Устанавливаем регистр нашей вершинной программы vc0 в нашу
// модель обзора projection matrix = vc0
context.setProgramConstantsFromMatrix(
Context3DProgramType.VERTEX, 0, _rendermatrix, true);
// оптимизация: изменяем только состояние рендеринга
// если до этого действие визуализации
// не использовало идентичный меш/шейдер как текущий
if (statechanged)
{

Отслеживая изменилось ли состояние, наш игровой движок сможет рисовать группы таких же мешей быстрее, потому что меш, шейдер, режимы наложения и текстуры остаются такими же. Если мы не должны инструктировать Stage3D обновить состояния рендеринга, тогда мы устраняем многочисленные бесполезные изменения состояний и существенно увеличиваем производительность. Превосходным примером здесь был бы рендеринг группы 100 идентичных деревьев. Матрица трансформации для каждого из них продолжает обновляться выше, но каждое дерево разделяет такую же геометрию и текстуру, так что мы можем рендерить группой их все, только визуализируя первое дерево, установив параметр statechanged на true.
//Устанавливаем программу AGAL
context.setProgram(shader);
//Устанавливаем в регистр ts0 фрагментной программы текстуру
if (texture) context.setTextureAt(0,texture);
//позиция
context.setVertexBufferAt(0, mesh.positionsBuffer,
0, Context3DVertexBufferFormat.FLOAT_3);
// текстурные координаты
if (shaderUsesUV)
context.setVertexBufferAt(1, mesh.uvBuffer,
0, Context3DVertexBufferFormat.FLOAT_2);
// rgba вершины
if (shaderUsesRgba)
context.setVertexBufferAt(2, mesh.colorsBuffer,
0, Context3DVertexBufferFormat.FLOAT_4);
// нормаль вершины
if (shaderUsesNormals)
context.setVertexBufferAt(3, mesh.normalsBuffer,
0, Context3DVertexBufferFormat.FLOAT_3);
context.setBlendFactors(blendSrc, blendDst);
context.setDepthTest(depthTest,depthTestMode);
context.setCulling(cullingMode);
context.setColorMask(true, true, true, depthDraw);
}

Наконец, мы просто инструктируем Flash какой шейдер, текстура, состояние рендеринга (прозрачность и режим наложения), а также вершинный и индексный буфер мы используем (если необходимо), и мы готовы визуализировать треугольники, которые составляют наш меш.
//рендерим его
context.drawTriangles(mesh.indexBuffer,
0, mesh.indexBufferCount);
}
} // конец класса
} // конец пакета

Что же произойдёт?
Предыдущая функция рисует сущности. Она обычно вызывается каждый кадр. Реализуются несколько оптимизаций , которые несомненно увеличивают вашу частоту кадров.

Это самая тяжелая часть в создании нашего интерактивного, анимированного игрового мира! Наш фантастический и в высшей степени многоразовый абстрактный класс игровых сущностей станет просто необходим в нашем непрерывном квесте по созданию великолепной 3D флеш игры. Вы будете использовать его для пуль, космических кораблей, машин, деревьев, взрывов и практически чего угодно, что ещё может появиться в вашей игре.

Самый последний шаг это объединить все эти новые функциональности в вашем основном игровом скрипте! Прежде, чем мы сделаем это, давайте возьмём передышку и повеселимся, создав больше артов, чтобы поместить их в нашу игру.
Проектируем арты для нашего усовершенствованного игрового мира.

Так как эта книга целиком о программировании, мы не собираемся вникать в то, как создавать меши в программах, таких как 3D Studio Max или Blender, не будем мы вникать и в технологию создания крутых текстур в Photoshop. Тем не менее, просто для развлечения, здесь есть ресурсы, которые мы добавим в наш игровой мир. Пошалите и немного развлекитесь, создав свои собственные.

Во-первых, как пример игры для этой книги, это перестрелять всех в космосе, что может быть лучшем местом, чтобы летать, чем поле астероидов? Используя 3ds Max, можно сделать несколько простых форм и экспортировать в .OBJ формат, создав сферы и сплющив их, чтобы они выглядели подобно астероидам.

Теперь мы должны сделать их похожими на астероиды, создав текстуры для них. Соответствующие покрытые кратерами текстуры астероидов были созданы в Photoshop. В этом примере были использованы реальные фотографии луны с публичного домена NASA, чтобы создать бесшовное изображение, которым можно покрыть 3D меш. Оно было сделано бесшовным с помощью свободного для использования штамповочного инструмента, чтобы сделать края обёртки привлекательными.

Текстура частиц была сделана так, чтобы выглядеть очень круто при рендеринге с режимами наложения, которые аддитивно освещают экран (ONE, ONE). При использовании модели "puff.obj" из предыдущей главы, светящийся огненный шар идеально подходит для использования как ракетный двигатель нашего корабля и готов быть прикреплённым к задней части корабля наших игроков в игре.

Наконец, до сих пор наше игровое демо имело уродливый простой чёрный фон. В каждом кадре, когда Stage3D очищается, он заполняется чистым чёрным цветом и эта пустота видна позади всех наших моделей. Давайте обновим нашу игровую вселенную, включив в неё «небесную» сферу. Обернём привлекательной картинкой тумана со звёздами в нём модель "sphere.obj" из предыдущей главы и затем отмасштабировав эту модель до гигантских размеров, мы можем поместить её в наш игровой мир.

Обновляем нашу игру.
На данный момент мы запрограммировали класс таймера, класс ввода игрока и класс игровых сущностей. Мы создали привлекательные изображения для нашей игры. Последний шаг – это объединить всё эти новые вещи в нашей игре, отредактировав файл Stage3dGame.as. Прежде всего, мы сделаем так, чтобы избежать путаницы в именах файлов или местоположениях, вот так должна выглядеть сейчас папка вашего проекта:

Убедитесь, что ваш проект соответствует папке на предыдущем рисунке, так что изменения, приведённые ниже, заработали. Естественно, если вы следовали образцу проекта этой книги, но делали другую игру, вы должны будете сделать незначительные изменения в следующем коде. В любом случае, основные изменения между этой главой и прошлой это удаление неиспользуемых текстовых полей, управления клавиатурой и связанного кода демонстрации «режимов наложения». Вместо парсинга моделей и рендеринга сцены прямо в этом файле, весь код, связанный с вводом, таймерам и моделями, теперь выполняется в новом файле, который мы сейчас создадим.

Время для действий: импортируем наши новые классы.
Stage3dGame.as начинается практически также как и раньше, за исключением того, что мы импортируем наши новые классы:
//
//Шаблон Stage3D игры – Глава Семь//
package
{
[SWF(width="640", height="4 80", frameRate="6 0", backgroundColor="#000000")]
import com.adobe.utils.*;
import flash.display.*;
import flash.display3D.*;
import flash.display3D.textures.*;
import flash.events.*;
import flash.geom.*;
import flash.utils.*;
import flash.text.*;
import Stage3dEntity;
import GameTimer; import GameInput;

Что же произойдёт?

Предыдущие изменения в примере нашей игры незначительны и включаются здесь только для полноты. Три класса, которые мы создали в этой главе, теперь являются основной базой вашего кода. Если вы используете собственные имена пакетов и имеете другую структуру папок, чем те, что в предыдущем примере изображения, тогда добавьте префиксы именам ваших пакетов, чтобы имена ваших классов были соответствующими.

Время для действий: добавляем новые переменные в нашу игру.
Теперь мы добавим несколько новых переменных, и удалим другие, которые больше не используются. Так как наша игра, в конечном счёте, будет заполнена сотнями объектов, включая взрывы врагов и на расстоянии, смысл в том, чтобы сохранить их все в списке, который мы можем циклически выполнять и манипулировать им. Поэтому давайте создадим массив Vector в Stage3dEntities для нескольких общих типов игровых объектов, таких чтобы, в конечном счёте, мы могли воспроизводить и отрисовывать различные виды объектов, подобных сотням пуль, без необходимости использовать сотни индивидуальных переменных.
public class Stage3dGame extends Sprite {
// управления всеми нашими таймерами
private var gametimer:GameTimer;
// управление вводами с мыши и клавиатуры
private var gameinput:GameInput;
// все известные сущности в мире
private var chaseCamera:Stage3dEntity ;
private var player:Stage3dEntity;
private var props:Vector.<Stage3dEntity>;
private var enemies:Vector.<Stage3dEntity>;
private var bullets:Vector.<Stage3dEntity>;
private var particles:Vector.<Stage3dEntity>;
// повторно используемый указатель сущности (для скорости и чтобы избежать GC)
private var entity:Stage3dEntity;
// мы хотим запомнить это для использования в gameStep()
private var asteroids1:Stage3dEntity;
private var asteroids2:Stage3dEntity;
private var asteroids3:Stage3dEntity;
private var asteroids4:Stage3dEntity;
private var engineGlow:Stage3dEntity;
private var sky:Stage3dEntity;
// используется gameStep()
private const moveSpeed:Number = 1.0;
// единиц на мс
private const asteroidRotationSpeed:Number = 0.001;
// град на мс
// используется GUI
private var fpsLast:uint = getTimer();
private var fpsTicks:uint = 0;
private var fpsTf:TextField;
private var scoreTf:TextField;
private var score:uint = 0;
// окно 3d графики на сцене
private var context3D:Context3D;
// скомпилированный шейдер используется, чтобы отрисовать наши меши
private var shaderProgram1:Program3D;
// матрицы, которые воздействуют на положение меша и углы камеры
private var projectionmatrix:PerspectiveMatrix3D = new PerspectiveMatrix3D();
private var viewmatrix:Matrix3D = new Matrix3D();

Что же произойдёт?
Как вы можете видеть, многое из предыдущего кода осталось таким же как то, что мы создали в предыдущей главе, за исключением того, что мы создали новые переменные для наших новых сущностей. Вместо использования класса Stage3dObjParser для наших внутриигровых моделей, мы позволим делать эту работу нашему новому классу Stage3dEntity.

Время для действий: встраиваем новые изображения.
Теперь мы просто создадим ссылки на наши текстуры и меши. Как и прежде метод выполнения различается в зависимости от того, используете ли вы Flash IDE и библиотеку или простой AS3/Flex проект и тег [embed].
/* ТЕКСТУРЫ: Версия для чистого AS3 и Flex:
* если вы используете Adobe Flash CS5
* закомментируйте следующее: */
[Embed (source = "art/spaceship_texture.jpg")]
private var playerTextureBitmap:Class;
private var playerTextureData:Bitmap = new playerTextureBitmap();
[Embed (source = "art/terrain_texture.jpg")] private var terrainTextureBitmap:Class;
private var terrainTextureData:Bitmap = new terrainTextureBitmap();
[Embed (source = "art/craters.jpg")] private var cratersTextureBitmap:Class;
private var cratersTextureData:Bitmap = new cratersTextureBitmap();
[Embed (source = "art/sky.jpg")] private var skyTextureBitmap:Class;
private var skyTextureData:Bitmap = new skyTextureBitmap();
[Embed (source = "art/engine.jpg")] private var puffTextureBitmap:Class;
private var puffTextureData:Bitmap = new puffTextureBitmap();
[Embed (source = "art/hud_overlay.png")] private var hudOverlayData:Class;
private var hudOverlay:Bitmap = new hudOverlayData();
/* ТЕКСТУРЫ: версия для Flash CS5:
* добавьте jpg в вашу библиотеку (F11)
* кликните правой кнопкой мыши и отредактируйте расширенные свойства
* такие как Экспортировать для использования в Actionscript
* и используйте правильные имена перечисленные ниже
* если вы используете Flex/FlashBuilder/FlashDevelop/FDT
* закомментируйте следующее: */
/*
private var playerTextureData:Bitmap = // spaceship_texture.jpg new Bitmap(new playerTextureBitmapData(512,512)); private var terrainTextureData:Bitmap = // terrain_texture.jpg new Bitmap(new terrainTextureBitmapData(512,512)); private var cratersTextureData:Bitmap = // craters.jpg new Bitmap(new cratersTextureBitmapData(512,512)); private var skyTextureData:Bitmap = // sky.jpg new Bitmap(new skyTextureBitmapData(512,512));
private var puffTextureData:Bitmap = // engine.jpg
new Bitmap(new engineTextureBitmapData(12 8,12 8)); private var hudOverlay:Bitmap = // hud_overlay.png
new Bitmap(new hudTextureBitmapData(640,4 80));
*/
// Текстуры Stage3d, которые используются выше
private var playerTexture:Texture;
private var terrainTexture:Texture;
private var cratersTexture:Texture;
private var skyTexture:Texture;
private var puffTexture:Texture;
Теперь мы должны встроить новые 3D меши, которые мы создали в этой главе, так же как несколько в прошлый раз:
// игрок
[Embed (source = "art/spaceship.obj", mimeType = "application/octet-stream")] private var myObjData5:Class;
// свечение двигателя
[Embed (source = "art/puff.obj", mimeType = "application/octet-stream")]
private var puffObjData:Class;
// Меш данных поверхности
[Embed (source = "art/terrain.obj", mimeType = "application/octet-stream")] private var terrainObjData:Class;
// поле астеройдов
[Embed (source = "art/asteroids.obj", mimeType = "application/octet-stream")] private var asteroidsObjData:Class;
// небо
[Embed (source = "art/sphere.obj", mimeType = "application/octet-stream")] private var skyObjData:Class;
Это все изображения и переменные, которые требуются нашей игре.

Время для действий: обновляем игровую инициализацию.
Теперь мы хотим изменить функцию конструктора для нашей игры, чтобы инициализировать всё. Это практически точно также как в прошлый раз, за исключением того, что мы создаём несколько пустых массивов Vector для наших новых списков сущностей:
public function Stage3dGame()
{
if (stage != null) init();
else
addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(e:Event = null):void {
if (hasEventListener(Event.ADDED_TO_STAGE))
removeEventListener(Event.ADDED_TO_STAGE, init);
// запускаем игровой таймер и ввод
gametimer = new GameTimer(heartbeat);
gameinput = new GameInput(stage);
// создаём пустые массивы
props = new Vector.<Stage3dEntity>();
enemies = new Vector.<Stage3dEntity>();
bullets = new Vector.<Stage3dEntity>();
particles = new Vector.<Stage3dEntity>();
// настраиваем сцену
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.align = StageAlign.TOP_LEFT;
// добавляем текстовые метки
initGUI();
// и запрашиваем context3D из Stage3d
stage.stage3Ds[0].addEventListener(
Event.CONTEXT3D_CREATE, onContext3DCreate); stage.stage3Ds[0].requestContext3D();

Что же произойдёт?
В нашей функции init() мы создали новый таймер и объекты ввода. Наша игра также может отслеживать время и реагировать на ввод с клавиатуры и мыши! Наконец, мы создали несколько списков различных типов сущностей, чтобы упростить циклическое прохождение через множество игровых объектов.

Две функции из предыдущей главы не перечислены здесь. Мы можем опустить тут функцию updateScore() – она осталась без изменений. Функция uploadTextureWithMipmaps() не изменялась вовсе. Вы можете ссылаться на них в исходном коде, скачав эту главу или отлистать назад к предыдущей главе.

Время для действий: обновляем GUI.
Помимо удаления неиспользуемых меток GUI, функция initGUI() осталось такой же, за исключением того, что шрифт был увеличен в размере и изменён цвет.
private function initGUI():void {
// наложение хэдз-ап-дисплея
addChild(hudOverlay);
// дескриптор формата текста, используемый всеми метками gui
var myFormat:TextFormat = new TextFormat();
myFormat.color = 0xFFFFAA;
myFormat.size = 16;
// создаём счётчик FPS, который отображает частоты кадров на экране.
fpsTf = new TextField();
fpsTf.x = 4;
fpsTf.y = 0;
fpsTf.selectable = false;
fpsTf.autoSize = TextFieldAutoSize.LEFT;
fpsTf.defaultTextFormat = myFormat;
fpsTf.text = "Initializing Stage3d...";
addChild(fpsTf);
// создаём дисплей очков
scoreTf = new TextField();
scoreTf.x = 54 0;
scoreTf.y = 0;
scoreTf.selectable = false;
scoreTf.autoSize = TextFieldAutoSize.LEFT;
scoreTf.defaultTextFormat = myFormat;
addChild(scoreTf);
}

Время для действий: упрощаем шейдеры.
Также устанавливаем true для функции initShaders(), которая практически не изменилась, за исключением того, что неиспользуемый код был удалён:
private function initShaders():void {
// Простой вершинный шейдер, который выполняет 3D трансформацию
// для простоты он используется всеми четырьмя шейдерами
var vertexShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler(); vertexShaderAssembler.assemble (
Context3DProgramType.VERTEX,
// 4x4 матричное умножение, чтобы получить угол камеры
"m44 op, va0, vc0\n" +
// рассказываем фрагментному шейдеру о XYZ
"mov v0, va0\n" +
// рассказываем фрагментному шейдеру о UV
"mov v1, va1\n" +
// рассказываем фрагментному шейдеру о RGBA
"mov v2, va2"
);
// текстурируем, используя UV коородинаты
var fragmentShaderAssembler1:AGALMiniAssembler = new AGALMiniAssembler(); fragmentShaderAssembler1.assemble (
Context3DProgramType.FRAGMENT,
// захватываем цвет текстуры из текстуры 0
// и uv координаты из регистра переменных 1
// и сохраняем интерполированное значение в ft0
"tex ft0, v1, fs0 <2d,linear,repeat,miplinear>\n"+
// перемещаем это значение в цвет для вывода
"mov oc, ft0\n"
);
// комбинируем шейдеры в программу, котоорую мы затем загружаем в GPU shaderProgram1 = context3D.createProgram(); shaderProgram1.upload(
vertexShaderAssembler.agalcode,
fragmentShaderAssembler1.agalcode);

Время для действий: используем новые текстуры.

Теперь, функция onContext3DCreate нуждается в нескольких незначительных изменениях. В частности, нам нужно загрузить наши новые текстуры и изменить матрицу проекции, чтобы иметь более крупный «дальний план», так чтобы мы могли вместить всё в представление нашего большого игрового мира.
private function onContext3DCreate(event:Event):void {
// Удаляем существующий обработчик кадров. Заметьте, что потеря
// контекста может произойти в любой момент, что заставит вас
// пересоздать все объекты, которые мы создаём здесь.
// Потеря контекста происходит, например, если вы нажимаете
// CTRL-ALT-DELETE на Windows.
// Пройдёт некоторое время, прежде чем новый контекст станет доступен
// следовательно удаление обработчика enterFrame важно!
if (hasEventListener(Event.ENTER_FRAME))
removeEventListener(Event.ENTER_FRAME,enterFrame);
// Получаем новый контекст
var t:Stage3D = event.target as Stage3D;
context3D = t.context3D;
if (context3D == null)
{
// Текущий3d контекст не доступен (ошибка!)
trace('ERROR: no context3D - video driver problem?');
return;
}
// Отключение проверки на ошибки резко улучшит производительность.
// Если установлено на true, Flash полезные сообщения об ошибках касательно
// ошибок компиляции AGAL, неинициализированных программных констант, и т.д.
context3D.enableErrorChecking = true;
// Размер заднего Размер заднего 3d буфера в пикселях (2=antialiased) context3D.configureBackBuffer(stage.width, stage.height, 2, true);
// собираем все шейдеры, которые нам нужны
initShaders();
playerTexture = context3D.createTexture(
playerTextureData.width, playerTextureData.height, Context3DTextureFormat.BGRA, false);
uploadTextureWithMipmaps(
playerTexture, playerTextureData.bitmapData);
terrainTexture = context3D.createTexture(
terrainTextureData.width, terrainTextureData.height, Context3DTextureFormat.BGRA, false);
uploadTextureWithMipmaps(
terrainTexture, terrainTextureData.bitmapData);
cratersTexture = context3D.createTexture(
cratersTextureData.width, cratersTextureData.height, Context3DTextureFormat.BGRA, false);
uploadTextureWithMipmaps(
cratersTexture, cratersTextureData.bitmapData);
puffTexture = context3D.createTexture(
puffTextureData.width, puffTextureData.height, Context3DTextureFormat.BGRA, false);
uploadTextureWithMipmaps(
puffTexture, puffTextureData.bitmapData);
skyTexture = context3D.createTexture(
skyTextureData.width, skyTextureData.height, Context3DTextureFormat.BGRA, false);
uploadTextureWithMipmaps(
skyTexture, skyTextureData.bitmapData);
// Инициализируем даные нашего меша – сначала запрашиваем шейдеры и текстуры
initData();
// создаём матрицу проекции для нашей 3D сцены
projectionmatrix.identity();
// 45 градусов FOV, 640/480 соотношение сторон, 0.1=near, 150000=far projectionmatrix.perspectiveFieldOfViewRH(
45, stage.width / stage.height, 0.01, 150000.0);
// запускаем цикл рендеринга!
addEventListener(Event.ENTER_FRAME,enterFrame);

Что же произойдёт?
Мы обновили наше событие onContext3DCreate, принимая во внимание наши новые игровые арты. Мы также изменили углы камеры (путём изменения значений, сохранённых в матрице проекции) так, чтобы вся сцена была видна.

Второе, и последнее изменение в нашей игре это изменить функцию initData(), чтобы создать несколько новых Stage3d сущностей. Вместо парсинга .OBJ файлов здесь, наш новый класс сущностей будет делать всю тяжёлую работу. Однажды создав эти новые объекты, мы поместим некоторые из них в наш список сущностей, так что мы сможем обращаться к ним в цикле во время рендеринга.

Один потенциальный момент «попался!» здесь это то, что ваши экспортируемые .OBJ файлы могут быть в немного других форматах, чем те, что включены в проект примера. Некоторые экспортёры выводят данные вершин в неправильном порядке и функция конструктора класса Stage3dObjParser.as имеет два булевых значения, которые инструктируют её обрабатывать данные в этом порядке.

Поэтому, если все ваши модели выглядят совершенно испорченными, когда вы компилируете источник, то отредактируйте ваш файл Stage3dEntity.as и измените строку 68 в публичной функции Stage3dEntity() с:
mesh = new Stage3dObjParser(mydata, context, 1, true, true);
на нечто подобное:
mesh = new Stage3dObjParser(mydata, context, 1, false, false);
Возможно вам придётся поэкспериментировать с этими строками, в зависимости от того какой экспортёр вы используете. Помимо этой потенциальной помехи с форматом файла, работа по получению ваших моделей этим новым классом должна быть лёгкой.

Время для действий: плодим игровые сущности.
Давайте обновим нашу функцию initData(), чтобы включить в неё новые изображения и использовать нашу новый фантастический класс Stage3dEntity:
private function initData():void {
// создаём сущность камеры
trace("Creating the camera entity...");
chaseCamera = new Stage3dEntity();

Обратите внимание, что в начале этой функции мы создали сущность «преследования камеры» без данных модели. Это вполне допустимо – преследование камеры это игровая сущность, но визуализировать её совсем не нужно. Так как наш класс сущностей имеет все виды удобных матриц, математические алгоритмы запрограммированы для нас, использовать их, даже для невидимых вещей подобно камере, идеально.
// создаём модель игрока
trace("Creating the player entity...");
player = new Stage3dEntity( myObjData5, context3D, shaderProgram1, playerTexture);
// поворачиваем лицом вперёд
player.rotationDegreesX = -90;
player.z = 2100;
trace("Parsing the terrain...");
// добавляем поверхность в список вещей
var terrain:Stage3dEntity = new Stage3dEntity( terrainObjData, context3D, shaderProgram1, terrainTexture);
terrain.rotationDegreesZ = 90;
terrain.y = -50;
props.push(terrain);
trace("Cloning the terrain...");
// используем такой же меш в другом местоположении
var terrain2:Stage3dEntity = terrain.clone();
terrain2.z = -4000;
props.push(terrain2);

В предыдущем коде мы создали сущность игрока (космический корабль), а также две копии меша поверхности, для того чтобы создать отличный большой мир, чтобы играть в нём. Обратите внимание, что мы используем функцию clone(), так что вторая копия нашей поверхности будет совместно использовать те же вершины и UV буфер, а также текстуры и шейдер.
trace("Parsing the asteroid field...");
// добавляем поле астероидов в список вещей
asteroids1 = new Stage3dEntity( asteroidsObjData, context3D, shaderProgram1, cratersTexture);
asteroids1.rotationDegreesZ = 90;
asteroids1.scaleXYZ = 200;
asteroids1.y = 500;
asteroids1.z = -1000;
props.push(asteroids1);
trace("Cloning the asteroid field...");
// используем один и тот же меш в различных местоположениях
asteroids2 = asteroids1.clone();
asteroids2.z = -5000;
props.push(asteroids2);
asteroids3 = asteroids1.clone();
asteroids3.z = -9000;
props.push(asteroids3);
asteroids4 = asteroids1.clone();
asteroids4.z = -9000;
asteroids4.y = -500;
props.push(asteroids4);

В предыдущем коде мы создали сущность, которая использует наш новый меш астероидов, затем клонировали её несколько раз и добавили в различные местоположения и под различными углами, таким образом, наполнив небо плавающими в космосе обломками.
trace("Parsing the engine glow...");
engineGlow = new Stage3dEntity( puffObjData, context3D, shaderProgram1, puffTexture);
// следуем за кораблём игрока
engineGlow.follow(player);
// рисуем прозрачные частицы
engineGlow.blendSrc = Context3DBlendFactor.ONE;
engineGlow.blendDst = Context3DBlendFactor.ONE;
engineGlow.depthTest = false;
engineGlow.cullingMode = Context3DTriangleFace.NONE;
engineGlow.y = -1.0;
engineGlow.scaleXYZ = 0.5;
particles.push(engineGlow);

В предыдущем коде мы создали сущность «спецэффектов», которая использует меш системы частиц и аддитивный режим наложения для прозрачности, осветления, чтобы быть использованной подобно «огню», выходящему из двигателей игрока. Мы «присоединяем» её к сущности игрока, так чтобы где бы ни двигался космический корабль, свечение двигателя следовало за ним. Наконец, мы создали гигантскую сферу, которая использует арт нашего нового космического бекграунда. Так чтобы, на расстоянии мы увидели звёзды и туманности вместо просто чёрной пустоты.
trace("Parsing the sky...");
sky = new Stage3dEntity( skyObjData,
context3D,
shaderProgram1,
skyTexture);
// следуем за кораблём игрока
sky.follow(player);
sky.depthTest = false;
sky.depthTestMode = Context3DCompareMode.LESS;
sky.cullingMode = Context3DTriangleFace.NONE;
sky.z = 2 000.0;
sky.scaleX = 40000;
sky.scaleY = 40000;
sky.scaleZ = 10000;
sky.rotationDegreesX = 30;
props.push(sky);
}

Что же произойдёт?
Мы создали несколько объектов Stage3dEntity для использования в нашей новой, улучшенной игре. Реквизит уровня, такой как поверхность, астероиды и «небо», так же как космический корабль игрока и светящееся пламя, выходящее из двигателей, помогут добавить деталей в нашу игру. Мы готовы программировать нашу новую, улучшенную функцию рендеринга. Раньше она была неаккуратной и наполненной кодом, но с тех пор мы уже обновили наш игровой движок до использования класса сущностей, чтобы делать всю тяжёлую работу.

Время для действий: обновляем функцию рендеринга.
Всё, что должно быть , чтобы визуализировать нашу сцену – это обновить матрицы обзора в соответствии местоположением и ориентацией нашей преследующей камеры (которая добавляет углы, требуемые для функциональности обзора мышью, которую мы запрограммировали в классе GameInput) и, затем, пройтись в цикле по каждой сущности в нашем игровом мире и инструктировать их визуализироваться.
private function renderScene():void {
viewmatrix.identity();
// смотрим на игрока
viewmatrix.append(chaseCamera.transform);
viewmatrix.invert();
// наклоняем немного вниз
viewmatrix.appendRotation(15, Vector3D.X_AXIS);
// если обзор мышью включён:
viewmatrix.appendRotation(gameinput.cameraAngleX,
Vector3D.X_AXIS);
viewmatrix.appendRotation(gameinput.cameraAngleY,
Vector3D.Y_AXIS);
viewmatrix.appendRotation(gameinput.cameraAngleZ,
Vector3D.Z_AXIS);
// визуализируем меш игрока под текущим углом камеры
player.render(viewmatrix, projectionmatrix);
// проходим в цикле все известные сущности и отображаем их
for each (entity in props)
entity.render(viewmatrix, projectionmatrix);
for each (entity in enemies)
entity.render(viewmatrix, projectionmatrix);
for each (entity in bullets)
entity.render(viewmatrix, projectionmatrix);
for each (entity in particles)
entity.render(viewmatrix, projectionmatrix);
}

Что же произойдёт?
Мы упростили нашу функцию renderScen, переместив всю низкоуровневую функциональность Stage3D в новый класс Stage3dEntity. Мы сначала установили матрицу проекции обзора как в предыдущей главе, но теперь мы можем добавлять дополнительные повороты, если игрок перетаскивает мышь, чтобы осмотреться. Затем мы инструктируем сущность игрока визуализироваться и, затем, проходимся в цикле по всем известным вещам, чтобы визуализировать что-нибудь ещё, что мы можем добавить в мир.

Мало того, мы хотим визуализировать всё один раз за кадр, но мы также должны обновить внутриигровой мир. Этот шаг симуляции обновления – теперь простая функция, но в конечном счёте можно расширить включённую физику, искусственный интеллект и всевозможные игровые события таймера и тому подобное. Для начала, давайте запрограммируем пошаговую функцию, которая учитывает количество миллисекунд, которые прошли с последнего кадра, и переместим различные анимированные сущности на подходящее расстояние.

Время для действий: создаём симулирующую пошаговую функцию.
Давайте заставим эту функцию обновлять позицию игрока в зависимости от того, какой ввод был нажат, а также изменять наше положение нашей камеры преследования, сглаживать анимацию изменчивого движения астероидов и заставим свечение двигателя, позади нашего космического корабля, пульсировать.
private function gameStep(frameMs:uint):void {
// управляем вводом игрока
var moveAmount:Number = moveSpeed * frameMs;
if (gameinput.pressing.up) player.z -= moveAmount;
if (gameinput.pressing.down) player.z += moveAmount;
if (gameinput.pressing.left) player.x -= moveAmount;
if (gameinput.pressing.right) player.x += moveAmount;
//if (pressing.fire)
// etc...
// следуем за игроком
chaseCamera.x = player.x;
chaseCamera.y = player.y + 1.5;
// над
chaseCamera.z = player.z + 3;
//позади
// анимируем астероиды
asteroids1.rotationDegreesX += asteroidRotationSpeed * frameMs; asteroids2.rotationDegreesX -= asteroidRotationSpeed * frameMs; asteroids3.rotationDegreesX += asteroidRotationSpeed * frameMs; asteroids4.rotationDegreesX -= asteroidRotationSpeed * frameMs;
// анимируем свечение двигателя – крутится быстро и пульсирует медленно engineGlow.rotationDegreesZ += 10 * frameMs; engineGlow.scaleXYZ = Math.cos(
gametimer.gameElapsedTime / 66) / 20 + 0.5;
}

Что же произойдет?
Предыдущая функция gameStep будет вызываться с параметром, который содержит число миллисекунд, которые прошли с предыдущего кадра. Это число вычисляется нашим новым классом игрового таймера. В предыдущем коде мы потом используем это число, чтобы определить, как далеко должны быть перемешены анимированные сущности, так, чтобы они вращались с одинаковой скоростью независимо от частоты кадров.

Время для действий: создаём функцию сердцебиения.
Для эффективности эта функция запускается только изредка, она идеально подходит для расчётов, которые не требуется запускать каждый кадр.
private function heartbeat():void {
trace('heartbeat at ' + gametimer.game_elapsed_time + 'ms');
trace('player ' + player.pos_string());
trace('camera ' + chase_camera.pos_string());
}
Наша текущая функция сердцебиения всего лишь пустой шаблон, готовый к использованию вместо дорогостоящих вычислений, как только ваша игра станет более сложной. Для начала мы сделаем вывод отладочной информации в окно отладки Flash.

Время для действий: обновляем функцию enterFrame.
Мы почти закончили обновление нашей игры! Всё, что осталось – это настроить функцию, которые запускаются каждый кадр. Изменим функцию enterFrame следующим образом:
private function enterFrame(e:Event):void {
// обязательно очищаем сцену перед рендерингом
context3D.clear(0,0,0);
// считаем кадры, измеряем прошедшее время
gametimer.tick();
// обновляем все позиции сущностей, и т.д.
gameStep(gametimer.frameMs);
// визуализируем всё
renderScene();
// теперь показываем/переворачиваем задний буфер
// чтобы все меши были нарисованы
context3D.present();
// обновляем дисплей FPS
fpsTicks++;
var now:uint = getTimer();
var delta:uint = now - fpsLast;
// обновляем дисплей только один раз в секунду
if (delta >= 1000)
{
var fps:Number = fpsTicks / delta * 1000;
fpsTf.text = fps.toFixed(1) + " fps";
fpsTicks = 0;
fpsLast = now;
}
// update the rest of the GUI
updateScore();
}
} // end of class
} // end of package

Что же произойдёт?
Функция выше – наша настоящая рабочая лошадка, «цикл рендеринга» enterFrame, который запускается каждый отдельный кадр. Как всегда, мы в первую очередь отчищаем экран. Затем мы обновляем таймер и, следующим шагом игровой симуляции, рассчитываем прошедшее время. Наконец, мы визуализируем сцену, заставляя её отобразиться на экране, и обновляем дисплей FPS.

Давайте посмотрим на всё это в действии!
Теперь, когда мы сделали массивное обновление нашего игрового движка, вы, возможно, взволнованы реализацией всей этой новой вычислительной мощности. Хотя мы только поскребли поверхность того, чего мы могли достичь с новым вводом, таймером и классом сущностей, вы, несомненно, наполнены идеями для будущих улучшений, которые используют их.

В данный момент у нас есть космический корабль с анимированным пламенем двигателя, который вы можете контролировать клавишами стрелок или W, A, S, D. Есть камера преследования, которая повсюду следует за игроком. Эта камера реализует обзор мышью в виде прицела, так что вы можете держать клавишу мыши нажатой и перетаскивать курсор всюду, чтобы поворачивать камеру как в шутере от первого лица. Есть небо в качестве фона и привлекательное, анимированное поле астероидов в нашем недавно улучшенном игровом мире. Неплохо для работы за одну главу!

Вы можете посмотреть законченное демо Главы 7 здесь:
http://www.mcfunkypants.com/molehill/chapter_7_demo/
Вы можете посмотреть полный исходный код здесь:
http://www.packtpub.com/adobe-flash11-stage3d-molehill-game-
programming-beginners-guide/book
Скомпилируйте ваш проект, чтобы убедиться, что нет опечаток и, затем, запустите index.html в папке вашего проекта. Если всё прошло как планировалось, вы должны увидеть сцену, идентичную предыдущему изображению.

Быстрый опрос – зарабатываем очки опыта.
Здесь будут несколько быстрых вопросов, чтобы проверит ваше понимание. Если вы можете ответить на них, то вы официально готовы перейти на следующую ступень в нашем грандиозном приключении.
1. Почему нашей пошаговой внутриигровой функци требуется параметр значения времени?
а) В целях отладки
б) Чтобы она знала, когда запускаться
в) Чтобы она могла анимировать вещи с одинаковой скоростью независимо от частоты кадров
г) Чтобы она не скучала

2.Почему мы хотим использовать функцию Stage3dEntity.clone() при добавлении предметов в наш игровой мир?
а) Чтобы повторно использовать тот же самый вершинный буфер для рисования множества мешей в различных местоположениях
б) Чтобы игра инициализировалась быстрее
в) Чтобы использовать меньше RAM
г) Всё вышеперечисленное

3.Почему мы устанавливаем «грязный флаг» _transformNeedsUpdate в true, когда мы устанавливаем одно из многих местоположений, поворотов или значений масштаба в нашем классе GameEntity?
а) Чтобы наши роботы знали, когда трансформироваться в транспорт
б) Чтобы игровой движок знал, какой меш визуализировать
в) Чтобы, когда в следующий раз потребуется преобразование, мы знали, что оно должно быть обновлено
г) Чтобы мы знали, когда в следующий раз мыть наш флаг.

Приготовься, герой – весёлый побочный квест: увлекательные виражи!
Ваш побочный квест на этот раз – заставить что-нибудь произойти, когда пользователь нажимает пробел. В этот момент в вашей функции gameStep() вы можете проверить, если gameinput. pressing.fire на true. Запрограммируйте функцию, которая вызывает некоторые изменения в игре, когда вы нажимаете пробел.

Отличной идеей было бы повышать скорость космического корабля, временно увеличивая значение moveAmount до большего числа. Для того, чтобы сделать это временным, сохраняйте текущее время (gametimer.currentFrameTime) перед повышением скорости и сбрасывайте скорость на оригинальное значение по прошествии пары секунд. Это прекрасное использование классов GameInput и GameTimer и хороший способ, чтобы вступить в борьбу, работая с ними обоими.

Итого.
В этой главе нам удалось достигнуть следующих рубежей:
У нас теперь есть класс, который измеряет время для использования в анимации
У нас также есть новый класс, который определяет ввод с клавиатуры и мыши игрока
Мы создали класс игровых сущностей для использования всеми внутриигровыми объектами
Мы запрограммировали простую камеру преследования с обзором мышью
Мы обновили нашу игру, включив наложение хэдз-ап-дисплея и много новых внутриигровых моделей и эффектов

Уровень 7 достигнут!
Поздравляем, отважный герой. В очередной раз ты бросил вызов и победил. Твоя игра эволюционирует экспоненциальными шагами. Мало того, она начинает действительно выглядеть как игра, а основополагающие компоненты теперь на месте, чтобы можно было сфокусироваться на геймплее. Больше мы не боремся с инициацией или изучением основ. Теперь мы работаем над повторно используемыми абстрактными классами, которые предоставляют общую, нацеленную на игру функциональность.

Вы определённо подняли уровень ваших навыков. Это становится интересно!
Вы заслужили право начать программировать спецэффекты следующего поколения, такие как взрывы, искры, свечения, дым и частицы. Это , наконец, время сосредоточится на приятном. Это и многое другое ожидает вас в следующей главе.

Переведено на Нотабеноиде
http://notabenoid.org/book/40281/152360

Переводчики: Orion45, oneassasin

Настройки

Готово:

100.00% КП = 1.0