Готовый перевод Adobe Flash 11 Stage3D (Molehill) game programming (Chapter 6 - 10) / Программирование игр в Adobe Flash 11 Stage3D (Molehill) (Глава 6 - 10): 3... 2... 1... Действие!
Вы достигли битвы с финальным боссом. Уровень 10.
Финальный шаг в нашем грандиозном приключении - это отполировать нашу игру. Чтобы развить её из технического демо в реальную, законченную, играбельную игру, которая имеет начало, середину и конец.
Финальные шаги в жизненном цикле разработки нашей игры могут занять большую часть времени и усилий. Хорошие новости это то, что вы уже вооружены всеми навыками, которые требуются чтобы выполнить это.
Мы не должны учить ничего радикально нового: с этого момента, главный вызов это совершение, чтобы качество и время, которое это заняло, закончили работу.
Полировка - это трудоёмкая работа. Ничего из этого в отдельности не сложно, но это всё складывается.
Поэтому не ожидайте, что этот финальный рывок в проекте будет лёгким: это займёт больше времнеи чем выожидаете. Почему? Полировка и проверка качества это то, где истинная душа игры принимает форму.
Это то, где вы разрабатываете уровни, битвы с боссами, меню и подправляете игровую механику.
Вы могли бы решать разогнать движение игрока или попросить друзей сыграть в образец уровня и реагировать на их отзывы. Вы можете захотеть создать больше контента или пересмотреть старый контент. Вы можете потратить недели на полировку, добавление новых артов, подправляя систему частиц, добавляя больше эффектов и сочиняя музыку. Этот процесс может продолжаться вечно, так что это хорошая идея позволить себе обрезать характеристики также часто как вы добаляете новые.
В этой финальной главе вы закончим пример аркадной стрелялки "ад пуль", которую мы разрабатываем. Так как есть много работы, которая могла бы быть сделана в области артов или в программировании игры, которые очень базовые и необязательно связаны со Stage3D, мы приукрасим несколько разделов вместо того, чтобы перечислять всё.
Вы близки к тому, чтобы продолжить приключение по своему усмотрению.
Наш финальный квест.
Для того, чтобы закончить игру, требуется несколько базовых механизмов. Мы хотим своего рода заглавный экран, например, с логотипом и стартовой кнопкой. Мы также хотим способность разрушать все виды врагов. Наши игроки также должны быть способны наносить урон кораблю игрока и, после достаточного урона, мы хотим чтобы отображался экран "ИГРА ОКОНЧЕНА".
В доплнение к в началу (заглавный экран) и к концу (конец игры) мы хотим, чтобы там был игро, стоящий игры: полностью проработанный уровень. Поэтому вместе с базовыми элементами геймплея и GUI экранами мы должны разработать огромный уровень, заполненный врагами.
Наконец, небольшие штрихи должны быть добавлены повсюду. Мы обсудим некоторые простые техники для улучшения производительности игры и способы добавить небольшие дополнения, которые нужны, чтобы заполнить её действиями.
Вот как будет выглядеть наш финальный образец игры:
Если вы не хотите печатать код и предпочли бы просто следовать вместе с текущими обсуждениями после скачивания полного исходного кода финальной игры, вы можете получить его по следующему URL:
http://www.packtpub.com/adobe-flash11-stage3d-molehill-gameprogramming-beginners-guide/book
Вы можете сыграть в игру здесь:
http://www.mcfunkypants.com/molehill/chapter_10_demo/
Добираемся до последней черты.
Это фаза в о многих проектах, которая, кажется, может занять вечность. Хорошие новости то, что этот последний квест в нашем приключении может также быть и самым весёлым! Учитывая, что программирование ядра движка, наконец, позади, мы свободны позволиь разыграться нашему изображению. Эксперементировать с новыми игровыми режимами, фантастическим новым оружием, писанием диалогов, планов и персонажей, плюс создание всех видов прекрасных артов.
Эта фаза, на которой вы захотите начатьь показывать игру вашим друзьям. Игровое тестирование это ключ к созанию великолепной игры. Не делайте безумные штрихи на последней черте и не ожидайте того, что ваше создание будет большим хитом пока вы не потратите потом какое-то время, чтобы заставить людей протестировать игру, получите отзывы, найдёте ошибки, что даст вам свежее представление или идеи для новых характеристик.
Для целей примера shoot-em-up игры "ад пуль" с космическими кораблями, которая является проектом примера использованном в курсе этой книги, первое в повесте дня это создать больше артов. Спроектируйте больше крутых кораблей, эффектов частиц, астероидов, космисеских станций и декораций, чтобы конкретизировать вашу игру. Используйте класс GameLevelParser, созданный в главе 9, чтобы реализовать сложный уровень, заполненный врагами и взрывающимися предметами.
Так как мы уже показывали как добавить новые модели в нашу игру, вместо объяснений, что в точности было сделано в приемере игры, использкуйте ваше воображение. На этом этапе вы можете посходить с ума и создать все виды интересных моделей и текстур. Вам больше не нужны пошаговые инструкции о том, как поместить их в вашу игру.
Отдельно от активов новых артов, полировка нашей стрелялки должна быть относительно просты процессом.
Есть несколько характеристик, удобных в играх, которые мы должны добавить.
Некоторые из финальных кусков полировки и геймплея, которые нам нужно реализовать, включают:
Реализацию заглавного экрана
Обновление движения игрока и удерживание игрока в пределах границ.
Обновление нашего GUI
Встраивание наших новых арт-активов.
Обновление нашего кода инициализации.
Определение новых типов действующих лиц и поведения.
Создание игрового уровня.
Улучшение цикла рендеринга.
Реализация игровых событий (начало, конец, урон).
Обновление управления игрока.
Обновление функции пошаговой симуляции.
Публикация... распостранение... выгода!
Как только игровой движок "сделан", финальный шаг вполне мог бы занять месяцы, если вы решили добавить сотни уровней и глубокий сюжет! Для целей этого гайда примера игры для начинающих, мы сохраним её простой.Мы сфокусируемся на улучшении движка и запуске, но давайте предположим, что игра, которую мы делаем, не богата чертами, как будет ваш оригинальный шедевр.
Выстовить вашу игру на волю публики, конечно, очень вероятно будет самой большой работой. Тут полно неосязаемых шагов "проверки качества", которые могут продолжаться некоторое время пока вы не будете полностью удовлетворены вашей игрой. Маркетинг, работа с социальными сетями и монетизация остаётся как упражение для читателя.
По этой причине, этот самый последний финальный шаг полировки будет украшен в то время как основа проекта, повторно используемый Flash 11 3D игровой движок будет нашей главной целью. Имея это виду, давайте погрузимся!
Время для действий - рисуем заглавный экран.
Для того, чтобы заставить заглавный экран работать в нашей игре, всё, что нам нужно сделать, это разработать привлекательное изображение используя Photoshop или любой другой редактор изображений. Во многом также как в предыдущих главах где мы создали внутриигровой HUD(наложенное изображение heads-up-display), который был сохранен как ббольшой прозрачный .PNG файл, сделайт е то же самое дляя заглавного экрана. Например:
Когда ваше полноразмерное изображение готово к наложению в самом начале, просто встройте его в ваш SWF тем же способом, как вы встраивали другие графические активы - либо через flex тег [embed], используя палитру библиотеки Flash .
Как только мы добавим заглавный экран на сцену в нашу функцию initGUI, он будет отображаться когда игра начинается. Единственная проблема сейчас то, что он остаётся видимым во время геймплея, который сам начинается немедленно после загрузки игры. Что нам действительно нужно, так это отследживать "состояние" игры и ставить её на паузу пока игрок не готов начать.
Время для действий - импортируем требуемые классы.
Для остатка этой главы все листинги кода есть для Stage3dGame.as, главного файла в нашем проекте. Начнём с того, что сделаем несколько изменений в врехней части файла, чтобы посчитать классы, которые мы создали в предыдущей главе плюс некоторую дополнительную функциональность Flash, которую мы собираемся использовать.
package
{
[SWF(width="640", height="480", frameRate="60",
backgroundColor="#FFFFFF")]
import com.adobe.utils.*;
import flash.events.*;
import flash.geom.*;
import flash.utils.*;
import flash.text.*;
import flash.filters.GlowFilter;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.system.System;
import flash.system.Capabilities;
import flash.system.ApplicationDomain;
import flash.display.*;
import flash.display3D.*;
import flash.display3D.textures.*;
import flash.net.*;
import GameActor;
import GameActorpool;
import GameInput;
import GameLevelParser;
import GameParticlesystem;
import GameTimer;
import Stage3dEntity;
import Stage3dParticle;
Что же произошло?
Предыдущий код просто импортирует функциональность, которая нужна для нашего игрового движка - есть просто несколько новых строк, чтобы добавить фантастические текстовые эффекты, также как наши недавно реализованные классы. Теперь обновите переменные вашего класса, чтобы учесть наш заглавный экран, textFields и прочее.
Добавляем новые переменные в нашу игру.
Несколько следующих резделов включают добавление многочисленных переменных требующихся для финальной функциональности в нашей игре.
Время для действий - отслеживаем состояние игры.
Мы добавим массу новых переменных в нашу игру в следующем разделе "врмя для действий". Чтобы начать, мы реализуем возможность отслеживать состояние игры, таким образом мы сможем иметь заглавный экран , который отображется когда ещё не началась. Отредактируйте свойства нашего класса Stage3dGame следующим образом.
public class Stage3dGame extends Sprite
{
private const VERSION_STRING:String = "Version: 1.10";
//текущее состояние игры (0 = заглавный экран)
private const STATE_TITLESCREEN:int = 0;
private const STATE_PLAYING:int = 1;
private var gameState:int = STATE_TITLESCREEN;
private var titlescreenTf:TextField;
Что же произошло?
Нам нужно определить переменные "состояния" для нашего игрового движка, который помнит в каком режиме игра. Возможно мы можем установить это таким образмо, что ноль означает, что игра находится на заглавном экране и ждёт, когда пользователь кликнет, чтобы играть. В вашей игре вы можете захотеть включить другие состояния, как такое, которое используется между уровнями или режим битвы с боссом или кинематографические события, такие как ролик. В нашем примере игры мы используем только два состояния: одно для момента, когда мы находимся на заглавном экране, и другое для момента когда игра идёт.
Время для действий - добавляем переменные для событий основанных на таймере.
Продолжим добавлять в наш игровой класс. Мы будем запускать различные события и отслеживать время и нам нужны переменные, чтобы сохранять все эти метки и сам таймер.
//отслеживаем время прошедешее в игре
private var gametimer:GameTimer;
//используеся с таймерами чтобы события
private var introOverlayEndTime:int = 0;
private var nextShootTime:uint = 0;
private var shootDelay:uint = 200;
private var invulnerabilityTimeLeft:int = 0;
private var invulnerabilityMsWhenHit:int = 3000;
private var screenShakes:int = 0;
private var screenShakeCameraAngle:Number = 0;
Что же произошло?
Мы собираемся запустить связанные со временем игровые события, такие как контроль как чсто вы можете стрелять, тряска экрана когда вы подбиты, делание игрока неуязвимы на какой-то период времени, после получения урона и простой "вводный кинематограф" где первые несколько секунд игры меняется угол камеры . Эти переменные будут использованы, чтобы сохранить метки для отслеживания когда что-то должно быть запущено.
Когда пользователь нажимет кнопку стрельбы (пробел и т.д.) мы хотим стрелять пулями вперёд.
Мы не хотим, однако, чтобы пули генерировались каждый отдельный кадр, а, вместо этого, после особого счётчика времени. Время стрельбы и переменные задержки в предыдущем когде учтены для этого.
Когда мы получаем урон от врага , распостранённый игровой обычай то, что игрок не может быть подбит небольшой промежуток времени пока они возвращают себе контроль и двигается прочь независимо от того, какая угроза только что ранила их. Маленькое окно веремени где игрок неуязвим звучит как интересная игровая механика.
Время для действий - добавляем переменные, связанные с движением.
//обработчики ввода клавиатуры и мыши
private var gameinput:GameInput;
//хорошо скрытая преследующая камера
private var chaseCameraXangle:Number = -5;
private var chaseCameraDistance:Number = 4;
private var chaseCameraHeight:Number = 2;
// как быстро мы движемся в единицах в мс.
private var flySpeed:Number = 1;
//как быстро мы движемся вверх/вниз/влево/вправо мс.
private var moveSpeed:Number = 0.02;
// медленная орбита астеройдов в градусах в мс.
private var asteroidRotationSpeed:Number = 0.002;
// заставляем корабль оставаться в пределах границ (0 = нет ограничений)
private var playerMaxx:Number = 32;
private var playerMaxy:Number = 32;
private var playerMaxz:Number = 0;
private var playerMinx:Number = -32;
private var playerMiny:Number = 0.1;
private var playerMinz:Number = 0;
private var levelCompletePlayerMinz:Boolean = true;
Что же произошло?
Предыдущие переменные используются, чтобы хранить клас игрового ввода и влиять на движения корабля игрока. Скорости и позиции максимума и минимума помогают держать игрока вблизи действия и двигаться достаточно быстро, чтобы быть взволнованным.
В этой игре мы хотим, чтобы корабль продолжал двигаться вперёд всё время, так что клавиши стрелок могут быть использованы, чтобы уклоняться от пуль и врагов. Поэтому скорость, с которой мы летим вперёд, также как скорость уклонений клавишами стрелок должны быть определены.
Кроме того, мы не хотим, чтобы игрок зажимал клавиедленно оставлял целый уровень позади, так что мы должны ограничить движение игрока областью, которая заставляет их оставаться вблизи действия. Максимальные и минимальные позиции координат сделают этот трюк.
Если уровень завершён Булево значение в предыдущем коде является true, игра "завершается" когда линия финиша была достигнута. Это позиция будет определена размером нашего уровня.
Время для действий - отслеживаем все сущности.
Нам нужно отслеживать несколько бассейнов действующих лиц и систем частиц вместе с отдельными игровыми сущностями, которые используются во время игры.
//все известные сущности в мире
private var chaseCamera:Stage3dEntity
private var props:Vector.<Stage3dEntity>;
private var player:GameActor;
private var enemies:GameActorpool;
private var playerBullets:GameActorpool;
private var enemyBullets:GameActorpool;
private var explo:Stage3dParticle;
private var particleSystem:GameParticlesystem;
// мы хотим запонить это для использования в 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;
// повторно используемая общая сущность (для скорости и избегания GC)
private var entity:Stage3dEntity;
private var actor:GameActor;
Что же произошло?
Списки сущностей в предыдущем коде были модифицированы для использования нашей богатой характеристиками системой бассйна действующих лиц, где это подходит. С этого момента вы хотим отслеживать пули и врагов в бассейне действующих лиц, так что они могут быть анимированы на столкновения.
Время для действий - обновляем HUD.
Теперь текстовые элементы GUI , определённые вместе с переменными, используются, чтобы отслеживать значения, которые будут отображаться на экране во время игры как часть HUD.
//используется GUI
private var fpsLast:uint = getTimer();
private var fpsTicks:uint = 0;
private var fpsTf:TextField;
private var gpuTf:TextField;
private var scoreTf:TextField;
private var healthTf:TextField;
private var score:uint = 0;
private var combo:uint = 0;
private var comboBest:uint = 0;
private var scenePolycount:uint = 0;
// используется для отображения полосы здоровья
private var healthText_0:String = '';
private var healthText_1:String =
'| | | | |';
private var healthText_2:String =
'| | | | | | | | | | |';
private var healthText_3:String =
'| | | | | | | | | | | | | | | | | |';
Что же произшло?
Интересен "измритель здоровья", который, в примере игры, просто устанавливает светящиееся линии, которые отражают сколько урона было нам нанесено. Кроме того, интересная игровая механика, которая может добавить глубины многим шутерам, это счётчик "комбо". Этот счётчик комбо отслеживает как много врагов было успешно подбито. Игроки с истинным мастерством могут попытаться спамить пулями везде для того, чтобы достичь впечатляющего счётчика комбо.
Время для действий - определляем переменные, используемые Stage3D.
Мы будем использовать следующие переменные, которые относятся к Stage3D:
//графическое 3d окно на сцене
private var context3D:Context3D;
// скомпилированный шейдер, использующийся для рендеринга наших мешей
private var shaderProgram1:Program3D;
// простой шейдер с одним статическим светом
private var shaderWithLight:Program3D;
// матрицы, которые воздействуют на положение мешей и угол камеры
private var projectionmatrix:PerspectiveMatrix3D =
new PerspectiveMatrix3D();
private var viewmatrix:Matrix3D = new Matrix3D();
Что же произошло?
Предыдущие переменные похожи на те, что использовались в большинстве предыдущих демо в других главах, чтобы отслеживать Context3D, углы камеры и шейдеры. Есть один новый шейдер, который мы реализуем - он относится к свету.
Добавляем арты в нашу игру.
Следующие несколкьо разделов будут включать встраивание всех арт-активов требующихся в коде для нашей игры.
Время для действий - встраивание новых арт-активов (AS3 версия).
Нам нужно встроить все изображения и MP3-файлы, требующихся, чтобы привнести привлекательный вид и звук. Если вы используете Adobe Flash IDE, тогда пропустите этот раздел, так как это применимо только если вы используете FlashDevelop, FlashBuilder, FDT, Flex, haXe и т.д.
[Embed (source = "art/player.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/particle1.jpg")]
private var particle1data:Class;
private var particle1bitmap:Bitmap = new particle1data();
[Embed (source = "art/particle2.jpg")]
private var particle2data:Class;
private var particle2bitmap:Bitmap = new particle2data();
[Embed (source = "art/particle3.jpg")]
private var particle3data:Class;
private var particle3bitmap:Bitmap = new particle3data();
[Embed (source = "art/particle4.jpg")]
private var particle4data:Class;
private var particle4bitmap:Bitmap = new particle4data();
Теперь, когда мы встроили несколкьо новых текстур для использования на врагах и других структурах, таких как космические станции, они появятся в игре.
[Embed (source = "art/badguys.jpg")]
private var badguyTextureData:Class;
private var badguyTextureBitmap:Bitmap = new badguyTextureData();
[Embed (source = "art/hulltech.jpg")]
private var techTextureData:Class;
private var techTextureBitmap:Bitmap = new techTextureData();
Дополнительные битмапы нужны для GUI: заглавный экран, внутриигровой верхний слой и небольшой вводный экран, где мы инструктируем игрока, что является его "миссией"
[Embed (source = "art/title_screen.png")]
private var titleScreenData:Class;
private var titleScreen:Bitmap = new titleScreenData();
[Embed (source = "art/hud_overlay.png")]
private var hudOverlayData:Class;
private var hudOverlay:Bitmap = new hudOverlayData();
[Embed (source = "art/intro.png")]
private var introOverlayData:Class;
private var introOverlay:Bitmap = new introOverlayData();
Теперь, когда мы имеем класс парсинга карты уровней, мы можем встроить ключевое изображение (определяя какие цвета представляют какой тип игровой сущности) и изображение карты (чертёж для нашего игрового мира).
[Embed(source = 'art/level_key.gif')]
private static const LEVELKEYDATA:Class;
private var levelKey:Bitmap = new LEVELKEYDATA();
[Embed(source = 'art/level_00.gif')]
private static const LEVELDATA:Class;
private var levelData:Bitmap = new LEVELDATA();
Это шрифт, который используется GUI. Мы определяем наборы персонажей, которые нам нужно сделать. в .SWF меньше.
[Embed (source = 'art/gui_font.ttf',
embedAsCFF = 'false',
fontFamily = 'guiFont',
mimeType = 'application/x-font-truetype',
unicodeRange='U+0020-U+002F, U+0030-U+0039, U+003A-U+0040,
U+0041-U+005A, U+005B-U+0060, U+0061-U+007A, U+007B-U+007E')]
private const GUI_FONT:Class;
Наконец, мы встраиваем звуки и музыку, следующим образом:
[Embed (source = "art/sfxmusic.mp3")]
public var introMp3:Class;
[Embed (source = "art/sfxblast.mp3")]
public var blastMp3:Class;
[Embed (source = "art/sfxexplode.mp3")]
public var explodeMp3:Class;
[Embed (source = "art/sfxgun.mp3")]
public var gunMp3:Class;
Что же произошло?
Работа с артами, требуемая нашим GUI и внутриигровыми текстурами, встроена в точности тем же способом как предыдущих главах.
LEVELDATA и LEVELKEYDATA арт-активы, которые требуются нашей игре - чертёжные карты уровня. В предыдущей главе мы написали простой класс GameLevelParser, который берёт крошечное ключевое изображение, чтобы просмотреть RGB значения, плюс изображение "карты" данных уровня, по которой вы нарисовали настоящий уровень.
Вместо использования шрифта textField по умолчанию (который в большинстве случаев уродливый Times New Roman), мы можем встроить фантастические шрифты. В подпапке art нашей директории исходного кода, мы можем включить истинные типы шрифтов, которые мы включим в наш SWF. Параметр unicodeRange инструктирует Flash не включать целый шрифт, а только символы, которые нужны, такие как буквы, цифры и знаки пунктуации, которые сохраняют размер файла, не включая глифы иностранных языков. Наконец, также были встроены MP3-файлы, так что наша игра будет способна выводить звук и музыку.
Время для действий - встраиваем наши новые арт активы (CS5 версия).
Следующий код может быть пропущен, если вы использовали предыдущий раздел, но требуется, если вы используете Adobe Flash CS5. Помните, что вместо [embed], нам нужно перетаскивать файлы на палитру библиотеки Flash-редактора и правым кликом на вводе редактировать их свойства.
Это также тот же самый способ как в прошлый раз, за исключением того, что мы также добавляем шрифт и звуки.
private var playerTextureData:Bitmap = // player.jpg
new Bitmap(new playerTextureBitmapData(256,256));
private var badguyTextureBitmap:Bitmap = // badguys.jpg
new Bitmap(new badguyTextureBitmapData(256,256));
private var techTextureBitmap:Bitmap = // hulltech.jpg
new Bitmap(new techTextureBitmapData(256,256));
private var terrainTextureData:Bitmap = // terrainTexture.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(128,128));
private var particle1bitmap:Bitmap = // particle1.jpg
new Bitmap(new particle1BitmapData(128,128));
private var particle2bitmap:Bitmap = // particle2.jpg
new Bitmap(new particle2BitmapData(128,128));
private var particle3bitmap:Bitmap = // particle3.jpg
new Bitmap(new particle3BitmapData(128,128));
private var particle4bitmap:Bitmap = // particle4.jpg
new Bitmap(new particle4BitmapData(128,128));
private var titleScreen:Bitmap = // titleScreen.png
new Bitmap(new titlescreenBitmapData(640,480));
private var hudOverlay:Bitmap = // hudOverlay.png
new Bitmap(new hudTextureBitmapData(640,480));
private var introOverlay:Bitmap = // intro.png
new Bitmap(new introOverlayBitmapData(640,480));
private var levelKey:Bitmap = // level_key.gif
new Bitmap(new levelKeyBitmapData(32,4));
private var levelData:Bitmap = // level_00.gif
new Bitmap(new levelDataBitmapData(15,640));
Вот как должна выглядеть ваша палитра библиотеке, когда вы закончите:
Что же произошло?Также как до этого, всё, что мы сделали это встроили требующиеся арт активы в наш Flash-файл. Обратите внимание на имена, которые были использованы и помните, что каждое сделано, чтобы быть экспортированным в ActionScript в кадр 1.
Время для действий - встраиваем все меши.
Последний шаг, требующийся, чтобы включить арты, используемые в нашей игре, это встроить .OBJ файлы, которые определяют всю геометрию меша. Этот шаг одинаков, не важно в какой среде разработке вы работаете.
//данные маеше
//игрок - 142 полигона
[Embed (source = "art/spaceship_funky.obj",
mimeType = "application/octet-stream")]
private var myObjData5:Class;
// the engine glow - 336 polygons
[Embed (source = "art/puff.obj",
mimeType = "application/octet-stream")]
private var puffObjData:Class;
После мешей, используемых космичесаким кораблём игрока, мы встроим различные меши используемые для игровой среды - земля, небо и поле астеройдов.
//Данные меша поверхности - 8192 полигона
[Embed (source = "art/terrain.obj",
mimeType = "application/octet-stream")]
private var terrainObjData:Class;
// поле астероидов - 3280 полигонов
[Embed (source = "art/asteroids.obj",
mimeType = "application/octet-stream")]
private var asteroidsObjData:Class;
// небо - 768 полигонов
[Embed (source = "art/sky.obj",
mimeType = "application/octet-stream")]
private var skyObjData:Class;
Теперь мы добавим меши для использования в наших системах частиц и для пуль
//начало взрыва - 336 полигонов
[Embed (source = "art/explosion1.obj",
mimeType = "application/octet-stream")]
private var explosion1_data:Class;
// конец взрыва - 336 полигонов
[Embed (source = "art/explosion2.obj",
mimeType = "application/octet-stream")]
private var explosion2_data:Class;
// "густой" взрыв - 336 полигонов
[Embed (source = "art/explosionfat.obj",
mimeType = "application/octet-stream")]
private var explosionfat_data:Class;
// пуля- 8 полигонов
[Embed (source = "art/bullet.obj",
mimeType = "application/octet-stream")]
private var bulletData:Class;
Мы добавим модели для врагов, которые мы хотим разрушить: несколько караульных пушек, топливные баки и огромный кораблю босса для разнообразия.
//караульные пушки - все разделяют одну текстуру
[Embed (source = "art/sentrygun01.obj",
mimeType = "application/octet-stream")]
private var sentrygun01_data:Class;
[Embed (source = "art/sentrygun02.obj",
mimeType = "application/octet-stream")]
private var sentrygun02_data:Class;
[Embed (source = "art/sentrygun03.obj",
mimeType = "application/octet-stream")]
private var sentrygun03_data:Class;
[Embed (source = "art/sentrygun04.obj",
mimeType = "application/octet-stream")]
private var sentrygun04_data:Class;
// топливные пушки - используют одну текстуру также как караул
[Embed (source = "art/fueltank01.obj",
mimeType = "application/octet-stream")]
private var fueltank01_data:Class;
[Embed (source = "art/fueltank02.obj",
mimeType = "application/octet-stream")]
private var fueltank02_data:Class;
// огромный корабль босса
[Embed (source = "art/boss.obj",
mimeType = "application/octet-stream")]
private var bossData:Class;
Кроме того, мы хотим, чтобы там была группа астероидов, разбросанных по всему уровню, чтобы игроку нужно было уворачиваться или разрушиться.
private var astroTunnelData:Class;
// отдельный астероид
[Embed (source = "art/asteroid_00.obj",
mimeType = "application/octet-stream")]
private var asteroid_00_data:Class;
[Embed (source = "art/asteroid_01.obj",
mimeType = "application/octet-stream")]
private var asteroid_01_data:Class;
[Embed (source = "art/asteroid_02.obj",
mimeType = "application/octet-stream")]
private var asteroid_02_data:Class;
// огромный плоский "стол" (круглая скала с плоской вершиной)
[Embed (source = "art/island.obj",
mimeType = "application/octet-stream")]
private var islandData:Class;
Чтобы добавить больше визуального интереса, огромные космические станции будут летать в стороне от действия. Они предназначены ни для чего больше, кроме как привлекательности заднего плана.
//космические станции и "плиты технологий"
[Embed (source = "art/slab01.obj",
mimeType = "application/octet-stream")]
private var slab01_data:Class;
[Embed (source = "art/slab02.obj",
mimeType = "application/octet-stream")]
private var slab02_data:Class;
[Embed (source = "art/slab03.obj",
mimeType = "application/octet-stream")]
private var slab03_data:Class;
[Embed (source = "art/slab04.obj",
mimeType = "application/octet-stream")]
private var slab04_data:Class;
[Embed (source = "art/station01.obj",
mimeType = "application/octet-stream")]
private var station01_data:Class;
[Embed (source = "art/station02.obj",
mimeType = "application/octet-stream")]
private var station02_data:Class;
[Embed (source = "art/station03.obj",
mimeType = "application/octet-stream")]
private var station03_data:Class;
[Embed (source = "art/station04.obj",
mimeType = "application/octet-stream")]
private var station04_data:Class;
[Embed (source = "art/station05.obj",
mimeType = "application/octet-stream")]
private var station05_data:Class;
[Embed (source = "art/station06.obj",
mimeType = "application/octet-stream")]
private var station06_data:Class;
[Embed (source = "art/station07.obj",
mimeType = "application/octet-stream")]
private var station07_data:Class;
[Embed (source = "art/station08.obj",
mimeType = "application/octet-stream")]
private var station08_data:Class;
[Embed (source = "art/station09.obj",
mimeType = "application/octet-stream")]
private var station09_data:Class;
[Embed (source = "art/station10.obj",
mimeType = "application/octet-stream")]
private var station10_data:Class;
Наконец, нам нужно что-то, чтобы представть "финишную линию" нашей простой игры. В самом конце уровня будет вращающаяся чревоточина, которая предоставляет средство побега и служит целью.
//вращающаяся чревоточина
[Embed (source = "art/wormhole.obj",
mimeType = "application/octet-stream")]
private var wormholeData:Class;
Время для действий - отслеживаем арт-активы.
Предыдущие встроенные данные артов будут превращены в текстуры и звуки для использования в игре. Нам нужно отслеживать эти объекты, сохраняя их в следующие переменные:
//все текстуры используемые в игре
private var playerTexture:Texture;
private var terrainTexture:Texture;
private var cratersTexture:Texture;
private var skyTexture:Texture;
private var puffTexture:Texture;
private var particle1texture:Texture;
private var particle2texture:Texture;
private var particle3texture:Texture;
private var particle4texture:Texture;
private var badguytexture:Texture;
private var techtexture:Texture;
// звуки, используемые в игре
public var introSound:Sound = (new introMp3) as Sound;
public var blastSound:Sound = (new blastMp3) as Sound;
public var explodeSound:Sound = (new explodeMp3) as Sound;
public var gunSound:Sound = (new gunMp3) as Sound;
public var musicChannel:SoundChannel;
Что же произошло?
Это всё для связзанных с игрой переменных и встроенных арт-активов! Мы успешно втсроили все виды новых произведений искусства, включая битмапы использующиеся как GUI слои, так же как текстуры и 3D ьеши вместе со звуком и музыкой.
Обновление финального игрового кода.
Мы закончили с областью артов. В коде. Следующие разделы объяснят все изменения и обновления, которые требует наша игра, чтобы привести это к играбельному состоянию.
Время для действий - обновляем инициализацию.
Теперь, когда мы достигли финального этапа нашей разработки игры, давайте используем все лучшие практики для инициализации Stage3D, включая обнаружение предыдущих версий Flash , которые которые ещё не поддерживали это.
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);
//конструктор класса - устанавливает сцену
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.align = StageAlign.TOP_LEFT;
// добавляем текстовые метки
initGUI();
// мы запускам Flash 11 с доступным Stage3D?
var stage3DAvailable:Boolean =
ApplicationDomain.currentDomain.hasDefinition
("flash.display.Stage3D");
if (stage3DAvailable)
{
// запускаем игровой таймер
gametimer = new GameTimer(heartbeat,3333);
gameinput = new GameInput(stage, gamestart);
// используется для сохранения определённых декораций уровня
props = new Vector.<Stage3dEntity>();
// обнаруживает когда мы получили context3D
stage.stage3Ds[0].addEventListener(
Event.CONTEXT3D_CREATE, onContext3DCreate);
//определяет когда swf не использует wmode=direct
stage.stage3Ds[0].addEventListener(
ErrorEvent.ERROR, onStage3DError);
//теперь запрашиваем аппаратный 3d режим
stage.stage3Ds[0].requestContext3D();
}
else
{
trace("stage3DAvailable is false!");
titlescreenTf.text = 'Flash 11 Required.\n' +
'Your version: ' + Capabilities.version +
'\nThis game uses Stage3D.\n' +
'Please upgrade to Flash 11\n' +
'so you can play 3d games!';
}
}
Следующее событие ошибки запускается, когда swf не использует wmode=direct. Stage3D требует, чтобы тег embed в вашем HTML файле использовал этот параметр.
private function onStage3DError(e:ErrorEvent):void
{
trace("onStage3DError!");
titlescreenTf.text = 'Your Flash 11 settings\n' +
'have 3D turned OFF.\n' +
'Ensure that you use\n' +
'wmode=direct\n' +
'in your html file.';
}
Что же произошло?
Предыдущий код инициализации почти такой же как в предыдущих главах, за искоючением того, чтомы учли для нашего заглавного экрана и игровых состояний, как обсуждалось ранее. Игра стартует в режиме "паузы" и не начинается до тех пор пока игрок не кликнет на заглавном экране, чтобы начать.
Поэтому мы добавили ссылку на функцию gamestart в конструктор gameinput в верхней части вводного алгоритма.
Мы также реализовали ещё немного логики обнаружения ошибок, чем до этого,совершая проверку действительно ли версия Flash, используемая пользователем, поддерживает Stage3D. Если нет, нам нужно просто вывести сообение об ошибке, так как игра не запустится в Flash 10.
Кроме того, мы также создали событие для обнаружения любых специфических ошибок Flash 11, а также сообщения о них.
Чуть-чуть полировки как эта является различием между техническим демо и законченной игрой, которая изящно обрабатывает ошибки во время инцииализации.
Время для действий - инициализация Stage3D.
Так же как в предыдущих главах, эта функция делает всю тяжёлую работу, когда она подготавливает Flash для 3D графики:
private function onContext3DCreate(event:Event):void
{
// Удаляем существующий обработчик кадров. Заметьте что утрата
// контекста может произойти в любое время, что заставит вас
// пересоздать все объяекты, которые мы создали здесь
// Потеря контекста происходит мгновенно если вы нажмёте
// CTRL-ALT-DELETE в Windows.
// Это занимает какое-то время пока новыый контест станет доступен.
// следовательно удаление обработчика enterFrame важно!
if (hasEventListener(Event.ENTER_FRAME))
removeEventListener(Event.ENTER_FRAME,enterFrame);
// Получаем текущий контекст
var mystage3D:Stage3D = event.target as Stage3D;
context3D = mystage3D.context3D;
if (context3D == null)
{
// В настоящий момент нет доступного 3d контекста (ошибка!)
trace('ERROR: no context3D - video driver problem?');
return;
}
// определяем режим ПО (html может не иметь wmode=direct)
if ((context3D.driverInfo == Context3DRenderMode.SOFTWARE)
|| (context3D.driverInfo.indexOf('oftware')>-1))
{
//Context3DRenderMode.AUTO
trace("Software mode detected!");
titlescreenTf.text = 'Your Flash 11 settings\n' +
'have 3D turned OFF.\n' +
'Ensure that you are\n' +
'using a shader 2.0\n' +
'compatible 3d card.';
}
//если это слишком большое, это изменяет размер сцены!
gpuTf.text = 'Flash Version: ' + Capabilities.version +
' - Game ' + VERSION_STRING +
' - 3D mode: ' + context3D.driverInfo;
Отключение провреки ошибок коренным образом повысит производительность. Так как это теперь финальная версия нашей игры, мы больше не хотим работать в форме "режима отладки". Всё, что мы хотим с этого момента, это быстрейшая из возможных скорость рендеринга. Вы можете захотеть оставить эту установку на true пока игра работает прекрасно - просто не забудьте отключиить проверку ошибок в финальной игре, которую вы отправляете друзьям или загружаете на вэб-сайт.
//если установлено в true, Flash отправляет полезные сообщения об ошибках, относящиеся к
//ошибкам компиляции AGAL, неинициализированным программным константам и т.д.
context3D.enableErrorChecking = false;
// Размер заднего 3d буфера в пикселях (2=сглаживание)
context3D.configureBackBuffer(stage.width, stage.height, 2, true);
// собираем все шейдеры, которые нам нужны
initShaders();
// инициализируем все текстуры
playerTexture = initTexture(playerTextureData);
terrainTexture = initTexture(terrainTextureData);
cratersTexture = initTexture(cratersTextureData);
puffTexture = initTexture(puffTextureData);
skyTexture = initTexture(skyTextureData);
particle1texture = initTexture(particle1bitmap);
particle2texture = initTexture(particle2bitmap);
particle3texture = initTexture(particle3bitmap);
particle4texture = initTexture(particle4bitmap);
badguytexture = initTexture(badguyTextureBitmap);
techtexture = initTexture(techTextureBitmap);
// Инициализируем данные нашего меша - первыми требуются шейдеры и текстуры
initData();
// создаём матрицу проекции для нашей 3D сцены
projectionmatrix.identity();
// 45 градусов FOV, 640/480 соотношение сторон, 0.1=ближний, 150000=дальний
projectionmatrix.perspectiveFieldOfViewRH(
45, stage.width / stage.height, 0.01, 150000.0);
// запускаем цикл рендеринга!
addEventListener(Event.ENTER_FRAME,enterFrame);
Что же произошло?
Как и до этого, функция onContext3DCreate проверяет, что все данные, требуемые нашей игрой загружены в видеокарту. Все текстуры созданы, меши обработаны и сохранены в видеопамяти, готовые к рендерингу. Шейдеры скомпилированы и всё подготовлено для рендеринга.
Одно больше различие по сравнению с всеми предыдущими итерациями этой функции то, что мы выключили context3D.enableErrorChecking для лучшей производительности. Это строка очень важна и должна быть установлена на false только тогда, когда игра готова к публичного использования и вы хотите максимально возможную производительность.
Причина того, что context3D.enableErrorChecking = false улучшает производительность, это то, что когда проверка ошибок выключена, рендеринг может происходить параллельно с исполнением другого кода, вместо "блокировки" дальнейшего кода, необходимостью ждать, чтобы увидеть есть ли ошибки. В сложных играх это неблокирующее поведение означает, что ваша графическая карта будет счастливо отрисовывать вещи в то время как ваша Flash игра будет занята отрисовыванием других вещей: в связи с этим, пользуются аппаратным ускорением 3D графики, работая в GPU и CPU одновременно.
Время для действий - обновление функции initGUI.
Наша финальная функция initGUI очень похожа на те из предыдущих глав, за исключением того, что сейчас мы включили наш наше наложенное изображение заглавного экрана, а также используем встроенный шрифт с фантастическими эффектами свечения. С надеждой, что это заставит вещи выглядеть более похожими на игру и профессионально.
private function initGUI():void
{
// заглавный экран
addChild(titleScreen);
var myFont:Font = new GUI_FONT();
Теперь мы добавим заглавный экран с уведомлением "игра окончена"
var gameoverFormat:TextFormat = new TextFormat();
gameoverFormat.color = 0x000000;
gameoverFormat.size = 24;
gameoverFormat.font = myFont.fontName;
gameoverFormat.align = TextFormatAlign.CENTER;
titlescreenTf = new TextField();
titlescreenTf.embedFonts = true;
titlescreenTf.x = 0;
titlescreenTf.y = 180;
titlescreenTf.width = 640;
titlescreenTf.height = 300;
titlescreenTf.autoSize = TextFieldAutoSize.NONE;
titlescreenTf.selectable = false;
titlescreenTf.defaultTextFormat = gameoverFormat;
titlescreenTf.filters = [
new GlowFilter(0xFF0000, 1, 2, 2, 2, 2),
new GlowFilter(0xFFFFFF, 1, 16, 16, 2, 2)];
titlescreenTf.text = "CLICK TO PLAY\n\n"
+ "Your objective:\n"
+ "reach the wormhole!";
addChild(titlescreenTf);
Мы добавляем слой heads-up-дисплея под экраном.
addChild(hudOverlay);
Следующий код устанавливает дескриптор тестового формата, используемый всеми метками GUI. Он использует шрифт, который мы встроили ранее.
var myFormat:TextFormat = new TextFormat();
myFormat.color = 0xFFFFAA;
myFormat.size = 16;
myFormat.font = myFont.fontName;
Затем мы создаём FPSCounter , который отображает частоту кадров на экране следующим образом:
fpsTf = new TextField();
fpsTf.embedFonts = true;
fpsTf.x = 0;
fpsTf.y = 0;
fpsTf.selectable = false;
fpsTf.autoSize = TextFieldAutoSize.LEFT;
fpsTf.defaultTextFormat = myFormat;
fpsTf.filters = [new GlowFilter(0x000000, 1, 2, 2, 2, 2)];
fpsTf.text = "Initializing Stage3d...";
addChild(fpsTf);
Мы также инициализируем хорошо отформатированный, светящийся дисплей очков, дисплей отладки, который находится внизу экрана, чтобы слушать подробности о драйвере и версии Flash, и измеритель здоровья.
Вместо использования битмапов, измеритель здоровья сделан из текста для простоты.
scoreTf = new TextField();
scoreTf.embedFonts = true;
scoreTf.x = 540;
scoreTf.y = 0;
scoreTf.selectable = false;
scoreTf.autoSize = TextFieldAutoSize.LEFT;
scoreTf.defaultTextFormat = myFormat;
scoreTf.filters = [new GlowFilter(0xFF0000, 1, 2, 2, 2, 2)];
scoreTf.text = VERSION_STRING;
addChild(scoreTf);
// отладочная информация: список подробностей видеокарты
var gpuFormat:TextFormat = new TextFormat();
gpuFormat.color = 0xFFFFFF;
gpuFormat.size = 10;
gpuFormat.font = myFont.fontName;
gpuFormat.align = TextFormatAlign.CENTER;
gpuTf = new TextField();
gpuTf.embedFonts = true;
gpuTf.x = 0;
gpuTf.y = stage.height - 16;
gpuTf.selectable = false;
gpuTf.width = stage.width-4;
gpuTf.autoSize = TextFieldAutoSize.NONE;
gpuTf.defaultTextFormat = gpuFormat;
gpuTf.text = "Flash Version: " +
Capabilities.version + " - Game " + VERSION_STRING;
addChild(gpuTf);
// добавляем измеритель здоровья
healthTf = new TextField();
healthTf.embedFonts = true;
healthTf.x = 232;
healthTf.y = 3;
healthTf.selectable = false;
healthTf.autoSize = TextFieldAutoSize.LEFT;
healthTf.defaultTextFormat = myFormat;
healthTf.filters = [new GlowFilter(0xFFFFFF, 1, 4, 4, 4, 2)];
healthTf.text = healthText_3;
addChild(healthTf);
Что же произошло?
Предыдущий код будет обновлять наш GUI и включать стартовый экран, который ждёт пока пользователь кликнет, до того, как начать геймлей. Это должно выглядеть так:
Время для действий - обновление инициализации текстур.
в предыдущей версии нашего игрового движка, каждая текстура инициализировалась, используя очень похожий код. Как известно программистам, если код копируется и вставляется более двух раз в одно месте, нужно рассмотреть создание абстрактной функции, которая может делать эту работу.
ublic function uploadTextureWithMipmaps(
dest:Texture, src:BitmapData):void
{
var ws:int = src.width;
var hs:int = src.height;
var level:int = 0;
var tmp:BitmapData;
var transform:Matrix = new Matrix();
tmp = new BitmapData( src.width, src.height, true, 0x00000000);
while ( ws >= 1 && hs >= 1 )
{
tmp.draw(src, transform, null, null, null, true);
dest.uploadFromBitmapData(tmp, level);
transform.scale(0.5, 0.5);
level++;
ws >>= 1;
hs >>= 1;
if (hs && ws)
{
tmp.dispose();
tmp = new BitmapData(ws, hs, true, 0x00000000);
}
}
tmp.dispose();
}
private function initTexture(bmp:Bitmap):Texture
{
var tex:Texture;
tex = context3D.createTexture(bmp.width, bmp.height,
Context3DTextureFormat.BGRA, false);
uploadTextureWithMipmaps(tex, bmp.bitmapData);
return tex;
}
Что же произошло?
uploadTextureWithMipmaps остаётся идентичной с последнего раза, но теперь мы реализовали новую фантастическую функцию initTexture, так что мы можем создавать множество текстур без избыточно повторяющегося кода.
Время для действий - обновляем шейдеры.
Алгоритм initShader из предыдущих глав был прекрасен, но, чтобы добавить больше професионализма в наш рендеринг, мы добавим второй шейдер с динамическим освещением.
private function initShaders():void
{
trace("initShaders...");
// Простой вершинный шейдер, который делает 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\n" +
// v2 рассказываем фрагментному шейдеру о нормалях
"mov v3, va3"
);
// текстурируем используя UV координаты
var fragmentShaderAssembler:AGALMiniAssembler
= new AGALMiniAssembler();
fragmentShaderAssembler.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,
fragmentShaderAssembler.agalcode);
trace("Shader 1 uploaded.");
Предыдущий код дословно такой же как был в предыдущих главах. Он создаёт фргаментную и вершинную программы, которые просто рисуют текстурированные полигоны на экране.
//это простой текстурный шейдер
//с динамическим освещением
trace("Compiling lit vertex program...");
var litVertexShaderAssembler:AGALMiniAssembler =
new AGALMiniAssembler();
litVertexShaderAssembler.assemble
(
Context3DProgramType.VERTEX,
// 4x4 матричное умножение, чтобы получить угол камеры
"m44 vt0, va0, vc0\n" +
// позиция вывода
"mov op, vt0 \n"+
// расказываем фрагментному шейдеру о XYZ
"mov v0, va0\n" +
// рассказываем фрагментному шейдеру UV
"mov v1, va1\n" +
// рассказываем фрагментному шейдеру о RGBA
"mov v2, va2\n" +
// рассказываем фрагментному шейдеру о нормалях
// независмио от угла камеры:
// для простейшего света
"mov v3, va3"
);
Вершинная программа для нашего динамически освещённого шейдера больше похожа на предыдущие, за исключением того, что мы посылаем нормали для кажой вершины в нашу фрагментную программу, так что она знает в каокм направлении каждый полигон повернути может оттенять наши модели подходящим образом, в зависимости от угла света.
//текстурировано используя UV координаты и LIT со статическим светом
trace("Compiling lit fragment program...");
var litFragmentShaderAssembler:AGALMiniAssembler
= new AGALMiniAssembler();
litFragmentShaderAssembler.assemble
(
Context3DProgramType.FRAGMENT,
// захватываем цвет текстуры из текстуры 0
// и uv координаты из переменного регистра 1
// и сохраняем интерполированное значение в ft0
"tex ft0, v1, fs0 <2d,linear,repeat,miplinear>\n"+
//вычисляем скалярное произведение вертикальной нормали и нормали света
// fc15 = нормаль света
"dp3 ft1 fc15 v3\n" +
// добавляем окружающий свет (fc16)
"add ft1 ft1 fc16\n" +
// умножаем цвет текстуры на значение света
"mul ft0 ft0 ft1\n" +
// перемещаем от значение в цвет на вывод
"mov oc, ft0\n"
);
trace("Uploading lit shader...");
shaderWithLight = context3D.createProgram();
shaderWithLight.upload(
litVertexShaderAssembler.agalcode,
litFragmentShaderAssembler.agalcode);
Фрагментная программа для нашего шейдера освещения не только собирает текстуры больше как предыдущие примеры, но также учитывает нормаль для каждой вершины и сравнивает её с нормалью света (направление света).
Наконец она добавляет окружающий свет в это значение, чтобы прийти к конечному значению света, получая тот конкретный фрагмент и затем умножает его на цвет извлечённой текстуры, эффективно "окрашивая" текстуру, так что области, не сталкивающиеся со световым источником будут темнее.
Нормаль света и цвет окружающего света сохраняются в регистрах фрагментной програмы fc15 и fc16 и определены ниже. Исправляя эти значения, вы можете заставить целую сцену выглядеть как если она была освещена цветным светом и заставить тени появляться у поверхности под различным углом.
trace("Setting lit shader constants...");
// для простоты позиция света никогда не меняется
// так что мы можем установить её здесь однажды вместо каждого кадра
var lightDirection:Vector3D = new Vector3D(1.5, 1, -2, 0);
lightDirection.normalize();
var lightvector:Vector.<Number> = new Vector.<Number>();
lightvector[0] = lightDirection.x;
lightvector[1] = lightDirection.y;
lightvector[2] = lightDirection.z;
lightvector[3] = lightDirection.w;
trace("Light normal will be: " + lightvector);
// fc15 = [нормаль света]
context3D.setProgramConstantsFromVector(
Context3DProgramType.FRAGMENT, 15, lightvector);
// fc16 = окружающий свет
var lightambient:Vector.<Number> = new Vector.<Number>();
lightambient[0] = 0.2;
lightambient[1] = 0.2;
lightambient[2] = 0.2;
lightambient[3] = 0.2;
context3D.setProgramConstantsFromVector(
Context3DProgramType.FRAGMENT, 16, lightambient);
}
Что же произошло?
Первый шейдер, как видно из предыдущей главы, просто рендерит каждый меш с текстурой с полной яркостю. Для того, чтобы дать добавленную глубенну нашим моделям, второой шейдер shaderWithLight чуть-чуть углубляется в AGAL. Он учитывает нормаль каждой вершины (направление, которое она указывает) и подсчитывает количество света, которое наш меш должен получить.
Этим способом, в зависимости от угла света, который определяется переменной lightvector и загружается в видеокарту как вершинный константный регистр fc15, каждая поверхность будет светлее или темнее в зависимости от угла, под которым он падает.
Чтобы осветить сцену, некоторое количество света, также добавляется в каждый пиксель, который выводится нашим фрагментным шейдером. Вы могли бы, например, сделать сцену немного краснее, увеличивая первое число в переменной lightambient.
Это наиболее базовый из возможных шейдер динамического освещения. Для того, чтобы сделать свет поистине динамическим вы можете использовать setProgramConstantsFromVector, чтобы загружать новый lightvector в каждом кадре, так чтобы значения сохранялись в fc15, постоянно изменяясь. Сейчас этот источник света никогда не будет перемещаться и достаточно определить fc15 однажды, во время инициализации, вместо того, чтобы делать это во время цикла рендеринга.
Как гайд для начинающих, эта книга не может слишком глубоко копаться в суперсложных AGAL шейдерах, которые сами по себе стоят целой книги. Этого должно быть достаточно, чтобы дать вам направление старта в создании фантастических шейдеров, которые используют множество источников света и многое другое.
Время для действий - определяем новые типы действующих лиц и поведение.
Это, наконец, время инструктировать нашу игру о всех мешах, в которых наша игра нуждается. Так как мы будем использовать нашу новую систему бассейна действующих лиц, чтобы генерировать множество копий определённой сущности, мы обычно создаём одно "оригинальное" действующее лицо, которое используется как определение того конкретного вида действующего лица.
private function initData():void
{
// создаём бассейны объектов
particleSystem = new GameParticlesystem();
enemies = new GameActorpool();
playerBullets = new GameActorpool();
enemyBullets = new GameActorpool();
// создаём сущность камеры
chaseCamera = new Stage3dEntity();
// космический корабль игрока и свечение двигателя
initPlayerModel();
// неинтерактивная поверхность,поле астероидов, небо
initTerrain();
// злые корабли, сторожевые пушки, топливные баки
initEnemies();
// добрые и злые пули
initBullets();
// различные астероиды, в которые можно стрелять
initAsteroids();
// космические станции и плиты "техники"
initSpaceStations();
// взрывы, искры, мусор
initParticleModels();
}
Что же произошло?
Мы с начала создаём несколько пустых бассейнов объектов, чтобы извлечь выгоду из наших высоко оптимизированных и эффективно повторно используемых классов сущностей из предыдущих глав. Затем создаётся преследующая камера, которая просто Stage3dEntity, которая никогда не отрисовывается потому, что не имеет мешей, ассоциированных с ней.
Наконец, мы определим множество типов игровых сущностей; всё, от корбаля игрока до всех видов плохих парней и реквезита уровня, каждый с немного раздичными мешами, шейдерами, скоростями движения и поведением ИИ. Некоторые будут неинтерактивными и просто хорошо выглядят там. Другие могут двигаться, стрелять и наносить урон игроку при попадании.
Для читабельности эти установки сущностей были разделены на более мелкие функции. Взгляните на то, как различные виды сущностей определяются изменением свойств объекта. Заметьте, что каждому даётся строковое имя, которое используется позже, для порождения множества копий этого "оригинального" источника клонов.
Это значит, что большинство сущностей и частиц, определённых ниже, никогда не появляются в игре: они используются как определение конкретного вида игрового действующего лица и во время выполнения, бассейн объектов будет клонировать и затем перерождать их по мере необходимости.
Время для действий - инициализируем меши поверхности.
Мы начнём с установки нескольких неинтерактивных декоративных мешей. Не необхадимости проверять столкновения с этими мешами, так как они вне игровой области и предназначены просто для хорошего вида.
private function initTerrain():void
{
trace("Parsing the sky...");
sky = new Stage3dEntity(
skyObjData,
context3D,
shaderProgram1,
skyTexture);
sky.depthTest = false;
sky.depthTestMode = Context3DCompareMode.LESS;
sky.cullingMode = Context3DTriangleFace.NONE;
sky.depthDraw = false;
sky.z = -10000.0;
sky.scaleX = 20;
sky.scaleY = 10;
sky.scaleZ = 20;
sky.rotationDegreesY = -90;
sky.rotationDegreesX = -90;
props.push(sky);
После создания гигантской внутренней сферы для использования как бэкграунда "неба" мы создаём меш поверхности и добавляем его в список "реквизита" следующим образом:
trace("Parsing the terrain...");
var terrain:Stage3dEntity =
new Stage3dEntity(
terrainObjData,
context3D,
shaderProgram1,
terrainTexture, 0.5);
terrain.rotationDegreesZ = 90;
terrain.y = -50;
props.push(terrain);
Затем мы делаем то же самое для нескольких мешей астероидов
trace("Parsing the asteroid field...");
asteroids1 = new Stage3dEntity(
asteroidsObjData,
context3D,
shaderWithLight,
cratersTexture);
asteroids1.rotationDegreesZ = 90;
asteroids1.scaleXYZ = 200;
asteroids1.y = 500;
asteroids1.z = -1100;
props.push(asteroids1);
trace("Cloning the asteroid field...");
// используем один и тот же меш во множестве локаций
asteroids2 = asteroids1.clone();
asteroids2.z = -1500;
props.push(asteroids2);
asteroids3 = asteroids1.clone();
asteroids3.z = -1900;
props.push(asteroids3);
asteroids4 = asteroids1.clone();
asteroids4.z = -1900;
asteroids4.y = -500;
props.push(asteroids4);
Наконец мы создаём определение действующего лица для скалистых "островов", которые плавают в космосе и используются, чтобы разместить на них сторожевые пушки и топливные баки, так же как и "финишную черту", которая вращается эффект червоточины.
actor = new GameActor(
islandData,
context3D,
shaderWithLight,
cratersTexture, 1, false);
actor.shaderUsesNormals = true;
actor.collides = false;
actor.radius = 64;
enemies.defineActor("island", actor);
actor = new GameActor(
wormholeData,
context3D,
shaderProgram1,
particle1texture,
0.1);
actor.collides = false;
actor.radius = 128;
actor.blendSrc = Context3DBlendFactor.ONE;
actor.blendDst = Context3DBlendFactor.ONE;
actor.cullingMode = Context3DTriangleFace.NONE;
actor.depthTestMode = Context3DCompareMode.LESS;
actor.depthDraw = false;
actor.depthTest = false;
actor.rotVelocity = new Vector3D(0, 0, -120); // spinning
enemies.defineActor("wormhole", actor);
}
Что же произошло?
Действующие лица, определённые выше, используются, чтобы сделать игровой мир более привлекательным. Они настроены не сталкиваться с игроком и, таким образом, никогда не нуждаются в проверке на столкновения во время цикла рендеринга. Небо настроено так, чтобы не записываться в z-буфер, так что оно всегда на заднем плане и никогда не заслоняет другие внутриигровые объекты. "Финишной черте" червоточины даётся rotVelocity с большим значением в оси z, так что она вращается, а также специальные режимы наложения должны быть прозрачными.
Время для действий - инициализируем сущности.
Враги в игре включают в себя корабли и другие разрушаемые объекты. Вражеские крейсеры должны двигаться к игроку и вращаться, а также будут стрелять в игрока.
private function initEnemies():void
{
// создаём врагов
actor = new GameActor(
myObjData5,
context3D,
shaderWithLight,
badguytexture,4);
actor.collides = true;
actor.radius = 2;
actor.particles = particleSystem;
actor.bullets = enemyBullets;
actor.shootName = 'evilbolt';
actor.shootDelay = 500;
actor.shootRandomDelay = 1500;
actor.shootDist = 250;
actor.shootVelocity = 35;
actor.shootAt = player;
actor.posVelocity = new Vector3D(0, 0, 20); // лететь к нам
actor.rotVelocity = new Vector3D(0, 0, 40); // вращение
enemies.defineActor("enemyCruiser", actor);
Предыдущее определение действующего лица устанавливает обычный вражеский корабль. Он будет стрелять в игрока, вращаться, двигаться к камере и испускать частицы во время стрельбы или разрушения. Теперь мы определяем похожее действующее лицо, но на этот раз это большой босс, который летит назад, принимая несколько прямых попаданий, чтобы разрушиться, зарабатывает очки для игрока и стреляет намного чаще.
actor = new GameActor(
bossData,
context3D,
shaderWithLight,
badguytexture, 32); // масштабируем
actor.collides = true;
actor.radius = 16;
actor.posVelocity = new Vector3D(0, 0.5, -20);
actor.particles = particleSystem;
actor.bullets = enemyBullets;
actor.shootName = 'evilbolt';
actor.shootDelay = 250;
actor.shootRandomDelay = 250;
actor.shootDist = 250;
actor.shootVelocity = 30; // пули движутся медленно
actor.shootAt = player;
actor.points = 1111; // награда за победу
actor.health = 20; // #удары для разрушения
enemies.defineActor("enemyBoss", actor);
Затем мы определяем группу сторожевых пушек, которые не движутся, но стреляют в игрока
actor = new GameActor(
sentrygun01_data,
context3D,
shaderWithLight,
badguytexture,4);
actor.collides = true;
actor.radius = 2;
actor.particles = particleSystem;
actor.bullets = enemyBullets;
actor.shootName = 'evilbolt';
actor.shootDelay = 500;
actor.shootRandomDelay = 1500;
ctor.shootDist = 250;
actor.shootVelocity = 25;
actor.shootAt = player;
enemies.defineActor("sentrygun01", actor);
actor = new GameActor(
sentrygun02_data,
context3D,
shaderWithLight,
badguytexture,4);
actor.collides = true;
actor.radius = 2;
actor.particles = particleSystem;
actor.bullets = enemyBullets;
actor.shootName = 'evilbolt';
actor.shootDelay = 500;
actor.shootRandomDelay = 1500;
actor.shootDist = 250;
actor.shootVelocity = 25;
actor.shootAt = player;
enemies.defineActor("sentrygun02", actor);
actor = new GameActor(
sentrygun03_data,
context3D,
shaderWithLight,
badguytexture,4);
actor.collides = true;
actor.radius = 2;
actor.particles = particleSystem;
actor.bullets = enemyBullets;
actor.shootName = 'evilbolt';
actor.shootDelay = 500;
actor.shootRandomDelay = 1500;
actor.shootDist = 250;
actor.shootVelocity = 25;
actor.shootAt = player;
enemies.defineActor("sentrygun03", actor);
actor = new GameActor(
sentrygun04_data,
context3D,
shaderWithLight,
badguytexture,4);
actor.collides = true;
actor.radius = 2;
actor.particles = particleSystem;
actor.bullets = enemyBullets;
actor.shootName = 'evilbolt';
actor.shootDelay = 500;
actor.shootRandomDelay = 1500;
actor.shootDist = 250;
actor.shootVelocity = 25;
actor.shootAt = player;
enemies.defineActor("sentrygun04", actor);
Наконец, мы определяем типы действующих лиц для нескольких разрушаемых "топливных баков", которые там просто, чтобы дать игроку цели для разрушения, которые не сопротивляются.
actor = new GameActor(
fueltank01_data,
context3D,
shaderWithLight,
badguytexture,4);
actor.collides = true;
actor.radius = 2;
enemies.defineActor("fueltank01", actor);
actor = new GameActor(
fueltank02_data,
context3D,
shaderWithLight,
badguytexture,4);
actor.collides = true;
actor.radius = 2;
enemies.defineActor("fueltank02", actor);
Что же произошло?
Различные враги в нашей игре определены выше. При установке свойства столкновений на true, эти действующие лица будут проверяться на столкновения с игроком и пулями игрока. Если они подбиты, они будут разрушены и могут наносить урон кораблю игрока при столкновении. Свойства радиуса убеждаются, что столкновения проверяются в соответствующих границах, а также помогают определить как далеко от камеры должно быть каждое действующее лицо до того, как оно будет считаться слишком далеко, чтобы волноваться о рендеринге, который увеличивает производительность.
Вражеским крейсерам даны дополнительные свойства ИИ, которые заставляют их стрелять в игрока, если они достаточно близко. Определив частицы, излучаемые, когда они разрушаются и имена действующих лиц для пуль, снарядов которыми они будут стрелять, мы создали движение, сталкиваемый автономный тип врагов, который может быть разрушен, будет зарабатывать очки для игрока во время выстрела и анимироваться, чтобы лететь вперёд вращаясь.
Время для действий - инициализируем пули.
Нам нужно определить только два типа пуль для этой простой демо-игры. Первый вид пуль будет блестящим синим стержнем энергии, которым игрок стреляет во врагов. Второй - красный энергетический стержень, выстреливаемый врагами в игрока.
private function initBullets():void
{
actor = new GameActor(
bulletData,
context3D,
shaderProgram1,
particle1texture);
actor.collides = true;
actor.radius = 1;
actor.ageMax = 5000;
actor.particles = particleSystem;
actor.spawnWhenNoHealth = "sparks";
actor.spawnWhenMaxAge = "sparks";
actor.spawnWhenCreated = "sparks";
actor.blendSrc = Context3DBlendFactor.ONE;
actor.blendDst = Context3DBlendFactor.ONE;
actor.cullingMode = Context3DTriangleFace.NONE;
actor.depthTestMode = Context3DCompareMode.LESS;
actor.depthDraw = false;
actor.depthTest = false;
actor.posVelocity = new Vector3D(0, 0, -100);
actor.runWhenMaxAge = resetCombo;
actor.shaderUsesNormals = false;
playerBullets.defineActor("bolt", actor);
actor = new GameActor(
bulletData,
context3D,
shaderProgram1,
particle2texture);
actor.collides = true;
actor.radius = 1;
actor.ageMax = 5000;
actor.particles = particleSystem;
actor.spawnWhenNoHealth = "sparks";
actor.spawnWhenMaxAge = "sparks";
actor.spawnWhenCreated = "sparks";
actor.blendSrc = Context3DBlendFactor.ONE;
actor.blendDst = Context3DBlendFactor.ONE;
actor.cullingMode = Context3DTriangleFace.NONE;
actor.depthTestMode = Context3DCompareMode.LESS;
actor.depthDraw = false;
actor.depthTest = false;
actor.posVelocity = new Vector3D(0, 0, 25);
actor.shaderUsesNormals = false;
enemyBullets.defineActor("evilbolt", actor);
}
Что же произошло?
Действующим лицам пуль даются специальные режимы наложения, которые делают их прозрачными и освещают сцену до прорисовки в z-буфере. Им даётся максимальный возраст в пять секунд, так что если они потеряют свою цель, они не будут лететь вечно, потребляя память и замедляя производительность. Каждой даётся скорость движения, так что они стреляют прямо от их владельца и взрываются в различные частицы, когда они попадают в цель.
Время для действий - инициализируем астероиды.
В дополнение к различным вражеским кораблям, которые движутся и стреляют в игрока, давайте определим несколько простых астероидов, которые могут быть разрушены или ударены, чтобы дать игроку ещё что-то, чтобы целиться и избегать.
private function initAsteroids():void
{
// нужно столкнуться 3 раза для разрушения
actor = new GameActor(
asteroid_00_data,
context3D,
shaderWithLight,
cratersTexture, 0.5);
actor.collides = true;
actor.radius = 6;
actor.health = 3;
actor.spawnWhenNoHealth = 'debris01,debris02,debris03';
actor.particles = particleSystem;
enemies.defineActor("asteroid_00", actor);
actor = new GameActor(
asteroid_01_data,
context3D,
shaderWithLight,
cratersTexture, 0.5);
actor.collides = true;
actor.radius = 6;
actor.health = 3;
actor.spawnWhenNoHealth = 'debris01,debris02,debris03';
actor.particles = particleSystem;
enemies.defineActor("asteroid_01", actor);
actor = new GameActor(
asteroid_02_data,
context3D,
shaderWithLight,
cratersTexture, 0.5);
actor.collides = true;
actor.radius = 6;
actor.health = 3;
actor.spawnWhenNoHealth = 'debris01,debris02,debris03';
actor.particles = particleSystem;
enemies.defineActor("asteroid_02", actor);
}
Что же произошло?
Мы определили несколько похожих астероидов, которые будут порождаться нашем парсером уровня. Эти астероиды будут свободно разбросаны по нашему игровому миру.
Время для действий - инициализация космических станций.
Во многом как предыдущая геометрия поверхности, эти космические станции (и простые "плиты" техники) предназначены для декорации заднего плана в нашей игре.
private function initSpaceStations():void
{
actor = new GameActor(
slab01_data,context3D,shaderWithLight,techtexture,1,false);
actor.collides = false;
actor.radius = 64;
enemies.defineActor("slab01", actor);
Повторите предыдущий код для каждой из четырёх "плит" и десяти объектов "космическая станция". Они все имеют одинаковые параметры и единственное различие это имя и используемые данные меша. Весь блок кода можно найти в .zip загруженном из исходного кода, котоорый идёт с книгой.
//и так далее от slab02 до slab04 и от station01 до station09...
actor = new GameActor(
station10_data,context3D,shaderWithLight,techtexture,1,false);
actor.collides = false;
actor.radius = 64;
enemies.defineActor("station10", actor);
}
Что же произошло?
Эти космические станции будут порождаться вне нашей игровой области и, таким образом, не будет сталкиваться. Они простые, не интерактивные меши, которые предназначены не для чего больше, кроме как добавить что-то интересное, чтобы посмотреть на то как мы движемся к нашей цели.
Время для действий - инициализируем модели частиц.
Финальный шаг в инициализации для наших арт-активов в игре это установить взрывы и другие спецэффекты.
private function initParticleModels():void
{
trace("Creating particle system models...");
// определяем типы частиц
particleSystem.defineParticle("explosion",
new Stage3dParticle(explosion1_data, context3D,
puffTexture, explosionfat_data));
particleSystem.defineParticle("bluebolt",
new Stage3dParticle(explosion1_data, context3D,
particle1texture, explosion2_data));
particleSystem.defineParticle("greenpuff",
new Stage3dParticle(explosion1_data, context3D,
particle2texture, explosion2_data));
particleSystem.defineParticle("ringfire",
new Stage3dParticle(explosion1_data, context3D,
particle3texture, explosion2_data));
particleSystem.defineParticle("sparks",
new Stage3dParticle(explosion1_data, context3D,
particle4texture, explosion2_data));
}
Что же произошло?
Эти частицы определены также как наши игровые действующие лица, но они используют класс Stage3dParticle, который оптимизирован для прозрачности, свечения, продуктивно симулированной системы частиц. Когда различные игровые действующие лица разрушаются, они будут использовать наш менеджер системы частиц, чтобы перерождать взрыв или искры как это подходит для их положения.
Это всё для установки наших данных. Все модели, текстуры, частицы, звуки и GUI изображения подходят. Следующий шаг это выстроить уровень из всех этих прекрасных активов.
Время для действий - создание игрового уровня.
После того как всех различные типы действующих лиц определены, от вращающихся астероидов до пуль, сторожевых пушек и декораций, мы используем наш новый GameLevelParser, чтобы порождать врагов в подходящих местоположениях в соответствии с нашим изображением карты уровня.
private function spawnTheLevel():void
{
// создаём новый уровень используя
// наше изображение карты как чертёж
trace("Spawning the game level parser...");
var level:GameLevelParser = new GameLevelParser();
// как далеко перед игроком первое действующее лицо
var levelStartDistance:Number = 350;
// списко сущностей, которые мы хотим порождать на спиральной карте
var enemyNames:Array =
["enemyCruiser","asteroid_00","asteroid_01","asteroid_02"];
// спиральное вражеское формирование - работает прекрасно
level.spawnActors(
levelKey.bitmapData,
levelData.bitmapData,
enemyNames,
enemies,
0, 16,
player.z - levelData.height * 16 - levelStartDistance,
4, 16, 0, 8);
// список сущностей, которые мы хотим порождать на плоской карте
var rockNames:Array =
[null,null,null,null, // skip the types used above
"sentrygun01","sentrygun02","sentrygun03","sentrygun04",
"fueltank01","fueltank02","enemyBoss","island",
"slab01","slab02","slab03","slab04",
"station01","station02","station03","station04",
"station05","station06","station07","station08",
"station09","station10","wormhole","astroTunnel"];
// плоское вражеское формирование
var gameLevelLength:Number;
gameLevelLength = level.spawnActors(
levelKey.bitmapData,
levelData.bitmapData,
rockNames,
enemies,
-8, 0,
player.z - levelData.height * 8 - levelStartDistance,
2, 8, 0, 0);
// теперь, когда мы знаем насколько длинная карта
// запоминаем какая z координата "финишная черта"
playerMinz = player.z - gameLevelLength - levelStartDistance;
Что же произошло?
Мы создаём объект GameLevelParser и используем его, чтобы поместить действующие лица в положения, определённые нашим крошечным чертежом изображения карты. В этом конкретном примере мы парсим одно и то же изображения карты двумя различными способами, используя два массива имён. Оди установлен для порождения действующих лиц в "спиральной" формации. Все другие действующие лица уровня размещаются на плоской поверхности.
Изображение карты и ключевое изображение, которые вы используете для наложения вашего игрового уровня, могут выглядеть как-то так:
Масив имён, который вы строите, передаёт пиксели согласно ключевому изображению карты, так что первое вхождение в массив имён, который мы передаём нашему парсеру уровней это тип действующего лица, который совпадает с RGB значениями первого пикселя в нашем ключевом изображении карты.
Этим способом, например, если вы разбрасываете синие пиксели по нашему изображению карты и, затем, включите синий пиксель в пятую колонку нашего
"ключевого" изображения, пятое имя действующего лица в нашем массиве то, что будет порождаться там.
Время для действий - обновляем цикл рендеринга.
Наш игровой движок инициализировал Stage3D, наши арты были обработаны, наш уровень был сгенерирован, и теперь мы готовы обновить функцию, котрая делает рендеринг нашей сцены!
private function renderScene():void
{
scenePolycount = 0;
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);
// если мы встряхиваем экран (получили удар)
viewmatrix.appendRotation(
screenShakeCameraAngle, Vector3D.X_AXIS);
Предыдущий код устанавливает угол камеры. Затем мы рендерим меш игрока и всех врагов следующим образом:
player.render(viewmatrix, projectionmatrix);
scenePolycount += player.polycount;
// обходим все известные сущности и рендерим их
for each (entity in props)
{
entity.render(viewmatrix, projectionmatrix);
scenePolycount += entity.polycount;
}
Для того, чтобы повысить частоту кадров в нашей игре, мы скрываем врагов, которые слишком далеко или позади нас. Эти дистанции рассчитываются радиусом сущности, так что большие меши видимы дальше, чтобы предотвратить "выскакивание".
enemies.hideDistant(player.position, 150, 2);
playerBullets.hideDistant(player.position, 150, 2);
enemyBullets.hideDistant(player.position, 150, 2);
enemies.render(viewmatrix, projectionmatrix);
scenePolycount += enemies.totalpolycount;
playerBullets.render(viewmatrix, projectionmatrix);
scenePolycount += playerBullets.totalpolycount;
enemyBullets.render(viewmatrix, projectionmatrix);
scenePolycount += enemyBullets.totalpolycount;
particleSystem.render(viewmatrix, projectionmatrix);
scenePolycount += particleSystem.totalpolycount;
Наконец, чтобы предоставить отклик игроку для уверенности, что он знает о получении повреждения, мы встряхиваем экран, когда он подбит вражеской пулей.
if (screenShakes > 0)
{
screenShakes--;
if (screenShakes > 0)
{
if (screenShakes % 2) // alternate
{
this.y += screenShakes / 2;
}
else
{
this.y -= screenShakes / 2;
}
screenShakeCameraAngle = this.y / 2;
}
else
{
// we are done shaking: reset
this.x = 0;
this.y = 0;
screenShakeCameraAngle = 0;
}
}
}
Что же произошло?
В первую очередь нам нужно убедиться, что преследующая камера в правильном положении, немного выше и позади игрока. Мы учитываем углы обзора мышью, перемещаемся в правильное положение и устанавливаем угол преследующей камеры.
Как только мы вычислили подходящую трансформацию вида для отправки в Stage3D, мы просто перебираем наши бассейны действующих лиц и рисуем действующие лица, которые видимы. Используя наши новые фантастические бассены действующих лиц и частиц из предыдущей главы, всё, что мы доолжны сделать, это это инструктировать весь бассейн отрендерить себя. После каждого шага рендеринга мы учитываем число полигонов, отрисованных для использования в нашем дисплее состояния.
Мы также делаем некоторые поправки, чтобы быть уверенными, что наша частота кадров держалась на гладких и шелковистых 60fps. Мы отбрасываем действующие лица, которые слишком далеко, чтобы волноваться об отрисовке на сцене перед рендерингом. Для наших целей простое "не рисуй, если это слишком далеко" решение оптимизации прекрасно подходит для наших нужд.
Это техника оптимизации часто называется PVS (настройка потенциальной видимости) и может быть расширена учётом блокаторов выидимости, усечением обзора или даже использованием сложных теххник окклюзии, таких как BSP деревья, Octrees или порталы. Эти термины прекрсно подходят для гугления и оставлены как стимулирующий сторонний квест для разработчиков, интересующихся дальнейшей оптимизацией массивных сложных сцен.
Мы приближаемся к концу обновлений нашего игрового движка! Теперь мы хотим определить спецефичные для геймплея события, так чтобы вещи происходили когда должны.
Определяем специфичные для игры события.
Теперь, когда графика была инициализирована и все арт-активы встроены и обработаны, чтобы создать привлекательный уровень, заполненный объектами, мы должны принять во внимание различные состояния игры и поведение этих игровых сущностей. Следующие несколько секций будут иметь дело со всем этим, связанным с геймплеем, кодом.
Время для действий - вычисляем игровые события.
Функция gamestart будет инструктировать нашу игру, когда игрок готов начать отстреливаться.
private function gamestart():void
{
if (gameState == STATE_TITLESCREEN)
{
//они кликнули ссылку книги?
if (gameinput.mouseClickX < 140)
{
try
{
navigateToURL(new URLRequest(
"https://www.packtpub.com/" +
"adobe-flash11-stage3d-molehill-game" +
"-programming-beginners-guide/book"));
}
catch (error:Error)
{
trace('Link denied by browser security.');
}
return;
}
trace('GAME START!');
После проверки, чтобы убедиться, что игра готова для начала и, в необязательно порядке, открытия окна со страницой книги, мы останавливаем игравшую до этого музыку, запускаем внутриигровой сундтрек и устанавливаем GUI для отображения экрана "задач миссии" на семь секунд.
if (musicChannel) musicChannel.stop();
// запускаем музыку заднего плана
musicChannel = introSound.play();
// очищаем заглавный экран
if (contains(titleScreen))
removeChild(titleScreen);
if (contains(titlescreenTf))
removeChild(titlescreenTf);
// вводный "кинематограф"
addChild(introOverlay);
// он будет удалён через 7с.
introOverlayEndTime = 7000;
Затем мы сбрасываем состояние игры и позицию игрока вместе с игровыми переменными, таких как число последовательных ударов, очки и здоровье.
gameState = STATE_PLAYING;
score = 0;
combo = 0;
comboBest = 0;
player.health = 3;
healthTf.text = healthText_3;
player.x = 0;
player.y = 8; // немного выше "земли"
player.z = 0;
player.active = true;
player.visible = true;
player.collides = true;
nextShootTime = 0;
invulnerabilityTimeLeft = 0;
player.spawnConstantly = '';
player.spawnConstantlyDelay = 0;
player.updateTransformFromValues();
// сбрасываем угбзора мышью
gameinput.cameraAngleX = 0;
gameinput.cameraAngleY = 0;
Если это не самая первая игра, в которую играли, мы хотим очищать все игровые действующие лица, которые могут все ещё существовать с предыдущей игры. Затем мы генерируем уровень, сбрасываем таймеры и очищаем память от временных переменных, так что игра запускается плавно с самого начала.
enemies.destroyAll();
enemyBullets.destroyAll();
playerBullets.destroyAll();
// создаём всех врагов, и т.д.
spawnTheLevel();
// сбрасываем игровой таймер
gametimer.gameElapsedTime = 0;
gametimer.gameStartTime =
gametimer.lastFrameTime =
gametimer.currentFrameTime =
getTimer();
// сейчас хорошее время освободить память от //временных переменных
System.gc();
}
}
Что же произошло?
Функция gamestart запускается в начале геймплея. Она вызывается нашим менеджером ввода, когда мышь нажата.
Как часть демо, которое идёт с книгой, проверка сделана, чтобы видеть если клик произоёл вблизи иконки книги на заглавном экране; если так, открывается новая веб-страница. В вашей игре выжете захотеть включить ссылку на спонсора или, вместо этого, на ваш собственный вебсайт.
Теперь заглавный экран удалён и заменён "вводным" GUI экраном, где игроку даётся миссия. Мы сбрасываем переменные, ассоциированные с тем, где находится игрок в нашем игровом мире, сбрасываем очки на ноль и обновляем игровое состояние так, чтобы с этого момента всё двигалось. Действующие лица из предыдущей игры разрушаются ( и устанавливаются готовыми к перерождению в наших бассейнах действующих лиц) и генерируется уровень. Таймер сбрасывается и всё готово к новой игре. Мы инструктируем Flash, что сейчас хорошее время, чтобы очистить мусор в RAM, который больше не используется, так что всё начинается с чистого листа.
Время для действий - обрабатываем конец игры.
Каждая игра нуждается в начале, середине и конце или, другими словами, заглавном экране, цикле рендеринга и окончании игры. Давайте начнём с начала и закончим путь так, чтобы остаток этой главы мог сфокусироваться на том, что происходит между.
private function gameover(beatTheGame:Boolean = false):void
{
if (gameState != STATE_TITLESCREEN)
{
trace('GAME OVER!');
gameState = STATE_TITLESCREEN;
if (contains(introOverlay))
removeChild(introOverlay);
addChild(titleScreen);
if (beatTheGame)
{
titlescreenTf.text = "VICTORY!\n"
+ "You reached the wormhole\nin " + Math.round(
gametimer.gameElapsedTime / 1000) + " seconds\n"
+ "Your score: " + score + "\n"
+ "Highest combo: " + comboBest + "x\n";
}
else // игрок разрушен
{
titlescreenTf.text = "GAME OVER\n"
+ "You were destroyed\nafter " + Math.round(
gametimer.gameElapsedTime / 1000) + " seconds\n"
+ "Your score: " + score + "\n"
+ "Highest combo: " + comboBest + "x\n";
}
//выключаем двигатели
engineGlow.scaleXYZ = 0.00001;
// делаем текст окончания игры видимым
addChild(titlescreenTf);
}
// освобождаем память от временных переменных
System.gc();
}
Что же произошло?
Это функция вызывается потому, что игрок был разрушен или достиг финишной черты и прошёл игру. Состояние игры снова меняется на режим заглавного экрана. Мы помещаем заглавный экран обратно на сцену, обновляем текстовое сообщение, которое сообщает игроком, насколько хорошо они прошли игру, и делаем нашу игру готовой быть сыграной ещё раз.
Время для действий - обновляем дисплей очков.
Во время геймплея GUI должно обновляться, чтобы отражать изменения в очках и наших комбо-состояниях.
private var combocomment:String = '';
private function updateScore():void
{
if (combo < 10) combocomment = combo + "x combo";
else if (combo < 20) combocomment =
combo + "x combo\nNot bad!";
else if (combo < 30) combocomment =
combo + "x combo\nGreat!";
else if (combo < 40) combocomment =
combo + "x combo\nSkilled!";
else if (combo < 50) combocomment =
combo + "x combo\nIncredible!";
else if (combo < 60) combocomment =
combo + "x combo\nAmazing!";
else if (combo < 70) combocomment =
combo + "x combo\nUnbelievable!";
else if (combo < 80) combocomment =
combo + "x combo\nInsane!";
else if (combo < 90) combocomment =
combo + "x combo\nFantastic!";
else if (combo < 100) combocomment =
combo + "x combo\nHeroic!";
else combocomment = combo + "x combo\nLegendary!";
if (combo > comboBest) comboBest = combo;
// заполняется нулями
if (score < 10) scoreTf.text =
'Score: 00000' + score + "\n" + combocomment;
else if (score < 100) scoreTf.text =
'Score: 0000' + score + "\n" + combocomment;
else if (score < 1000) scoreTf.text =
'Score: 000' + score + "\n" + combocomment;
else if (score < 10000) scoreTf.text =
'Score: 00' + score + "\n" + combocomment;
else if (score < 100000) scoreTf.text =
'Score: 0' + score + "\n" + combocomment;
else scoreTf.text = 'Score: ' + score + "\n" + combocomment;
Что же произошло?
Функция просто выводит хорошо отформатированные очки на экран, который заполняется нулями. Теперь, когда мы отслеживаем "комбо" игрока, считая успешные последовательные удары, всё, что мы должны сделать, это обновить нашу существующую функцию updateScore, чтобы учесть это. Просто для интереса мы можем включить чуть-чуть позитивного одобрения с беззаботными восклицательными прилгательными.
Время для действий - обновляем FPS дисплей.
Наша игра будет отслеживать различные состояния, такие как кадры в секунду (FPS), а также сколкьо памяти использовано и другое.зводительность вашего игрового движка впечатляющая, давайте покажем с помощью некоторых состояний так, что любые скриншоты, которые делают игроки, сообщают хорошие новости.
private function updateFPS():void
{
// обновить FPS дисплей
fpsTicks++;
var now:uint = getTimer();
var delta:uint = now - fpsLast;
// обновляем дисплей только раз в секунду
if (delta >= 1000)
{
// также отслеживаем использование ram
var mem:Number = Number((System.totalMemory *
0.000000954).toFixed(1));
var fps:Number = fpsTicks / delta * 1000;
fpsTf.text = fps.toFixed(1) +
" fps"
+ " (" + scenePolycount + " polies)\n"
+ "Memory used: " + mem + " MB";
//+ enemies.totalrendered + " enemies\n"
//+ (enemyBullets.totalrendered
//+ playerBullets.totalrendered) + " bullets\n"
//+ "[step=" + (profilingEnd - profilingStart) + "ms]"
;
fpsTicks = 0;
fpsLast = now;
}
}
Что же произошло?
GUI обновляется, чтобы отобразить текущую частоту кадров, число отрендеренных полигонов в предыдущем кадре и сколько мегабайт RAM было потреблено. Во время отладки вашей игры вы можете захотеть включить здесь дополнительную информацию, такую как позиция игрока, сколько частиц активны или сколько мс заняла функция gameStep.
Теперь, когда мы закончили с инициализацией, GUI и обработкой начала и конца каждой игры, все функциональность, свзянная с тем что происходиттечением времени в игре, которая остаётся, это собственно процесс игры.
Время для действий - обрабатываем события столкновений.
Есть дополнительные триггеры игровых событий, которые мы хотим реализовать. Это реакция на столкновения (обычно между пулями и космическими кораблями). Когда пуля действительно попадает во врага ( или в нас) мы должны инструктировать игру, что делать.
private function resetCombo():void
{
trace("Player bullet missed: resetting combo.");
combo = 0;
}
Мы хотим вызывать алгоритм независимо от того, где пуля удалена со сцены, потому, что она потеряна и улетела в пространство без какого-либо столкновения с чем либо. Эта функция будет сбрасывать наш счётчик комбо успешных попаданий.
private function hitAnEnemy(
culprit:GameActor, victim:GameActor):void
{
trace(culprit.name
+ " at " + culprit.posString()
+ " hitAnEnemy " + victim.name
+ " at " + victim.posString());
blastSound.play();
victim.health--;
if (victim.health <= 0)
{
particleSystem.spawn("explosion",
victim.transform);
score += victim.points;
// убеждаемся, что die(), звуки, частицы запущены:
victim.step(0);
}
particleSystem.spawn("sparks",
culprit.transform);
culprit.die();
combo++;
}
Если игроку удалось успешно попасть во врага, мы генерируем взываем звук, награждаем игроков очками и увеличиваем их комбометер. И наоборот, когда игрок подбит, мы сокращаем здоровье игроков и проверям умерли они или нет.
private function playerGotHit(
culprit:GameActor, victim:GameActor):void
{
trace("Player got hit!");
screenShakes = 30;
particleSystem.spawn("explosion",
victim.transform);
particleSystem.spawn("sparks",
culprit.transform);
culprit.die();
explodeSound.play();
player.health--;
trace("Player health = " + player.health);
invulnerabilityTimeLeft =
invulnerabilityMsWhenHit;
if (player.health == 3)
{
healthTf.text = healthText_3;
}
if (player.health == 2)
{
healthTf.text = healthText_2;
}
if (player.health == 1)
{
trace("Player is almost dead!");
player.spawnConstantly = 'explosion';
player.spawnConstantlyDelay = 100;
player.spawnConstantlyNext = 0;
healthTf.text = healthText_1;
}
if (player.health <= 0)
{
trace("Player's health is zero!");
healthTf.text = healthText_0;
gameover();
}
}
Что же произошло?
Так как алгоритмы обнаружения столкновений запускает функции обратного вызова везде, где что-либо сталкивается, мы можем отслеживать, сколько попаданий подряд сделано и использовать это, чтобы сообщить о точности игрока, обновляя "комбометр".
Когда происходит столкновение, мы запускаем звук взрыва и эффект частиц, обновляем очки и здоровье "жертвы" и разрушаем её, если она исчерпала здоровье. Если жертва это игрок, мы начинаем трясти экран, чтобы предоставить дополнительный отклик, что случилось что-то плохое. Для того, чтобы избежать мгновенных смертей, когда несколько вражеских пуль поблизости друг от друга одновременно попадают, игроки временно неуязвимы короткий период времени, чтобы позволить им предпринять действия по уклонению.
Время для действий - обрабатываем вводы игрока.
Мы должны улучшить игроков в соответствии с их нажатиями клавиш. В предыдущих главах мы просто меняли x, y, и z положения игрока в зависимости от того, какие клавиши нажаты. Эта версия была обновлена, чтобы учитывать движение в любом направлении.
private function handlePlayerInput(frameMs:uint):void
{
// обрабатываем вводы игрока
var moveAmount:Number = moveSpeed * frameMs;
В нашей игре мы всегда летим вперёд, в то же время двигаясь вверх/вниз/влево/вправо, чтобы уворачиваться. Закоментированный код в следующем примере показывает простой способ скользить по кругу, который работает для нашей конкретной игры, но, возможно, вы всегда обращены к оси z:
// if (gameinput.pressing.up) player.y += moveAmount;
// if (gameinput.pressing.down) player.y -= moveAmount;
// if (gameinput.pressing.left) player.x -= moveAmount;
// if (gameinput.pressing.right) player.x += moveAmount;
Вместо этого должен быть использован улучшенный метод делать то, что выше, который делает то же самое, но также работал бы, если мы повёрнуты в другом направлении, потому, что движение относительно к нашим углам.
if (gameinput.pressing.up)
player.moveUp(moveAmount);
if (gameinput.pressing.down)
player.moveDown(moveAmount);
if (gameinput.pressing.left)
player.moveLeft(moveAmount);
if (gameinput.pressing.right)
player.moveRight(moveAmount);
Здесь - три дополнительных примера обработки движения. Первый это простейший способ реализовать FPS-стиль с поворотом на 360 градусов. Это оставлено здесь как пример того, чего не делать, потому, что хотя это выглядит правильным, есть определённые углы, которые "переворачивают" оси. Если вы погуглите термин "шарнирный замок", вы сможете прочитать больше о этом эффекте.
Второй пример работает альтернативно этому, который был бы прекрасен для системы шести градусов свободы, которая прекрасна для игр с полётами, где вы полететь в любом направлении.
Последний законмментированный пример показывает хороший способ обрабатывать FPS -шутеры, где клавиатура контролирует ваше движение вперёд и назад и позволяет пользователю "пикировать" (или скользить из стороны в сторону) в то время как настоящие углы обзора контролируются мышью.
//ПРИМЕР: поворачиваем корабль через углы Эйлера
//сломается из-за шарнирного замка на 180.. -180
//не поворачивайте предметы, используя эту технику:
// if (gameinput.pressing.left)
// player.rotationDegreesY += moveAmount*4;
// if (gameinput.pressing.right)
// player.rotationDegreesY -= moveAmount*4;
// if (gameinput.pressing.up)
// player.rotationDegreesX -= moveAmount*4;
// if (gameinput.pressing.down)
// player.rotationDegreesX += moveAmount*4;
//ПРИМЕР: FPS стиль 6DOF поворота
//избегает шарнирного замка, используя повороты кватернионов
//которые являются способом поворота, используя единственную ось
// if (gameinput.pressing.left)
// player.transform.prependRotation(moveAmount * 4,
// Vector3D.Y_AXIS);
// if (gameinput.pressing.right)
// player.transform.prependRotation(-moveAmount * 4,
// Vector3D.Y_AXIS);
// if (gameinput.pressing.up)
// player.transform.prependRotation(-moveAmount * 4,
// Vector3D.X_AXIS);
// if (gameinput.pressing.down)
// player.transform.prependRotation(moveAmount * 4,
// Vector3D.X_AXIS);
//ПРИМЕР: скользим вперёд и назад
//и "пикируем" скользя из стороны в сторону
//хорошо для ходьбы, такой как в FPS игре
// if (gameinput.pressing.up)
// player.moveForward(moveAmount);
// if (gameinput.pressing.down)
// player.moveBackward(moveAmount);
// if (gameinput.pressing.strafeLeft)
// player.moveLeft(moveAmount);
// if (gameinput.pressing.strafeRight)
// player.moveRight(moveAmount);
Предыдущие приеры кода включены просто, чтобы помочь вам достичь любого вида системы движения, которую вы желаете. Если повезёт, они помогут вам сэкономить некоторое время. Чтобы продолжить с shootem-up стиля движения, который нужен нашей игре, мы теперь "потянем" космический корабль пользователя вперёд, основываясь на том, сколько времени прошло. Мы проверяем была ли нажата кнопка огня и обрабатываем это событие подходящим образом.
//продолжаем движение вперёд: в нашей игре с полётом
//мы постоянно движемся и используем клавиши стрелок, чтобы уворачиваться.
player.moveForward(flySpeed);
if (gameinput.pressing.fire)
{
if (gametimer.gameElapsedTime >= nextShootTime)
{
//trace("Fire!");
nextShootTime =
gametimer.gameElapsedTime + shootDelay;
// выстрелить пулю
actor = playerBullets.spawn("bolt", player.transform);
actor.updateValuesFromTransform();
actor.rotationDegreesY = 90;
// мы не хотим быть способны стрелять в себя
actor.owner = player;
gunSound.play();
}
}
Наконец, мы хотим заставить игрока оставаться внутри границ. Игрок может уворачиваться от пуль, но мы не хотим, чтобы они летали далеко от действия или двигались после финишной черты.
if ((playerMaxx != 0) && (player.x > playerMaxx))
player.x = playerMaxx;
if ((playerMaxy != 0) && (player.y > playerMaxy))
player.y = playerMaxy;
if ((playerMaxz != 0) && (player.z > playerMaxz))
player.z = playerMaxz;
if ((playerMinx != 0) && (player.x < playerMinx))
player.x = playerMinx;
if ((playerMiny != 0) && (player.y < playerMiny))
player.y = playerMiny;
if ((playerMinz != 0) && (player.z < playerMinz))
{
player.z = playerMinz;
if (levelCompletePlayerMinz)
{
// we got to the "finish line"
trace('LEVEL COMPLETE!');
gameover(true)
}
}
}
Что же произошло?
В первую очередь мы определяем сколько времени прошло и используем это, чтобы подсчитать как далеко игрок должен двигаться в этом конкретном кадре.
Двигаясь в соответствии с углом в котором указывает корабль игрока, вы можете изменить игру, позволив свободное 6DOF (шесть градусов свободы) движение в любом направлении как в лётном симуляторе.
Для нашей shoot-em-up game игры мы хотим, чтобы корабль постоянно двигался вперёд и клавиши стрелок для уклонений влево, вправо, вверх и вниз вместо того, чтобы позволить игроку прийти к полной остановке.
Вы могли бы, наоборот, сделать игру более похожей на шутер от первого лица и заставить игрока стрелять и штурмовать как в Call of Duty, Quake, или Doom. Примеры включены в комментарии, чтобы помочь вам достичь этих целей.
Затем мы в необязательном порядке стреляем пулями, запускаем звук ружья и запускаем жерло системы частиц, если игрок зажал клавишу огня (и если прошло достаточно времени с момента выпуска предыдущей пули).
Наконец, мы заставляем игрока оставаться внутри границ нашего игрового уровня и проверяем пересёк ли игрок "финишную черту". В игре с более чем одним уровнем это было бы местом, где вы загружаете следующую сцену. В этом примере игры игрок достигает червоточины и попадает в безопасное место, запуская конец игры.
Время для действий - обновляем функцию gameStep.
Это функция - рабочая лошадка нашей игры. Во время геймплея она запускается раз в секунду.
private var profilingStart:uint = 0;
private var profilingEnd:uint = 0;
private function gameStep(frameMs:uint):void
{
// время, которое это функция занимает
profilingStart = getTimer();
Так же как функция step может быть там, где влияние частоты кадров наиболее высоко, таймер требуется, чтобы позволить профилирование. Измеряя число миллисекунд, которое функция step занимает каждый кадр, вы можете определить, если новые характеристики замедляют всё слишком сильно.
//если мы на заглавном экране, не перемещаем игрока
if (gameState == STATE_PLAYING)
{
handlePlayerInput(frameMs);
// intro "cinematic" pans the camera for a few sec
if (gametimer.gameElapsedTime < introOverlayEndTime)
{
screenShakeCameraAngle = 45 * (1 -
(gametimer.gameElapsedTime / introOverlayEndTime));
}
}
Преследующая камера должна следовать за игроком, не важно в каком направлении она повёрнута
chaseCamera.x = player.x;
chaseCamera.y = player.y;
chaseCamera.z = player.z;
chaseCamera.rotationDegreesY = player.rotationDegreesY;
chaseCamera.rotationDegreesX = chaseCameraXangle;
chaseCamera.moveBackward(chaseCameraDistance);
chaseCamera.moveUp(chaseCameraHeight);
// во время заглавного экрана и дрожание камеры
if (gameState != STATE_PLAYING)
{
var wobbleSize:Number = 8;
var wobbleMs:Number = 1000;
chaseCamera.moveUp(
Math.cos(gametimer.gameElapsedTime / wobbleMs)
/ Math.PI * wobbleSize);
chaseCamera.moveLeft(
Math.sin(gametimer.gameElapsedTime / wobbleMs)
/ Math.PI * wobbleSize);
chaseCamera.moveForward(
Math.sin(-1 * gametimer.gameElapsedTime / wobbleMs)
/ (Math.PI * wobbleSize * 5) - (wobbleSize / 2));
// force some swivelling as well
gameinput.cameraAngleX =
Math.sin(gametimer.gameElapsedTime / wobbleMs)
/ Math.PI * 60;
gameinput.cameraAngleY =
Math.sin(gametimer.gameElapsedTime / wobbleMs / 3)
/ Math.PI * 90;
}
После добавления дрожания камеры, во время нахождения на заглавном экране, мы анимируем частицы и расстояние астероидных полей
// анимируем астероиды на заднем плане
asteroids1.rotationDegreesX += asteroidRotationSpeed * frameMs;
asteroids2.rotationDegreesX -= asteroidRotationSpeed * frameMs;
asteroids3.rotationDegreesX += asteroidRotationSpeed * frameMs;
asteroids4.rotationDegreesX -= asteroidRotationSpeed * frameMs;
// advance all particles based on time
particleSystem.step(frameMs);
// не обрабатываем больше, если мы на заглавном экране
if (gameState != STATE_PLAYING) return;
Предыдущий код запускается независимо от того, находимся мы на заглавном экране или активно играемая игра предоставляет некоторую минимальную анимацию, даже когда игра эффективно приостановлена. Следующий код запускается только тогда, когда состояние игры не установлено на заглавный экран состояния "паузы". Этим способом враги и игрок не движутся когда мы на самом деле не играем.
//анимируем свечение двигателя - вращаем быстро и пульсируем медленно
engineGlow.rotationDegreesZ += 10 * frameMs;
engineGlow.scaleXYZ =
Math.cos(gametimer.gameElapsedTime / 66) / 20 + 0.75;
playerBullets.step(frameMs, enemies.colliding, hitAnEnemy);
//когда игроки повреждены, они становятся
// неуязвимыми на короткий период времени
if (invulnerabilityTimeLeft > 0)
{
invulnerabilityTimeLeft -= frameMs;
if (invulnerabilityTimeLeft <= 0)
{
trace("Invulnerability wore off.");
}
}
// проверяем столкновения с игроком
// между вражескими кораблями и кораблём игрока
// пока мы не неуязвимы
if (invulnerabilityTimeLeft <= 0)
{
// проходим по вражеским пулями и проверяем на столкновения
enemyBullets.step(frameMs, player.colliding, playerGotHit);
// проходим по врагам и проверяем не столкнулись ли мы с кем-то из них
enemies.step(frameMs, player.colliding, playerGotHit);
}
else
{
// игрок неуязвим:
// проходим по пулям но не проверяем на столкновения
enemyBullets.step(frameMs);
// проходим по врагам, но не проверяем столкновения
enemies.step(frameMs);
}
// позволяем игроку обновлять такие вещи как частицы
player.step(frameMs);
// время, которое занимает эта функция
profilingEnd = getTimer();
}
Что же произошло?
Во многом как наша функция render, функция gameStep перебирает все известные активные игровые действующие лица и достигает симуляции в соответствии с тем, сколько времени прошло с предыдущего кадра. Мы также проверяем столкновения, когда это подходит и запускаем предыдущие функции когда пули и враги попадают во что-то.
Проверяя слишком много столкновений между слишком многими активными действующими лицами, можно сильно замедлить вещи. Если вам нужно проверять миллион потенциальных столкновений, должна быть создана более продвинутая эвристика для оптимизации обнаружения столкновений.
Например, вы можете разделить мир на отдельные "зоны" и проверять только пули и сущности, которые в той же зоне. Сейчас, обход всего списка действующих лиц и проверка столкнулись ли они друг с другом работает прекрасно, так как мы проверяем только пули игрока с врагами и врагов с игроком.
Так как пулям игрока не позволено попадать в их союзников, нам не нужно проверять все возможные столкновения. По этой причине пули игрока сохраняются в отдельном от пулей игрока бассейне действующих лиц.
Наконец игрок также шагает (анимируется и обновляется) и профилирующий таймер останавливается. Это значение профилирующего таймера будет сообщаться в лог отладки и может использоваться, чтобы измерять сколько времени заняла функция gameStep.
Время для действий - обновляем функцию сердцебиения.
Как и в предыдущих главах, функция сердцебиения вызывается каждые несколько секунд.
//для эффективности это функция запускается только время от времени
//идеал для вычислений, когда не нужно вызывать её каждый кадр
таких как нахождения пути, сложный ИИ, потоковая загрузка и т.д.
private function heartbeat():void
{
trace('heartbeat at ' + gametimer.gameElapsedTime + 'ms');
trace('player pos ' + player.posString());
trace('player rot ' + player.rotString());
trace('camera ' + chaseCamera.posString());
trace('particles active: ' + particleSystem.particlesActive);
trace('particles total: ' + particleSystem.particlesCreated);
trace('particles polies: ' + particleSystem.totalpolycount);
if (gameState == STATE_PLAYING)
trace('step: ' + (profilingEnd - profilingStart) + 'ms');
// время удалить вводный "кинематограф"?
if (gametimer.gameElapsedTime > introOverlayEndTime)
{
if (contains(introOverlay))
removeChild(introOverlay);
screenShakeCameraAngle = 0;
}
}
Что же произошло?
Функция сердцебиения всё ещё будет использоваться в целях отладки, сообщая интересную статистику, которая полезна для отладки. Во время отладки вашей игры вы можете использовать эти отслеживающие сообщения, чтобы увидеть сколько частиц в настоящий момент активно, где игрок и сколько точно миллисекунд занимает функция gameStep каждый кадр.
Не забывайте, что это прекрасное место, чтобы поместить продвинутые алгоритмы ИИ, PVS отбор, нахождение пути и даже потоковую загрузку уровня, проверку на завершённые загрузки, смену музыки или другие алгоритмы, которые должны запускаться каждый раз спустя некоторое время вместо того, чтобы запускаться каждый кадр.
Время для действий - обновляем функцию enterFrame
Финальная функция в нашем игровом движке это функция enterFrame, которая обновляет игровой таймер, достижения игровой симуляции и подталкивает цикл рендеринга. Все предыдущие функции вызываются этой функцией, когда нужно.
private function enterFrame(e:Event):void
{
// принудительно очищаем сцену перед рендерингом
context3D.clear(0,0,0);
// считаем кадры, измеряем прошедшее время
gametimer.tick();
// обновляем все позиции сущностей и т.д.
gameStep(gametimer.frameMs);
// рендерим всё
renderScene();
// показываем/переворачиваем задний буфер
// теперь, когда все меши были нарисованы
context3D.present();
// обновляем GUI
if (gameState == STATE_PLAYING)
{
updateScore();
}
updateFPS();
}
} // конец класса
} // конец пакета
Что же произошло?
После того, как вся инициализация игры произошла, событие enterFrame запускается каждый кадр. Это используется, чтобы вызывать всю другую функциональность геймплея, которую мы запрограммировали в нашей игре, от движения и управления вводом до ИИ и симуляции частиц. Мы также обновляем GUI с частотой кадров и текущими очками по необходимости.
Это всё! Мы закончили! Наша игра готовая быть сыгранной друзьями и семьёй.
Публикуем... распространяем... выгода!
Это, отважный искатель приключений, то, зачем мы прошли этот опасный квест. Чтобы получить выгоду от чистой игровой мощи Stage3D. Мы успешно взобрались на Molehill и построили полнофункциональную 3D игру во Flash.
После бета-тестирования вы можете собрать отзывы ваших игроков и обновить игру ещё немного, добавив полировку и исправления для полного совершенства игры. Когда вы удовлетворены, публикуйте игру!
Это то, как выглядит финальная версия нашей игры. Заметьте сколько пуль, взрывов, вражеских кораблей, астероидов и прочего было отрендерено. Обратите внимание на потрясающую частоту кадров. Заметьте, что мы рендерим более 250000 полигонов.
Так как это 3D Flash игра заполнена действием, спецэффектами и массой веселья, ваша игра может быть сыграна фактически на любом компьютере в мире. Она может быть загружена на веб-сайты, сыграна на определённых мобильных устройствах и в будущем может даже быть сыграна на новых телевизорах или игровых консолях.
Пределы достижимого это только ваше воображение. Есть буквально миллионы компьютеров, которые могут воспроизводить вашу игру прямо сейчас - и миллионы людей, которые могут наслаждаться играя в неё.
Загрузите вашу Flash игру в блог, игровые Flash порталы, такие как Kongregate, Newgrounds и тысяча других более маленьких игровых порталов, интегрируйте это с Facebook, создайте автономный проектор, исполняемый или продающий игру в Steam... выбор за вами.
Вперёд, герой - весёлый сторонний квест.
Ваш сторонний квест на этот раз - учесть то, что вы выучили, успешно достали из исходного кода, включённого в эту книгу, и создать вашу собственную полностью отполированную, в высшей степени играбельную, супер-крутую игру.
Возможно вы имеете тонные идей для игр, которые вы хотите сделать. Некоторые так просты, что вам может совсем не понадобиться половина кода в этом проекте. Прекрасным примером была бы игра в пазлы. Используйте рендеринг, вводы, таймер и код GUI, чтобы сделать великолепную 3D игру в пазлы.
Некоторые идеи чрезмерно сложные, такие как создание чего-нибудь с ценностю производства и масштабом, которые можно удачно сравнить с Call of Duty, Fallout 3 или другими многомиллионными консольными играми. Помните, что пока у вас нет большой команды, больших денег и пары лет свободного времени, лучше нацелиться на что-нибудь поменьше.
Другие правы; сидение в тех удобных местах зарезервировано для по-настоящему успешных разработчиков инди-игр. Попытайтесь выбрать что-то, что позволит вам достичь финишной черты. Помнте, многие крупнобюджетные игры имеют 100 людей работающих над ними по два года. Работая для себя, это значит вы бы должны были вложить 200 лет труда в такую игру. Очевидно, простота лучше.
Независимо от того, что вы выберете, попытайтесь осуществить идею. Не сдавайтесь, когда вы "натыкаетесь на стену". Вы МОЖЕТЕ сделать это!
Итого.
Вы этой главе нам удалось обновить наш движок, чтобы предоставить полностью играбельную игру. Все мелкие дополнения здесь. Мы обновили цикл рендеринга, улучшили GUI, добавили игровые события, такие как обнаружение столкновений, и проработали игру с подробным уровнем, заполненным новыми видами декораций и врагов.
Уровень 10 достигнут. Вселенная спасена!
Поздравляем. Вы победили финального босса. Вы успешно достигли конца вашего эпического приключения. Это главное достижение. Добиться чего-то несмотря на трудные препятствия это то, чем вы должны гордиться.
Вы запрограммировали простую, но наполненную действиями 3D Flash игру, с завершённой анимацией, звуками, заглавным экраном, движением игрока, обнаружением столкновений, таймерами, системами частиц, пулями, взрывами, здоровьем, очками и прочим.
Вы сделали что-то с начала, середины и до конца. От заглавного экрана до конца игры, ваше создание может стоять отдельно как законченный продукт.
Вы можете теперь официально считать себя разработчиком 3D видеоигр.
Куда двигаться дальше?
Эта книга была предназначена для самого базового введения в программирование игр Adobe Stage3D (Molehill). Есть так много способов, которыми ваша игра может быть улучшена.
От добавления настоящего физического движка, зацикливания звуков, фрактально-сгенерированной геометрии, большего числа форматов 3D файлов, множества уровней, кинофильмов и продвинутых AGAL шейдеров, "небо это предел" в пересчёте на то, что может быть добавлено.
Если вы хотите создать сложные физические симуляции так, чтобы вы могли создать гоночную игру или баллистическую симуляцию в стиле Angry Birds, вам может быть интересно будет погуглить физические движки "Box2D", "bulletflash", или "jiglib".
Теперь, когда вы знаете основы Stage3D, вы можете захотеть заменить наши простейшие шейдеры и персер .OBJ файлов обширным и богатым чертами движком, таким как Away3D, Minko, Alternativa, Flare3D и т.п. Эти движки будут управлять областью рендеринга для вас, но большинство исходного кода вашей игры (со всеми таймерами, алгоритмами ввода, ИИ, заглавным экраном, очками и комбо дисплеями и другой "специфичной для игры" функциональностью) останется. Вы можете легко добавить сложный рендеринг, но по прежнему использовать всё остальное. У вас есть работающий "игровой движок", который находится на более высоком уровне, чем большинство "3D движков", которые вы найдёте онлайн и которые обычно управляют только камерой и шейдерами.
Для замечательного списка игровых инструментов, написанных на AS3 проверьте в Google "AS3 Game Gears".
Примечание от автора.
Спасибо большое за покупку этой книги. Я действительно насладился этим приключением вместе с вами. Я надеюсь, что усилия, которые пошли на написание её, стоили того. Если вы изучили что-то, получили удовольствие или просто удовлетворили ваше любопытство, моя миссия выполнена.
Как лояльному читателю, вы имеет доступ к совершенно секретным изменениям игрового движка, созданного здесь. Не забудьте проверить www.mcfunkypants.com, который является моим персональным блогом. Там также могут быть новые игры, чтобы поиграть, больше исходного кода для скачивания, дополнения, список опечаток и многое другое. Вы тепло поддержали меня, следуя за мной на Twitter (http://twitter.com/McFunkypants) и Google Plus
(http://www.mcfunkypants.com/+).
Наконец, если вы делаете свою собственную игру, используя то, что вы выучили в этой книге, я бы хотел сыграть в неё. Пожалуйста, свяжитесь со мной в моём блоге и дайте знать о ней. Возможно я сыграю в вашу игру и предоставлю положительный отклик и слова поддержки. Я также смогу послать дополнительных игроков вашим путём, распространяя слово через социальные медиа.
Я был бы горд и признателен знать, что я помог вам на вашем пути написания 3D Flash игры. Я был бы рад слышать вам, так что добро пожаловать.
Поздравляю, храбрый искатель приключений. Вы сделали это. Я приветствую вас.
С уважением,
Christer Kaitila
(aka Breakdance McFunkypants)
Переведено на Нотабеноиде
http://notabenoid.org/book/40281/226848
Переводчики: Orion45
Настройки
Готово:
