Внимание! Для
работы с этой лекцией необходимы учебные
файлы, которые Вы можете загрузить
здесь.
Перед началом работы с этой
лекцией вы можете загрузить
sphereCagecommented.fla
с сайта
www.friendsofed.com/books/studio/flash_mx/code.html.
В
двух последних лекциях мы учились использовать
API-рисование для создания двух- и трехмерного
содержимого в режиме run-time в фильмах Flash.
Теперь настало время применить эти знания и
новые возможности на практике. Мы создадим
полноценную трехмерную игру исключительно с
помощью рисования API, ActionScript и вашего
воображения. Я сказал "исключительно", но это
является правдой только на 99%. Одно из
прекрасных преимуществ Macromedia Flash MX
заключается в возможности быстро и
беспрепятственно комбинировать готовые рисунки и
код в дружественной пользователю среде.
Сейчас я хочу создать игру - с графикой,
интерфейсом и всем, что положено - используя
только рисование API, чтобы продемонстрировать
ее возможности. На практике преимущества Flash в
интеграции кода и графики используются для
улучшения игр, сайтов и приложений, созданных во
Flash или в другой программе. В структуре Flash
нелегко разобраться с рисованием API, поэтому
рассмотрим конкретные примеры. Вы приобретете
необходимые навыки и подходы для реализации
ваших будущих проектов.
Перед тем как начать работу над игрой,
ознакомьтесь с результатом - он находится в
файле sphere_cage.swf
(на компакт-диске). Подумайте, что происходит в
коде игры во время ее работы, и как реализовать
те или иные ее моменты (но не слишком долго!).
Концепция
SphereCage впервые
была представлена общественности около года
назад, когда я практиковался с вариацией
классической игры Breakout. В моей версии игроку
позволялось поворачивать ракетку на 45 градусов
и, как следствие, изменять угол отражения шарика
при каждом попадании. Я тогда подумал, что
неплохо было бы создать вариант игры Breakout со
всеми конечными блоками в центре большого круга
и с игроком, перемещающим ракетку по кругу. Я
так и не доделал эту игру до конца, но сейчас,
при написании этой лекции, мне пришлось
вспомнить то, чем я тогда занимался, так как это
очень подходит к обсуждаемой нами теме.
Я
хотел использовать рисование API для трехмерной
игры, но, как я говорил в предыдущей лекции,
скорость Flash при работе с трехмерной графикой,
иногда слишком мала. Разумеется, вы смогли
создать трехмерный куб, однако создание
полноценной трехмерной игры, которая не "подвесит"
ваш компьютер, является очень трудоемкой задачей.
Я
очень люблю простые захватывающие игры, как
следствие увлечения детскими аркадными (теперь
уже классическими) играми 80-х годов, и мне не
доставляет никакого удовольствия запоминать 50
различных комбинаций клавиш для стрельбы из того
или иного оружия. Одна из самых замечательных
игр, по-моему - Tetris, а прапрадед всех
увлекательных игр - Pong.
Может, взять классическую игру и модернизировать
ее, используя наши новые навыки? Вместо
оригинального двухмерного настольного тенниса,
мы могли бы создать игру с третьим измерением и
обеспечить абсолютно новые ощущения от игры. Две
ракетки - пользователя и компьютера - будут
прорисовываться с помощью нашего 3D-кода в
режиме run-time.
Внешняя сфера и сам шарик будут имитацией
трехмерной графики, что делается с помощью
симуляции глубины, однако шарик должен будет
двигаться в трехмерном пространстве. Мы будем
иметь дело лишь с 9 изменяющимися вершинами (четыре
для каждой из ракеток и одна для шарика, хотя, в
конце концов, я решил добавить еще две вершины в
траекторию шарика, поэтому общее количество
вершин стало равно 11). Все это обеспечит
быструю и красивую трехмерную графику.
Я
начал с разработки концепции отражения шарика,
находящегося между центром и периметром сферы, с
ракеткой компьютера, случайным образом
вращающейся в центре (что убирает все
препятствия с пути построения AI). Если шарик
достигает периметра, игрок проигрывает одну "жизнь".
Игроки могут не только управлять ракеткой, но
также вращать сферу (а следовательно, и все
трехмерное пространство), чтобы лучше наблюдать
траекторию шарика. Самым трудным моментом в коде
будет определение угла отражения при ударении
шарика о движущуюся ракетку компьютера. Вся
остальная программа будет использовать код,
разработанный нами в предыдущей лекции. Мы четко
обрисовали идею игры, теперь давайте перейдем к
делу!
Тот самый 1%
Перед тем, как открывать Flash, позаботимся об
элементах, которые нельзя создать с помощью кода.
Первым из них будет неэстетичный и
необязательный элемент, но, мне кажется, это
будет неплохим дополнением к игре.
Карта
отражения
Если вы посмотрите на законченную игру, то
увидите "стеклянную сферу" с примененной к ней
картой отражения, что добавляет большую
реалистичность графике.
Я
подскажу вам, как я реализовал этот объект,
чтобы вы смогли создать его сами. Я использовал
для этого Adobe Photoshop 7.0, однако вы можете
использовать другую, возможно более знакомую
программу для работы с графикой, если в ней есть
фильтр, аналогичный фильтру Photoshop Spherize.
(Если хотите, можете использовать законченную
карту, находящуюся на компакт-диске в файле
reflect.png.)
- Откройте
любой рисунок, который вы хотите
использовать в качестве карты отражения.
Лучше всего для этого подходят рисунки с
высоким контрастом света (как нельзя лучше
подойдут отражающие окна). Конечно, вы
всегда можете увеличить контраст с помощью
Levels или Curves.
- Нам в
конечном счете понадобится прямоугольный
рисунок, поэтому сделайте ваше изображение
выпуклым с помощью средства Crop, удерживая
клавишу (Shift) для соблюдения пропорций.
Конечным рисунком будет настолько
расплывчатое изображение, что разрешение не
будет здесь играть никакой роли, однако я
работал с разрешением приблизительно 500х500
пикселей.
- С помощью
прямоугольного маркера, установленного на
квадрат (это делается в строке опций или
удержанием клавиши (Shift)), выберите около
80% площади рисунка от центра, оставив края
изображения невыделенными.
-
Установите Filters > Distort > Spherize на
100% и снимите выделение.
-
(Убедитесь, что ваше изображение не является
фоновым слоем - это необходимо для
выполнения данного шага.) С помощью
эллиптического маркера, настроенного на
окружность, выделите, начиная от центра,
около 50% вашей измененной центральной части
рисунка. При активном выделении щелкните на
кнопке Add Layer Mask. Над центром
изображения появится маска, прозрачная по
краям.
- С
выделенной маской (а не рисунком), выберите
команду Filters > Blur > Gaussian Blur и
примените размывку так, чтобы четкие края
стали невидимым. Используйте предварительный
просмотр для контроля результата перед его
принятием. В идеале, он должен быть
непрозрачным в центре, затем постепенно
становиться все прозрачнее вплоть до полной
прозрачности у границы сферической части
рисунка (см. рис. вверху следующей
страницы).
- Используя
команду Image > Trim с выбранной опцией
Based on Transparent Pixels, удалите лишнюю
часть рисунка. Теперь выберите Image > Image
Size и убедитесь, что этот параметр
установлен на значение 72 ppi. Я также
изменил размер рисунка до 300х300 пикселей.
- Наконец,
выберите команду File > Save For Web и
экспортируйте изображение в виде png-24 with
transparency. Закройте Photoshop и перейдите
во Flash, где мы и будем работать в
оставшейся части лекции.
SphereCage:
звуки и отражение
Теперь, перейдя во Flash, импортируем все
необходимые нам данные (имеются в виду звуки и
карта отражения), для управления ими с помощью
ActionScript.
- Откройте
новый фильм, установите частоту кадров на
значение 24 кадра, а также цвет фона на
#666666.
- Выберите
File > Import to Library и импортируйте
карту отражения (reflect.png) и четыре
звуковых файла blip0.wav, blip1.wav,
blip2.wav и miss.wav (все они находятся на
компакт-диске). Можете использовать ваши
собственные звуковые эффекты вместо
вышеперечисленных.
-
Перетащите карту отражения на рабочее место
и преобразуйте ее в фильм с именем reflect.
Убедитесь, что точка регистрации установлена
на центр изображения, и что выбрана опция
Export for ActionScript с идентификатором
reflect. Теперь удалите инстанс с рабочего
стола.
- Откройте
Library и в диалоговом окне Linkage
Properties для каждого из четырех звуков
отметьте опцию Export for ActionScript и
присвойте им идентификаторы blip0, blip1,
blip2 и miss, соответственно.
- Далее нам
нужно будет сохранить некоторые шрифты в
Library, чтобы их можно было использовать в
динамических текстовых полях. Я выбрал два
шрифта: CosmicTwo для заголовков и Courier
New (жирный) для статистики. Вы можете
выбрать любые шрифты, однако постарайтесь
выбрать для статистики моноширинный шрифт,
тогда числа по ходу игры не будут "залезать"
друг на друга, и не нужно будет настраивать
их расположение на рабочем месте. Выберите
New Font: в меню опций справа вверху в
Library. Назовите шрифт для статистики
именем statsFont и выберите шрифт в списке
Font:
- Повторите
этот шаг, присвоив выбранному вами шрифту
для заголовков имя
statsTitleFont.
Я обычно
использую информативные имена для
экспортированных шрифтов. Так, Courier New,
находясь в Library, сообщает мне кое-что о
том, как мой код использует данный шрифт.
Кроме этого, я могу изменить текущий
выбранный шрифт в любое время, не беспокоясь
об изменении ссылок, имен символов или кода.
- Теперь,
если вы еще не использовали экспортированные
шрифты во Flash MX, вам необходимо выполнить
дополнительный шаг и экспортировать шрифт из
вашей библиотеки Library так же, как если бы
вы экспортировали значки вашего фильма.
Щелкните правой кнопкой мыши на имени шрифта
в Library, выберите Linkage: и отметьте
опцию Export for ActionScript. Также
добавьте идентификатор, соответствующий
имени символа (statsFont
или statsTitleFont).
Теперь мы сможем заполнять динамические
текстовые поля с использованием нашего
встроенного шрифта.
Мы почти
готовы к началу работы с кодом. Последнее,
что нужно сделать - это создать шарик. Я
изначально планировал использовать для этого
также ActionScript, но выяснил, что анимация
шарика при применении градиента дает
нежелательные эффекты типа смещения пикселей
в растровом изображении.
- Создайте
новый фильм с именем
innerBall. Нарисуйте круг диаметром
35 пикселей без контура и закрасьте его
радиальным бело-черным градиентом.
Расположите центр градиента в левой верхней
части сферы для создания иллюзии света,
падающего со стороны вашего левого плеча.
Наконец, выровняйте рисунок точно по центру
фильма.
- Теперь
создайте новый фильм с именем
ball. В окне
Linkage Properties отметьте опцию Export for
ActionScript и присвойте ему идентификатор
ball.
Перетащите копию
innerBall из Library и выровняйте ее
в точности по центру рабочего места.
Присвойте ей идентификатор
innerBall.
Здесь
использован вложенный фильм для облегчения
управления innerBall
в трехмерном пространстве. Мы установим
внешний фильм ball на центр нашего
трехмерного пространства и в течение всей
игры будем двигать фильм
innerBall,
точка регистрации которого (внутри
ball) будет
всегда соответствовать центру пространства.
Это предотвратит необходимость использования
переменной сдвига для каждого фильма, что
очень неудобно.
Ну что ж!
Рабочее место пусто, а в Library десять
пунктов - настало время для написания
кода...
Код программы
Хочу сразу вас предупредить - вас ждет работа
над достаточно большим количеством кода, поэтому
будет лучше сейчас потратить некоторое время и
разобраться в том, что мы будем писать, чтобы в
дальнейшем не запутаться. Я разделил код на
четыре логических части. Рассмотрим, что будет
реализовываться в каждой из них.
Интерфейс SphereCage
-
Объявление переменных для игры.
- Создание
фильмов, в которых будут находиться рисунки.
- Создание
звуковых объектов для управления звуковыми
эффектами.
- Создание
и форматирование текстовых полей для
хранения данных о количестве очков.
- Рисование
фоновых рисунков
Классы
- Создание
отдельного класса
Class LightSource для информации об
источнике света.
- Создание
отдельного класса
Class Model для информации о нашей
трехмерной модели.
Наследственность
- Создание
BallModel,
подкласса Model,
который будет содержать информацию о шарике.
- Создание
PlaneModel,
подкласса Model,
который будет содержать информацию о
ракетках.
- Создание
PopUp,
подкласса Model,
который будет содержать информацию о
всплывающих окнах.
Функции игры и
реализация объектов
- Создание
общих функций игры
- Рисование
изображений первого плана.
-
Инициализация игры.
- Учет
очков.
-
Управление звуками.
-
Сортировка глубин.
-
Обновление моделей по нажатию клавиш.
-
Форматирование всплывающих окон.
- Создание
инстансов объектов классов (ракетки, шарик и
цели).
По
мере работы с кодом возвращайтесь время от
времени к этому списку. Вы также можете
распечатать полный код FLA, имеющийся на
компакт-диске. Я настоятельно рекомендую сделать
это, так как вам будет удобнее разбираться в
некоторых частях кода.
Интерфейс
SphereCage
Сначала нужно настроить элементы нашей игры. Мы
создадим все необходимые фильмы для дальнейшего
использования, а также нарисуем фоновые
изображения на рабочем месте.
- На
главной точке временной шкалы переименуйте
слой по умолчанию, присвоив ему имя Actions.
Весь код этой игры будет располагаться в
первом кадре этого слоя. Откройте панель
Actions и введите следующий ActionScript.
stageHeight =
Stage.height;
stageWidth = stageHeight*1.375;
centerX = stageWidth*2/3;
centerY = stageHeight/2;
focalLength = stageHeight;
radius = stageHeight*.4;
radAdj = radius*.8;
planeSize = stageHeight/20;
smoothness = 80;
this.createEmptyMovieClip("backShadow",
1);
this.createEmptyMovieClip("targetMC",
2);
this.attachMovie("ball", "ballMC", 10);
this.createEmptyMovieClip("planeMC",
20);
this.createEmptyMovieClip("paddleMC",
30);
this.createEmptyMovieClip("sphere",
100);
this.createEmptyMovieClip("painting",
150);
this.createEmptyMovieClip("paintMask",
151);
this.createEmptyMovieClip("blip0MC",
250);
this.createEmptyMovieClip("blip1MC",
251);
this.createEmptyMovieClip("blip2MC",
252);
this.createEmptyMovieClip("missMC",
253);
this.attachMovie("reflect", "reflect",
200);
reflect._height =
reflect._width=radius*2;
reflect._alpha = 4;
sphere.createEmptyMovieClip("rim", 0);
sphere.createEmptyMovieClip("shadow",
1);
sphere.createEmptyMovieClip("highlight",
2);
targetMC.createEmptyMovieClip("topTarget",
0);
targetMC.createEmptyMovieClip("bottomTarget",
1);
reflect._x =
targetMC._x=ballMC._x=planeMC._x=paddleMC._x=sphere._x=
КbackShadow._x=centerX;
reflect._y =
targetMC._y=ballMC._y=planeMC._y=paddleMC._y=sphere._y=
КbackShadow._y=centerY;
blip0 = new Sound (blip0MC);
blip1 = new Sound(blip1MC);
blip2 = new Sound(blip2MC);
miss = new Sound(missMC);
blip0.attachSound("blip0");
blip1.attachSound("blip1");
blip2.attachSound("blip2");
miss.attachSound("miss");
Пример
11.1.
В этом коде
нет ничего особо удивительного. Сначала
устанавливаем число переменных, включая
размеры рабочего места. Заметьте, что все
основано на высоте рабочего места, даже
переменная stageWidth.
Это означает, что изменение размера рабочего
места будет также изменять все параметры
игры при управлении пропорциями, включая
размер шрифта.
centerX и
centerY
являются координатами центра сферы, а также
центра трехмерного пространства.
radius
является радиусом нашего круга, а
radAdj - это
просто небольшой сдвиг, находящийся внутри
radius. Этот
сдвиг является местом расположения ракетки
игрока для защиты периметра и придает сфере
толщину, что вы можете наблюдать в конечной
версии игры. Наконец,
planeSize будет размером наших двух
ракеток, а smoothness
будет указывать число линий, используемых
для рисования наших сфер - чем больше число,
тем более гладкая поверхность. Я думаю, что
значение, равное 80, нас устроит.
После этого
мы создаем все необходимые фильмы, а также
добавляем шарик и карту отражения. Мы не раз
возвратимся к этому при работе с нашими
объектами. Обратите внимание, что мы
применили к фильму
reflect размытие со значением 4%.
Если результат кажется вам слишком нечетким,
увеличьте это значение.
targetMC
(маленькие значки "x", представляющие
траекторию шарика) и sphereMC имеют
вложенные фильмы, которые здесь и создаются.
Далее мы
устанавливаем большинство рисунков на
позицию centerX
и centerY.
Сейчас, при доработке, я, наверное, и дальше
использовал бы наследование (с размещением
этих фильмов в фильме-родителе и
перемещением его на позицию
centerX и
centerY),
однако наш способ также годится.
Наконец,
создаем новые объекты
Sound для каждого звукового фильма и
добавляем соответствующие звуки из Library.
Если вам интересно, почему мы создали
фильмы, на которые ссылаются звуковые
объекты, вместо использования
_root,
объясняю: связывание звуков с различными
точками временной шкалы позволяет управлять
каждым звуком независимо от других при
необходимости настройки громкости или
проигрывания. На самом деле это не
обязательно для нашей игры, однако это
хороший способ настройки звуков, который вы
сможете использовать для добавления этой
функциональности впоследствии.
- Введите
этот фрагмент кода, рисующий фоновое
изображение.
this.lineStyle(10,
0x222222, 100);
this.beginFill(0, 100);
this.lineTo(stageWidth/3, 0);
this.lineTo(stageWidth/3, stageHeight);
this.lineTo(0, stageHeight);
this.lineTo(0, 0);
this.endFill();
this.lineTo(stageWidth, 0);
this.lineTo(stageWidth, stageHeight);
this.lineTo(0, stageHeight);
this.lineStyle(2, 0xEEEEEE, 60);
this.moveTo(0, 0);
this.lineTo(stageWidth/3, 0);
this.lineTo(stageWidth/3, stageHeight);
this.lineTo(0, stageHeight);
this.lineTo(0, 0);
this.lineTo(stageWidth, 0);
this.lineTo(stageWidth, stageHeight);
this.lineTo(0, stageHeight);
paintMask.beginFill(0, 100);
paintMask.moveTo(3, 3);
paintMask.lineTo(stageWidth/3-3, 3);
paintMask.lineTo(stageWidth/3-3,
stageHeight-3);
paintMask.lineTo(3, stageHeight-3);
paintMask.endFill();
painting.setMask(paintMask);
Пример
11.2.
Первые два
блока кода рисуют границы (как внешние, так
и внутренние) и черную заливку. Обратите
внимание, что сначала я использовал более
толстую границу, а затем границу с меньшей
шириной и меньшей степенью затемнения. Это
обеспечивает довольно интересный эффект
границ с помощью минимального количества
кода.
paintMask
- это фильм, который будет маскировать наше
градиентное закрашивание внутри темной
области с левой стороны. Последняя строка
устанавливает маску (хотя мы еще должны
нарисовать что-либо в фильме painting).
- Теперь
пришло время подготовить форматирование
шрифтов и добавить поля статистики. Здесь
хочу сказать, что одной из моих любимых
возможностей Flash MX является элемент
управления шрифтами, поэтому я не могу не
использовать его в программе.
textHeight =
stageHeight/20;
statsTitle = new TextFormat();
statsTitle.color = 0x449944;
statsTitle.font = "statsTitleFont";
statsTitle.size = stageHeight*.04;
statsTitle.underline = 1;
stats = new TextFormat();
stats.color = 0xFFFFFF;
stats.font = "statsFont";
stats.size = stageHeight*.05;
stats.bold = 1;
this.createTextField("livesTitleTF",
501, stageWidth/40, textHeight, 0, 0);
this.createTextField("highTitleTF", 502,
stageWidth/40, textHeight*4, 0, 0);
this.createTextField("scoreTitleTF",
503, stageWidth/40, textHeight*7, 0, 0);
this.createTextField("highTF", 504,
stageWidth/40, textHeight*5, 0, 0);
this.createTextField("scoreTF", 505,
stageWidth/40, textHeight*8, 0, 0);
livesTitleTF.setNewTextFormat(statsTitle);
highTitleTF.setNewTextFormat(statsTitle);
scoreTitleTF.setNewTextFormat(statsTitle);
highTF.setNewTextFormat(stats);
scoreTF.setNewTextFormat(stats);
highTitleTF.embedFonts =
scoreTitleTF.embedFonts =
livesTitleTF.embedFonts=1;
highTF.embedFonts =
scoreTF.embedFonts=1;
highTitleTF.autoSize =
scoreTitleTF.autoSize =
livesTitleTF.autoSize="left";
highTF.autoSize =
scoreTF.autoSize="left";
livesTitleTF.text = "LIVES";
highTitleTF.text = "HIGH SCORE";
scoreTitleTF.text = "SCORE";
highTF.text = "00000000";
Пример
11.3.
Здесь мы
создаем два объекта
TextFormat:
stats и
statsTitle. Устанавливаем их размер,
цвет и стиль, а также шрифт (идентификаторы
связей шрифтов в Library). Затем создаем
пять текстовых полей, используя переменные
stageHeight и
stageWidth для
настройки расположения.
Теперь
рассмотрим код, приведенный выше, еще раз и
обратим внимание на то, что мы устанавливаем
ширину и высоту для каждого текстового поля
на ноль. Это возможно благодаря тому, что
при форматировании текстовых полей мы
устанавливаем их параметр autoSize на
значение "left", а значит, текстовое поле
будет расширяться в правую сторону по мере
добавления в него текста. Это полезная
возможность. При необходимости можно также
использовать "center" и "right".
В дополнение
к autoSize, мы
применяем соответствующий
TextFormat и
встраиваем шрифты (в противном случае, текст
не будет отображаться в фильме). Наконец,
добавляем нужные строки в параметр текста
для каждого текстового поля.
- Запустите
фильм, чтобы отобразить фон и текст, который
мы создали до сих пор. С этого момента мы не
сможем видеть какие-либо изменения на
рабочем столе до полного завершения кода.
Классы
Теперь мы перейдем к рассмотрению трехмерных
объектов. В этом параграфе мы создадим новые
классы, которые будут определять эти объекты в
игре SphereCage. Это будет первым шагом в
изучении ООП-программирования.
- Первый
добавляемый объект уже знаком вам, так как
мы использовали его в предыдущей лекции.
Введите
следующий код.
LightSource =
function(x, y, z, brightness) {
this.x = x;
this.y = y;
this.z = z;
this.brightness = brightness;
this.calcMag();
};
LightSource.prototype.calcMag =
function() {
this.magnitude = Math.sqrt
(this.x*this.x+this.y*this.y+this.z*this.z)
};
Это вам
знакомо из предыдущей лекции. Мы создаем
отдельный класс Class
для содержания параметров и методов для всех
источников света, добавляемых на рабочее
место. Добавляемый метод
calcMag
вычисляет расстояние от источника света до
центра пространства.
- Следующий
фрагмент кода вам знаком, однако у него
будет одно значительное отличие. Попробуйте
найти его.
Model = function()
{
};
Model.prototype.applyTransform =
function() {
if (this.transformMatrix) {
for (var 1=0; i<this.vertexList.length; i++) {
var vert = this.vertexList[i];
var x = this.transformMatrix.a*vert.x+
Кthis.transformMatrix.b*vert.y+this.transformMatrix.c*
vert.z+this.transformMatrix.d*vert. w;
var y = this.transformMatrix.e*vert.x+
Кthis.transformMatrix.f*vert.y+this.transformMatrix.g*
vert.z+this.transformMatrix.h*vert. w;
var z = this.transformMatrix.i*vert.x+
Кthis.transformMatrix.j
*vert.y+this.transformMatrix.k*
vert.z+this.transformMatrix.l*vert.w;
vert.x = x;
vert.y = y;
vert.z = z;
}
delete this.transformMatrix;
}
};
Model.prototype.getSideColor =
function(side) {
var verts = [this.vertexList[side.vertices[0]],
this.vertexList[side.vertices[1]],
Кthis.vertexList [side.vertices [2]]];
var lightFactor = this.factorLightAngle(verts);
var r = side.sideColor.substr(0, 2);
var g = side.sideColor.substr(2, 2);
var b = side.sideColor.substr(4, 2);
r = parselnt(r, 16)*lightFactor;
g = parselnt(g, 16)*lightFactor;
b = parselnt(b, 16)*lightFactor;
return r << 16 | g << 8_b;
};
Model.prototype.factorLightAngle =
function(vertices) {
var U = [(vertices[0].x-vertices[1].x), (vertices[0].y-
vertices [1].y), (vertices
[0].z-vertices [1].z)];
var V = [(vertices[1].x-vertices[2].x), (vertices[1].y-
Кvertices[2].y), (vertices [1].z-vertices[2].z)];
var p = [((U[1]*V[2]) - (U[2]*V[1])), - ((U[0]*V[2]) - (U[2]*V[0])),
К((U[0]*V[1])-(U[1]*V[0]))] ;
var magP - Math.sqrt ((p[0]*p[0]) + (p[1]*p[1]) + (p[2]*p[2]));
var dP = ((p[0]*light.x) + (p[l]*light .y) + (p[2]*light.z));
if (dP>0) {
dP*= -1;
}
return ((Math.acos(dP/(magP*light.magnitude))/Math.PI)*
Кlight.brightness/100);
};
Пример
11.4.
Этот код -
начало нашей собственной модели
Model Class. В
предыдущей лекции мы использовали почти в
точности тот же самый код для
factorLightAngle
и getSideColor,
а функция
applyTransform была непосредственной
частью большей функции
render,
поэтому здесь вам все должно быть понятно.
Теперь мы,
вместо создания общих функций вызова в нашем
фильме, создаем отдельный класс
Class, в
котором будут находиться все наши основные
3D-методы. Когда нам потребуется новая
модель, мы просто используем созданный выше
конструктор.
plane = new Model
();
plane будет
использовать все методы
Model, так как
этот объект сам является моделью, инстансом
объекта класса Model.
Это облегчит работу, когда на рабочем месте
будет несколько моделей, если сравнивать с
простым трехмерным кубом из предыдущей
лекции.
Для создания
методов объекта Model
мы используем его параметр
prototype,
который является свойством каждого объекта,
содержащего все параметры и методы данного
объекта. Сделав
factorLightAngle частью параметра
prototype
объекта Model,
мы сделали его доступным для каждой
создаваемой нами модели.
Существует
другой способ добавления параметров и
методов всем инстансам
Class, что
будет вкратце обсуждаться, чтобы вы имели
представление об отличиях и преимуществах
использования параметра
prototype.
Попробуйте поэкспериментировать с этим в
новом фильме.
Выделите кадр
1 слоя по умолчанию и введите следующий код
в панели Actions:
MyClass =
function() {
this.myMethod = function() {};
};
instance1 = new MyClass();
instance2 = new MyClass();
trace(instance1.myMethod);
Отладьте
фильм (Control > Debug Movie или комбинацией
клавиш (Ctrl)+(Shift)+(Enter)) и нажмите
зеленую кнопку проигрывания в отладчике
Debugger. В области слева вверху выберите
level0 и
щелкните на вкладке Variables под этой
областью. Вы увидите
instance1 и
instance2 со значками "+" с левой
стороны. Развернув эти инстансы, вы увидите,
что они оба содержат копии
myMethod. Окно
Output подтверждает это возвращая
[type function].
Теперь
вернитесь в редактор сценариев и измените
строки кода следующим образом.
MyClass =
function() {};
MyClass.prototype.myMethod = function();
instance1 = new MyClass();
instance2 = new MyClass();
trace(instance1.myMethod);
Отладив код
второй раз, вы увидите, что, несмотря на
успешное создание двух инстансов
MyClass, а
также на то, что окно Output возвратило
ожидаемое [type
function], instance1 и instance2,
расположенные на вкладке Variables, НЕ ИМЕЮТ
знаков плюса рядом с собой. Что случилось?
В первом
способе создания метода мы непосредственно
применяем функцию
myMethod к каждому отдельному
инстансу MyClass.
Мы присваиваем не указатель, а САМУ ФУНКЦИЮ.
Это означает, что у нас будет несколько
копий одной и той же функции, занимающих
память.
Во втором
способе создания методов они применяются к
параметру prototype. Вследствие этого
создается только одна копия функции, к
которой могут обращаться ВСЕ инстансы. При
вызове метода (например,
instance1.myMethod();),
Flash будет сначала отслеживать сам инстанс
и все локальные методы, которые к нему были
применены (методы, добавленные к одному
инстансу). Если не будет найдена ссылка на
myMethod, то
Flash будет отслеживать параметр
prototype
класса Class.
Поэтому, для любого метода или параметра,
которые необходимо повторять для инстансов,
имеет смысл добавлять их параметру
prototype.
Вы увидите
пример добавления локальных параметров
инстансу в нашем предыдущем классе
LightSource. В
конструкторе мы добавляем параметры
x,
y,
z и яркость
вновь созданному инстансу. Несмотря на то,
что каждый инстанс
LightSource будет содержать эти
параметры, значения будут различными для
каждого из них, поэтому нам нужно сделать их
локальными для инстанса.
По большому
счету, этот подход не является настоящим
объектно-ориентированным программированием.
Это ООП для Flash, являющееся скорее
программно-графической средой Flash,
описанной мной в начале лекции. Я надеюсь,
что вы сможете увидеть, насколько могут быть
полезны подходы в стиле ООП при
программировании. Здесь код содержится
внутри объектов, которым он нужен и которые
используют его, что делает структуру
взаимодействия четче. Объект сам управляет
своими собственными действиями и
параметрами. В нашей игре движение будет
контролировать сам шарик, а не ракетка. Если
ракетке будет необходимо узнать магнитуду
шарика, она отправит запрос шарику
(например:
ball.getMagnitude();), вместо того,
чтобы вычислять значение самостоятельно.
Таким образом, я знаю, что любой код,
напрямую изменяющий или управляющий шариком,
может быть найден в самом объекте шарика. По
этой причине я сделал вывод, что намного
легче выяснить, где в сценариях содержатся
ошибки (ведь их далеко не всегда можно легко
исправить), когда вы знаете, что объекты
управляют сами собой.
В качестве
последнего пояснения к вышеизложенному коду
- вы увидите, что метод
applyTransform
(который применяет матрицу преобразования
модели) непосредственно связан с параметрами
каждой вершины: x,
y,
z и
w. Но
подождите: что такое
w? В этой игре мы используем матрицы
размером 4х4 для преобразования наших
моделей, в отличие от предыдущей лекции, где
использовались матрицы 3x3. С помощью матриц
преобразования размером 4х4 мы можем
включать метод
translate для наших моделей, который
можно добавить в матрицы преобразования, что
было бы невозможно сделать в случае с
матрицами размером 3х3. Для этого нужно
добавить дополнительный параметр каждой из
вершин (нельзя умножить матрицу размером 4х4
на матрицу 3х1 функции applyTransform, так
как это просто заполнитель). Значение w
будет всегда равно 1, однако это очень
полезное дополнение, так как оно позволит
использовать матрицы размером 4х4.
- Завершим
написание кода класса
Model.
Model.prototype.rotateX =
function(degree) {
var rad = degree*Math.PI/180;
var sin = Math.sin(rad);
var cos = Math.cos(rad);
var matrix = {a:1, b:0, c:0, d:0, e:0, f:cos, g:sin, h:0, i:0,
Кj:-sin, k:cos, 1:0, m:0, n:0, O:0, p:1};
this.transform(matrix);
};
Model.prototype.rotateY =
function(degree) {
var rad = degree*Math.PI/180;
var sin = Math.sin(rad);
var cos = Math.cos(rad);
var matrix = {a:cos, b:0, c:-sin, d:0, e:0, f:1, g:0, h:0, i:sin,
Кj:0, k:cos, 1:0, m:0, n:0, O:0, p:1}; this.transform(matrix);
};
Model.prototype.translate = function(x,
y, z) {
var matrix = {a:1, b:0, c:0, d:x, e:0, f:1, g:0, h:y, i:0, j:0,
Кk:1, 1:z, m:0, n: 0, O:0, p:1};
this.transform(matrix);
};
Model.prototype.transform =
function(matrix) {
if (this.transformMatrix) {
var a = matrix.a*this.transformMatrix.a+matrix.b*
Кthis.transformMatrix.e+matrix.c*this.transformMatrix.i+
Кmatrix.d*this.transformMatrix.m;
var b = matrix.a*this.transformMatrix.b+matrix.b*
Кthis.transformMatrix.f+matrix.c*this.transformMatrix.j
+
Кmatrix.d*this.transformMatrix.n;
var c = matrix.a*this.transformMatrix.c+matrix.b*
Кthis.transformMatrix.g+matrix.c*this.transformMatrix.k+
matrix.d*this.transformMatrix.o;
var d = matrix.a*this.transformMatrix.d+matrix.b*
Кthis.transformMatrix.h+matrix.c*this.transformMatrix.1+
Кmatrix.d*this . transformMatrix.p;
var e = matrix.e*this.transformMatrix.a+matrix.f*
Кthis.transformMatrix.e+matrix.g*this.transformMatrix.i+
Кmatrix.h*this.transformMatrix.m;
var f = matrix.e*this.transformMatrix.b+matrix.f*
Кthis.transformMatrix.f+matrix.g*this.transformMatrix.j
+
Кmatrix.h*this.transformMatrix.n;
var g = matrix.e*this.transformMatrix.c+matrix.f*
Кthis.transformMatrix.g+matrix.g*this.transformMatrix.k+
Кmatrix.h*this.transformMatrix.o;
var h = matrix.e*this.transformMatrix.d+matrix.f*
Кthis.transformMatrix.h+matrix.g*this.transformMatrix.1+
Кmatrix.h* this.transformMatrix.p;
var i = matrix.i*this.transformMatrix.a+matrix.j*
Кthis.transformMatrix.e+matrix.k*this.transformMatrix.i+
Кmatrix.l*this.transformMatrix.m;
var j = matrix.i*this.transformMatrix.b+matrix.j*
Кthis.transformMatrix.f+matrix.k*this.transformMatrix.j +
Кmatrix.l*this.transformMatrix.n;
var k = matrix.i*this.transformMatrix.c+matrix.j*
Кthis.transformMatrix.g+matrix.k*this.transformMatrix.k+
Кmatrix.l*this.transformMatrix.o;
var 1 = matrix.i*this.transformMatrix.d+matrix.j*
Кthis.transformMatrix.h+matrix.k*this.transformMatrix.1+
Кmatrix.l*this.transformMatrix.p;
this. transf ormMatrix = {a:a, b:b, c:c, d:d, e:e, f:f, g:g,
Кh:h, i:i, j:j, k:k, l:l, m:0, n:0, o:0, p:1};
} else {
this.transformMatrix = matrix;
}
};
Model.prototype.render = function() {
this.applyTransform();
for (var i = 0; i<this.vertexList.length; i++) {
var scale = focalLength/(focalLength-this.vertexList[i].z);
this.clip[i]._xscale = this.clip[i]._yscale=(scale/4)*
this.vertexList[i].z+50;
this.clip[i]._x = this.vertexList[i].x*scale;
this.clip[i]._y = this.vertexList[i].y*scale;
}
};
Пример
11.5.
rotateX и
rotateY
являются тем же, чем и в предыдущей лекции,
хотя вы можете видеть, что матрицы имеют
дополнительные значения. Функция
translate -
новая, однако станет совершенно понятной для
вас, как только вы разберетесь в
использовании матриц. Это метод, который
будет перемещать наш шарик через сферу.
Здесь мы устанавливаем параметры
d,
h и
l нашей
матрицы со сдвигом координат (x, y, z).
Функция transform
(мы ее также использовали в предыдущей
лекции) стала несколько больше из-за
дополнительных параметров матрицы. При
детальном рассмотрении, вы заметите, что нам
не нужно вычислять последнюю строку матрицы,
так она всегда будет равна (0,0,0,1).
С помощью
метода render
мы сначала применяем текущее преобразование
модели и затем обрабатываем циклом ее
vertexList для
установки экранных координат. Эта функция
нужна для этой игры и ее структуры, однако
она была бы бесполезна при применении в этом
же виде к другим объектам, поскольку только
две из наших моделей непосредственно
используют этот метод - шарик и цель. Шарик
имеет только одну вершину, тогда как у цели
их две (две мини-цели, каждая с одной
вершиной). Поэтому нам не нужно беспокоиться
о сторонах, цвете и т.д., и мы проходим
циклом вершины только из-за того, что у нас
есть две цели.
В остальном,
этот код в точности повторяет программу для
куба в предыдущей лекции (рассматриваемый
параметр clip
содержит ссылку на один из объектов
innerBall,
topTarget или
bottomTarget),
с добавленной строкой для непосредственного
изменения размеров фильмов по мере их
движения около сферы. В случае с кубом мы
этого не делали, но при работе с моделями с
единственными вершинами физический фильм
должен увеличиваться для моделирования
глубины.
Создание
Model
завершено, однако этот код не обеспечивает
все, что необходимо для трехмерного
пространства. На самом деле, это лишь
методы, необходимые для всех моделей на
нашем рабочем месте.
Изучение
наследственности
С
этого момента ракетки, шарик и цели будут иметь
различные функциональности, с которыми мы будем
иметь дело с помощью наследственности. Не
страшно, если вы не работали с наследственностью
ранее. Если вы разберетесь с данной темой, вы
будете знать почти все, что вам понадобится.
- Под всем
предыдущим кодом ActionScript введите
следующий код, и затем я объясню, что в нем
реализуется (сейчас вы уже должны
разбираться в структуре кода!).
BallModel =
function() {
};
BallModel.prototype = new Model();
BallModel.prototype.move = function() {
var x = this.direction.x*this.velocity;
var y = this.direction.y*this.velocity;
var z = this.direction.z*this.velocity;
this.translate(x, y, z);
with (this.vertexList[0]) {
var mag = Math.sqrt(x*x+y*y+z*z);
if (mag+this.rad>radAdj) {
if (this.checkPaddle() && !ok2Hit) {
playBlip(this.velocity);
ok2Hit = 1;
updateScore();
if (this.velocity<6) {
this.velocity += .1;
}
this.setDirection(-x, -y, -z);
} else if (!ok2Hit) {
miss.start();
killBall();
}
} else if (mag-this.rad<10 && ok2Hit) {
delete ok2Hit;
playBlip(this.velocity);
this.checkAngle();
}
}
};
Пример
11.6.
Итак, что же
такое BallModel?
Мы только что создавали
Model и
говорили о том, как шарик будет использовать
его методы. Однако сейчас нам нужно, чтобы
шарик стал самостоятельно обрабатывать свои
действия. Для этого надо создать другой
класс Class специально для шарика, причем
так, чтобы этот класс наследовал параметры и
методы класса Model
Class.
Если вы
раньше не слышали об аналогии
наследственности на примерах с собаками,
сочту за честь ознакомить вас с ней. У меня
есть йоркширский терьер по кличке Кейси.
Кейси имеет свои собственные повадки,
привычки, но у нее также есть определенные
методы, которые наследуются у всех собак.
Она любит бегать за своим собственным
хвостом. Она гоняет кошек и птиц и защищает
свою территорию. Когда ей жарко, она часто
дышит, высунув язык. Переходя к
программированию, можно сказать, что Кейси -
это объект инстанса класса
Dog. Однако
это не все. Кейси также покрыта шерстью. Она
родит щенков (если мы захотим свести ее) и
будет кормить их молоком. Она теплокровное
животное. Все эти особенности она
унаследовала из класса
Mammal. Итак,
она имеет методы и параметры
Dog, которые
прибавляются к ее изначальным методам и
параметрам Mammal.
Как вы можете себе представить, отсюда можно
перейти вверх по цепочке к классу
Animal, затем
к CarbonBasedLifeForm
(форма жизни, основанная на углероде),
и.т.д.
Мы будем
работать над шариком, т.е. инстансом класса
BallModel,
который сам по себе является подклассом
Model. Мы
создали наш первый метод
BallModel - move
- и для движения шарика используем следующее
выражение.
ball.move ();
Однако при
реализации прорисовки инстанса ball мы будем
использовать следующее выражение.
ball.render ();
При
использовании приведенной выше строки Flash
будет отслеживать инстанс
ball для
выяснения того, содержит ли он локальный
метод с именем render. Если это не так,
Flash будет искать метод в классе объекта
ball, которым
является BallModel.
Если поиск не даст результатов, Flash
продолжит его выше по наследственной
цепочке, осуществляя поиск метода render в
Model.
Так почему же
мы провели такое разделение? Это было
сделано потому, что у ракеток и шарика
должны быть полностью различные методы,
связанные с ними, но также и некоторые общие
3D-методы преобразования. Мы помещаем методы
преобразования в Class и создаем два
подкласса для ракеток и шарика (больше всего
это похоже на то, что все кошки и собаки
принадлежат одному классу млекопитающих).
Для достижения этого, мы устанавливаем
параметр prototype в
BallModel на новый инстанс класса
Model. Вот и
все, практически ничего сложного здесь нет.
Разобравшись
в этом, нам следует перейти к методу move,
написанному в последнем параграфе кода.
Здесь мы снчала перемещаем нашу модель с
использованием четырех переменных - трех
измерений пространства (представлены
векторами) и скорость шарика (velocity). Так
как шарик двигается с постоянной скоростью,
эти значения не обрабатываются ни одним
методом. Умножаем текущее направление
(которое будет единичным вектором) на
текущую скорость и вызываем метод
translate
(определен для класса
Model).
После
движения шарика нам необходимо проверить его
положение. Это осуществляется вычислением
его магнитуды, или расстояния от центра
пространства (здесь опять работает теорема
Пифагора). С помощью этого значения
проверяем, достиг ли шарик линии периметра.
Если да, проверяем, блокирует ли ракетка
удар (с помощью метода
checkPaddle,
который мы сейчас определим). Если да, то
проигрывается соответствующий звук (зависит
от скорости шарика), обновляется количество
очков, немного увеличивается скорость шарика
и, наконец, устанавливается переменная с
именем ok2hit,
которая является просто флажком, исключающим
необходимость возврата шарика, если он
немного перейдет за пределы радиуса (в этом
случае магнитуда установит это условие на
значение "истина" еще раз в следующем
кадре).
setDirection - это метод для установки
нового направления нашего шарика, которое в
данном случае будет противоположным текущему
направлению. Здесь особенно удобно то, что
мы перемещаем шарик обратно точно в центр,
так как наш новый вектор направления
(значение, хранимое в
ball.direction) берется прямо из
наших текущих координат. Представьте себе
шарик с координатами (5,-10,0). Чтобы
переместиться в центр пространства, шарику
нужно перейти на -5 вдоль оси x и на 10 по
оси y. Это и есть направление - коротко и
понятно. Оно получается прямо из текущих
координат. setDirection будет брать это
направление и превращать его в единичный
вектор, о котором мы сейчас поговорим.
Теперь, если
ракетка не отбивает шарик, вызывается
функция killBall
и проигрывается звуковой эффект
miss.
В последнем
выражении if
мы проверяем, достиг ли шарик центра. Если
да, мы проигрываем звук и проверяем текущий
угол диаметральной плоскости (ракетки
компьютера) с использованием метода
checkAngle. В
этом методе мы получаем угол столкновения
между шариком и плоскостью и отражаем шарик
в соответствующем направлении.
- Теперь
будем добавлять другие методы
BallModel
(этот объект не является ядром
функциональности нашей игры).
BallModel.prototype.setDirection =
function(x, y, z) {
var mag = Math, sqrt (x*x+y*y+z*z);
var nD = [x/mag, y/mag, z/mag];
var dest = [nD [0]*radAdj-this.vertexList[0].x, nD[1]*radAdj-
Кthis.vertexList [0].y, nD[2]*radAdj-this.vertexList[0].z];
mag = Math.sqrt(dest[0]*dest[0]+dest[1]*dest[1]+dest[2]*dest[2]);
this.direction = {x:dest[0]/mag, y:dest[1]/mag, z:dest[2]/mag};
};
BallModel.prototype.checkPaddle =
function() {
var x;
var y;
var z;
var numVertices = paddle.vertexList.length
for (var i = 0; i< numVertices; i++) {
x += paddle.vertexList[i].x;
y += paddle.vertexList[i].y;
z += paddle.vertexList[i].z;
}
x /= numVertices;
y /= numVertices;
z /= numVertices;
x -= this.vertexList[0].x;
y -= this.vertexList[0].y;
z -= this.vertexList[0].z;
var dist = Math.sqrt(x*x+y*y+z*z);
if (dist<planeSize+this.rad) {
return true;
}
return false;
};
Пример
11.7.
Настало время
рассмотреть математику трехмерного
пространства - давно пора! Метод
setDirection
вызывается каждый раз при изменении шариком
направления, как в центре, так и на
периметре. Имя является неточным, так как
направление на самом деле отправляется этому
методу при вызове (аргументы
x,
y и
z).
setDirection
переводит эти числа в форму, в которой мы
сможем их использовать. Первые две строки
превращают вектор направления в единичный
вектор. Вектор длины магнитуды равен
квадратному корню из суммы квадратов каждого
компонента. Чтобы сделать его единичным
вектором, необходимо поделить каждый
компонент вектора на магнитуду вектора. С
помощью направления единичного вектора мы
можем управлять скоростью шарика, изменяя
его скорость по направлению, и вектор
направления будет просто указывать нам
нужное направление, не оказывая влияния на
скорость. Это nD,
сокращение от "нормализованное направление"
(normalized direction).
После
нахождения направления наша цель вычисляется
умножением вектора направления на измененную
переменную радиуса (radAdj), настраиваемую
согласно текущей позиции шарика со сдвигом
от центра пространства. (Эти несколько новых
строк не повлияют на проверку периметра,
однако необходимы, когда шарик попадает в
центр ракетки.) Найдя конечные координаты,
мы нормализуем этот вектор еще раз и
добавляем значения в параметр шарика
direction.
Следующий
метод checkPaddle
вызывается каждый раз при достижении шариком
периметра сферы, когда нам необходимо
проверять, блокирует ли ракетка шарик. Метод
находит центральную точку ракетки как
среднее всех ее вершин, затем находит
расстояние между этим центром и положением
шарика. Если оно меньше, чем
paddleWidth
(это значение может быть половиной длины
ракетки) плюс радиус шарика, регистрируется
попадание.
Это не совсем
аккуратное обнаружение коллизий, но оно
достаточно хорошо служит нашим целям.
- Добавьте
следующий код.
BallModel.prototype.checkAngle =
function() {
var vertices = [plane.vertexList[0],
plane.vertexList[1], plane.vertexList [2]];
var U = [(vertices[0].x-vertices[1].x), (vertices[0].y-
Кvertices[1].y), (vertices[0].z-vertices[1].z)];
var V - [ (vertices[1].x-vertices[2].x), (vertices[1].y-
Кvertices [2].y), (vertices [1].z-vertices [2].z) ];
var p = [((U[1]*V[2])-(U[2]*V[1])), -
((U[0] *V[2]) - (U[2] *V[0])) ,
К((U[0]*V[1])-(U[1]*V[0]))] ;
var magP = Math, sqrt ((p[0]*p[0]) +
(p[1] *p[1]) + (p[2] *p[2]));
p = [p[0]/magP, p[1]/magP, p[2]/magP];
var b = [-this.direction.x, -this.direction.y, -this.direction.z];
var dP = p[0]*b[0]+p[1]*b[1]+p[2]*b[2];
var dPxN-[p[0]*dP, p[1]*dP, p[2]*dP];
b = [dPxN[0]+dPxN[0]-b[0], dPxN[1]+dPxN[1]-b[1],
dPxN[2]+dPxN [2]-b[2]];
this.setDirection(b[0], b[1], b[2]);
setTarget();
};
Основной
принцин checkAngle
- то, что угол падения (угол, под которым
шарик ударяется о ракетку) равен углу
отражения (это один из законов отражения).
Сначала мы
используем вершины центральной плоскости для
определения нормали диаметральной плоскости,
который мы превращаем в единичный вектор
делением его на его магнитуду. Мы все это
уже делали и получили p. Для b мы обращаем
направление шарика так, что можно
использовать его в формулах.
dP является
скалярным произведением вектора настроенного
направления шарика и нормали плоскости.
Вспомните, что скалярное произведение - это
расстояние между двумя векторами, которое, в
этом случае, будет расстоянием от точки на
пути шарика к нашей нормали плоскости. Мы
можем использовать это расстояние для
определения угла и направления, которые
будут на другой стороне нормали.
Затем мы
умножаем нормаль вектора на результат
скалярного произведения. Это дает нам
проекцию нашего вектора на нормаль. Новое
направление шарика будет в два раза больше
расстояния этой проекции от исходного
вектора нашего шарика, которое мы определяем
в конечном присвоении b. Мы отправляем эти
новые значения функции
setDirection,
чтобы найти нормаль к вектору и затем
нарисовать цели в соответствующих позициях
(мы вскоре добавим реализующую это функцию).
Мы
рассмотрели довольно сложные концепции,
однако весь наш проект основан именно на
них. Оставшаяся часть кода - это интерфейс и
инициализация игры, но сначала нужно
закончить создание класса для шарика.
- Ниже
приведены два последних метода для
BallModel.
BallModel.prototype.rotateDirection =
function(axis, degree) {
var rad = degree*Math.PI/180;
var sin = Math.sin(rad);
var cos = Math.cos(rad);
if (axis == "y") {
var x = cos*this.direction.x-sin*this.direction.z;
this.direction.z = cos*this.direction.z+sin*this.direction.x;
this.direction.x = x;
} else {
var z = cos*this.direction.z-sin*this.direction.y;
this.direction.y = cos*this.direction.y+sin*this.direction.z;
this.direction.z = z;
}
};
BallModel.prototype.reset = function() {
this.clip[0]._visible = 1;
this.velocity = 1.5;
this.vertexList [0] = {x:0, y:0, z:0, w:l};
this.setDirection(Math.random()-1, Math.random()-.5,
Math. random());
setTarget();
};
rotateDirection
вызывается каждый раз, когда пользователь
поворачивает весь трехмерный мир. При этом
нам не только необходимо настраивать
расположение каждого объекта в трехмерном
пространстве, но также и вектор направления
самого шарика. Это не сложно выполнить.
ball.direction
- единичный вектор, содержащий компоненты
x,
y и
z (его
направление определяется по отношению к
центру пространства), и нам нужно повернуть
координаты этого вектора около
соответствующей оси. Код вам знаком, если вы
проделывали тривиальные действия с любым
типом трехмерного содержимого во Flash.
Аналогичная функция использовалась в начале
предыдущей лекции. Мы осуществляем поворот
около осей x и
y (мы не
реализуем поворот вокруг оси
z в этой игре)
с помощью уже готовых формул.
При начале
проигрывания нового звука (каждый раз после
того, как игрок теряет шарик) вызывается
метод reset.
Он отображает фильм
innerball (хранимый в массиве
clip),
устанавливает
velocity обратно на 1.5,
устанавливает случайное направление и
устанавливает координаты шарика на центр
пространства (обратите внимание на параметр
w, который
всегда равен 1). Наконец, он устанавливает
цель в соответствующем месте. В этом легко
разобраться, за исключением функции
setTarget, которую мы сейчас обсудим.
Вот мы и
создали класс
BallModel! Помните, что при создании
шарика с помощью выражения
ball = new
BallModel();
он будет
содержать не только все методы класса
BallModel, но
и все параметры и методы класса Model.
- Создание
BallModel
завершено. Теперь нужно определить
плоскости.
PlaneModel =
function() {
};
PlaneModel.prototype = new Model();
PlaneModel.prototype.render = function()
{
super.applyTransform();
this.clip.clear();
this.clip.lineStyle(1, 0, 100);
var verts2D = [];
this.zDepth = 0;
for (var i = 0; i<this.vertexList.length; i++) {
var whichVert = this.vertexList[i];
verts2D[i] = {};
var scale = focalLength/(focalLength-whichVert.z);
verts2D[i].x = whichVert.x*scale;
verts2D[i].y = whichVert.y*scale;
this.zDepth += whichVert.z;
}
this.clip.moveTo(verts2D[0].x, verts2D[0].y);
this.clip.beginFill(this.getSideColor(this.side[0]), 100);
for (var j = 1; j<verts2D.length; j++) {
this.clip.lineTo(verts2D[j].x, verts2D[j].y);
}
this.clip.lineTo(verts2D[0].x, verts2D[0].y);
this.clip.endFill();
};
Вы
обрадуетесь, узнав, что этот блок кода
полностью представляет собой определение
класса PlaneModel.
Для добавления ракеток (которые являются
плоскостями) нам нужен только метод
прорисовки.
Минуточку! У
нас уже метод render, определенный для
класса Model!
Да, это так, однако давайте вернемся к
обсуждению цепочек наследования. При
использовании
paddle.render, Flash будет сначала
искать метод с именем render в классе
PlaneModel.
Если он будет найден, поиск будет прекращен.
Flash никогда не дойдет до класса
Model для
выяснения метода с таким же именем. Таким
образом, мы предоставили отдельный метод
render для
наших плоскостей, однако разрешили
использование оставшихся методов
Model.
Этот метод
сначала вызывает метод
applyTransform
своего большого класса, в данном случае -
Model. Так же,
как this
ссылается на его собственные методы,
super будет
обращаться к методам своего суперкласса,
используя при этом ссылки на них в параметре
prototype
(посмотрите, в каком месте предыдущего кода
мы установили его).
После
применения преобразования удаляем фильм с
рисованием объектов, обрабатываем циклом
вершины и рисуем плоскость. Этот процесс
очень похож на то, что мы делали в
предыдущей лекции, поэтому я не хочу
вдаваться в более мелкие подробности, однако
вы должны иметь в виду добавление параметра
zDepth. Он
содержит сумму координат
z всех вершин
и будет использоваться лишь для определения
того, расположена ли плоскость спереди или
сзади сферы. Поэтому дальнейшие вычисления
z нам не
нужны.
-
Рассмотрим последний имеющийся у нас тип
модели. Ранее, когда вы пробовали играть в
игру, вы, наверняка, даже не подозревали о
его присутствии в этой программе. Введите
следующий код.
PopUp = function
(w, h) {
this.vertexList = [];
this.vertexList.push({x:-w, y:-h, z:20, w:l});
this.vertexList.push({x:-w, y:h, z:20, w:l});
this.vertexList.push({x:w, y:h, z:20, w:l});
this.vertexList.push({x.-w, y:-h, z:20, w:l});
this.vertexList.push({x:-w-15, y:-h-15, z:0, w:l});
this.vertexList.push({x:-w-15, y:h+15, z:0, w:l});
this.vertexList.push({x:w+15, y:h+15, z:0, w:l});
this.vertexList.push({x:w+15, y:-h-15, z:0, w:l});
this.vertexList.push({x:-w+5, y:-h+5, z:15, w:l});
this.vertexList.push({x:-w+5, y:h-5, z:15, w:l});
this.vertexList.push({x:w-5, y:h-5, z:15, w:l});
this.vertexList.push({x:w-5, y:-h+5, z:15, w:l});
this.side = [];
this.side.push({vertices:[0, 1, 2, 3], sideColor:"666666"});
this.side.push({vertices:[0, 4, 5, 1], sideColor:"888888"});
this.side.push({vertices:[0, 3, 7, 4], sideColor:"888888"});
this.side.push({vertices:[7, 3, 2, 6], sideColor:"333333"});
this.side.push({vertices:[1, 5, 6, 2], sideColor:"333333"});
this.side.push({vertices:[8, 9, 10, 11], sideColor:"666666"});
this.side.push({vertices:[0, 8, 9, 1], sideColor:"444444"});
this.side.push({vertices:[3, 2, 10, 11], sideColor:"777777"});
this.side.push({vertices:[9, 10, 2, 1], sideColor:"777777"}) ;
this.side.push({vertices:[0, 3, 11, 8], sideColor:"444444"});
};
PopUp.prototype = new Model();
Пример
11.8.
Да,
всплывающие окна также являются моделями.
Здесь видно, что конструктору будут
отправлены переменные ширины (w) и высоты
(h) для изменения размера всплывающего окна,
однако я оставил размер таким же. Почти весь
этот блок кода вам знаком из работы с
предыдущей лекцией. Обратите внимание на
присутствие дополнительного параметра w для
каждой вершины так, чтобы можно было
использовать матрицы размером 4x4. Также
видно, что я оптимизировал 3D-подсветку
модели, добавив свои собственные настройки
применительно к sideColor освещенных и
затемненных сторон.
- Для
модели PopUp есть только одна функция.
PopUp.
prototype.render = function() {
this.clip.clear();
var verts2D = [];
for (var i = 0; i<this.vertexList.length; i++) {
var whichVert = this.vertexList[i];
verts2D[i] = {};
var scale = focalLength/(focalLength-whichVert.z);
verts2D[i].x = whichVert.x*scale;
verts2D[i].y = whichVert.y*scale;
}
for (var i = 0; i<this.side.length; i++) {
this.clip.moveTo(verts2D[this.side[i].vertices[0]].x,
Кverts2D[this.side[i].vertices[0]].y);
this.clip.beginFillthis.getSideColor(this.side[i]), 100);
for (var j = 1; j<this.side[i].vertices.length; j++) {
this.clip.lineTo(verts2D[this.side[i].vertices[j]].x,
Кverts2D[this.side[i].vertices[j]].y);
}
this.clip.lineTo(verts2D[this.side[i].vertices[0]].x,
verts2D[this.side[i].vertices[0]].y);
this.clip.endFill();
}
};
Это еще один
метод render.
По идее нужно объединить этот метод с
методом render
PlaneModel и сделать
PopUp
подклассом Plane,
однако это лишь несколько строк уже
знакомого вам дополнительного кода.
Последним
фрагментом кода объекта будет расширение
самого объекта Object.
Object.prototype.duplicate = function()
{
var temp = {};
for (var i in this) {
temp[i] = this[i];
}
return temp;
};
Когда в игре
пользователь поворачивает трехмерный мир,
этот метод используется для создания копии
модели матрицы преобразования. Так как Flash
хранит ссылки на объекты, а не копии, копии
нужно создавать вручную. Я объясню, как это
делается, когда мы дойдем до этой части
кода.
Мы достигли
конца нашего ООП-кода Flash, и теперь
осталось лишь определить несколько
_root-функций, перед запуском нашей игры.
Если вы сейчас запустите фильм, вы не
увидите абсолютно никакой разницы по
сравнению с предыдущим запуском. Так что же
мы делали все это время? Это была
подготовка. Теперь все наши собственные
классы определены и настроены на обработку
их собственных действий, и остается создать
лишь несколько служебных функций.
Функции игры и
инсталляция объектов
В
этом последнем параграфе мы напишем несколько
функций, которые будут обрабатывать основные
действия, напрямую не относящиеся к отдельным
3D-объектам, такие как учет количества очков,
рисование интерфейса и непосредственное
выполнение игры. В идеале для них нужно было бы
использовать классы и ООП (интерфейс для запуска
игры и поддержки рисования, или объект-доска для
учета очков или жизней), но они требуют большего
количества кода, и, к сожалению, в этой книге
это рассматриваться не будет.
После объявления служебных функций мы создадим
инстансы объектов классов, над которыми так
долго работали в предыдущем параграфе, включая
шарик, ракетки и цели.
- Эта
функция предназначена для сброса рабочего
места в начале игры.
initGame =
function () {
paint();
scoreTF.text = "00000000";
lives = 4;
for (var i = 1; i < lives; i++) {
var b = this.attachMovie("ball", "ball" + i, 160 + i);
b._xscale = b._yscale = stageHeight/8;
b._y = stageHeight/7;
b._x = (stageWidth/40*(i*2-1) ) + b._width/2;
}
ball.reset();
paddleMC.onEnterFrame = frameCode;
};
paint
- это функция, рисующая в левой части
интерфейса градиент случайным образом (ее мы
создадим в следующем шаге). Сбрасываем
количество очков на 0 и общее число жизней
на 4. Пользуясь этим значением, добавляем на
рабочее место 3 шарика (плюс текущий шарик,
итого - 4) для графического отображения
жизней. Изменяем размер шариков и
располагаем их с левой стороны (позиция
_x была
определена методом "тыка"). Наконец,
ball
сбрасывает сам себя, и устанавливается
событие onEnterFrame
для объекта paddle для функции с именем
frameCode. frameCode
содержит код, который будет выполняться в
игре повсеместно.
- Ниже
приведена вызываемая функция paint.
paint = function
() {
for (i=0; i<25; i++) {
var p = painting.createEmptyMovieClip("p"+i, i);
p. _x = Math.random()*(stageWidth/3-0);
p._y = Math.random()*(stageHeight-stageHeight/4) +
stageHeight/4;
var w = Math.random0*50+100;
var col = Math.ceil(Math.random()*100);
col = col << 8;
var colors = [col, col];
var alphas = [50, 0];
var ratios = [0, 200];
var matrix = {matrixType:"box", x:-w/2, y:-w/2, w:w, h:w, r:
0};
p.beginGradientFill("radial", colors, alphas, ratios,
matrix);
p.moveTo(-w/2, -w/2);
p.lineTo(w/2, -w/2);
p.lineTo(w/2, w/2);
p.lineTo(-w/2, w/2);
p.endFill();
}
};
Этот код
просто повторяется 25 раз и располагает
фильмы случайно в левой части интерфейса
(маскируется фильмом
paintMask). Каждый фильм содержит
квадрат, закрашенный градиентом,
начинающимся с непрозрачного и
заканчивающимся прозрачным зеленым цветом.
Видно, что для каждого фильма мы установили
случайную позицию, размер квадрата и оттенок
зеленого цвета.
- Далее
создаем функцию для поддержки рисования
стеклянной сферы в главной области просмотра
игры в правой части экрана.
drawSphereAssets =
function (me, params, center,
smoothness) {
var ang = 360/smoothness;
var rad = ang*(Math.PI/180);
mc.moveTo(0, radius);
var matrix = {a:params.hotspot, b:0, c:0, d:0, e:params.hotspot,
Кf:0, g:center.x, h:center.y, i:0};
me.beginGradientFill("radial", params.colors, params.alphas,
params.ratios, matrix);
for (var i = 1; i<=smoothness; i++) {
var dx = Math.sin(i*rad)*radius;
var dy = Math.cos(i*rad)*radius;
me.lineTo (dx, dy);
}
mc.endFill();
};
У вас будет
возможность лучше понять, что означает
каждый из аргументов, при дальнейших вызовах
функций. Вкратце - эта функция рисует круг,
закрашенный градиентом на основе аргументов.
Наслаивая различные градиенты, создаем
стеклянную сферу. Это отличный способ
достижения сложных эффектов с использованием
только рисования API и небольшой доли
экспериментирования.
Основные
тригонометрические функции позволяют
рисовать линии около границы нашего круга,
заполняя его градиентом, определяемым
аргументами. Изначально я сделал это с
использованием матрицы типа 3х3, однако вы
можете реализовать то же самое с помощью
другого типа матриц градиента, доступного
для beginGradientFill
(содержащей параметры
matrixType, x,
y,
w,
h и
r).
- Для
управления нашими целевыми объектами нужны
две функции.
setTarget =
function () {
target.vertexList[0].x = (ball.direction.x*radAdj) + ball.vertexList
[0].x;
target.vertexList[0].y = (ball.direction.y*radAdj) + ball.vertexList
[0].y;
target.vertexList[0].z = (ball.direction.z*radAdj) + ball.vertexList
[0].z;
target.vertexList[1].x = (ball.direction.x*radius) + ball.vertexList
[0].x;
target.vertexList[1].y = (ball.direction.y*radius) + ball.vertexList
[0].y;
target.vertexList[1].z = (ball.direction.z*radius) + ball.vertexList
[0].z;
drawTarget ();
};
drawTarget = function () {
for (var i = 0; i<target.clip.length; i++) {
target.clip[i].clear();
if (target.vertexList[i].z>0) {
targetMC.swapDepths(99);
var col = 0xFFFFFF;
} else {
targetMC.swapDepths(2);
var col = 0x000000;
}
target.clip[i].lineStyle(1, col, 100);
target.clip[i].moveTo (-planeSize/10, -planeSize/10);
target.clip[i].lineTo(planeSize/10, planeSize/10);
target.clip[i].moveTo(planeSize/10, -planeSize/10);
target.clip[i].lineTo(-planeSize/10, planeSize/10);
}
target.render();
};
Пример
11.9.
Изначально я
задавался вопросом создания этих методов для
нового класса целей, но в итоге пришел к
выводу, что проще поддерживать их таким
способом. setTarget
устанавливает 3D-координаты двух вершин,
содержащихся в целевой модели (внешняя и
внутренняя цели). Обе вершины определяются
умножением единичного вектора направления
шарика на скалярное значение. Вершина
внутренней цели установлена приблизительно
на точке, в которую шарик попадет на
периметре сферы (смещение шарика при начале
движения от центра может повлечь за собой
его выход за периметр), а вершина внешней
цели расположена немного дальше этой токи,
чтобы игрок мог лучше отслеживать
траекторию. После этого вызывается метод
drawTarget для
непосредственной прорисовки и расположения
этих целей.
Вам также
нужно знать, как будет установлен целевой
объект (который мы сейчас реализуем при
создании инстансов всех наших объектов). У
нас есть фильм с именем
target,
содержащий два вложенных фильма с именами
topTarget и
bottomTarget.
Мы установили их в начале кода. Каждый фильм
будет содержать крест - фигуру "X", и
представлять одну из двух вершин нашей
модели цели. Таким образом, мы можем просто
нарисовать крест в центре вложенных фильмов
и переместить фильмы по отдельности в
3D-пространстве, что мы и делаем в этой
функции. Цикл обрабатывает оба клипа,
устанавливает глубину либо перед, либо за
двумя плоскостями, в зависимости от
параметра z,
устанавливает черный или белый цвет, в
зависимости от стороны сферы, в которой он
находится, и затем рисует простой крестик,
представляющий собой цель. Наконец, он
вызывает функцию
render цели для расположения ее в
нужной позиции на экране.
- Введите
две следующие функции.
sortDepths =
function () {
if (ball.vertexList[0].z>-1) {
ballMC.swapDepths(25);
} else {
ballMC.swapDepths(15);
}
if (paddle.zDepth> 0) {
paddle.clip.swapDepths(30);
} else {
paddle.clip.swapDepths(10);
}
};
spinWorld = function (axis, rate) {
paddle.render();
plane.render();
ball.render();
if (axis == "y") {
paddle.rotateY(rate);
} else {
paddle.rotateX(rate);
}
plane.transformMatrix = paddle.transformMatrix.duplicate();
target.transfonnMatrix = paddle.transformMatrix.duplicate();
ball.transformMatrix = paddle.transformMatrix.duplicate();
ball.rotateDirection(axis, rate);
drawTarget();
};
Пример
11.10.
sortDepths
располагает наш шарик и ракетку на
соответствующей глубине, в зависимости от
стороны сферы, в которой они находятся.
spinWorld
вызывается, когда игрок использует клавиши
для поворота сферы. В этом случае, все
четыре модели (плоскость, ракетка, шарик и
цель) и направление шарика требуют
обработки.
Нашим первым
шагом является прорисовка каждой модели для
применения всех остальных отдельных
преобразований (их быть не должно, однако
лишняя предосторожность не повлияет на
игру). После этого мы поворачиваем нашу
ракетку по оси вращения, отправленной
функции. Так как все матрицы преобразования
были удалены при предыдущей прорисовке,
преобразование, необходимое для каждой
модели при данном вращении, будет в точности
таким же. По этой причине, нам не нужно
создавать одну и ту же матрицу четыре раза
(один раз для каждой модели). Мы можем
использовать одно и то же преобразование для
всех моделей. С помощью метода
duplicate
объектов, которые мы создали ранее, мы можем
скопировать преобразование для каждой модели
и применить их по отдельности. Мне кажется,
что этот способ безопаснее, нежели просто
копирование ссылки в объект матрицы
преобразования, что имело бы место, если бы
мы не использовали метод
duplicate.
Перед выходом
из функции, мы также изменяем вектор
направления нашего шарика (помните созданиe
метода
rotateDirection? Здесь он и
применяется!) и рисуем нашу цель на ее новом
месте.
- Ниже
приведены три несложные функции, которые
вызываются во время выполнения игры.
killBall =
function () {
lives--;
this ["ball"+lives].removeMovieClip();
if (lives == 0) {
delete paddleMC.onEnterFrame;
ball.clip[0]._visible = 0;
gameOverPopUp();
} else {
ball.reset();
}
};
updateScore = function () {
var score = parselnt(scoreTF.text, 10);
score += Math.floor(ball.velocity*20);
score += "";
while (score.length<8) {
score = "0"+score;
}
scoreTF.text = score;
};
playBlip = function (velocity) {
this ["blip"+(Math.ceil(velocity/2)-1)].start();
};
killBall,
вызываемая каждый раз при потере шарика
игроком, уменьшает переменную
lives и
удаляет одно из соответствующих изображений
шарика. После этого данная функция
проверяет, закончились ли жизни у игрока.
Если это так, вызывается всплывающее окно с
информацией об окончании игры (мы сейчас
напишем этот код). В противном случае шарик
перемещается в центр.
updateScore
использует текст из текстового поля
scoreTF и
превращает его в число.
parseInt
используется для указания Flash на
использование десятичных чисел, в противном
случае он воcпринимает их как двоичные.
После этого данное значение помещается
обратно в параметр
text текстового поля
scoreTF после
проверки наличия в числе восьми цифр.
Наконец,
playBlip
определяет соответствующий звук для
проигрывания в зависимости от параметра
velocity
шарика: blip0,
blip1 или
blip2.
- Следующая
функция, которую мы присвоили событию
onEnterFrame
объекта paddle,
выполняется в каждом кадре для обновления
игры.
frameCode =
function () {
if (Key.isDown(65)) {
spinWorld("y", -paddle.rate);
} else if (Key.isDown(68)) {
spinWorld("y", paddle.rate);
}
if (Key.isDown(83)) {
spinWorld("x", -paddle.rate);
} else if (Key.isDown(87)) {
spinWorld("x", paddle.rate);
}
ball.move();
plane.rotateX(2);
plane.rotateY(1);
if (Key.isDown(Key.RIGHT)) {
paddle.rotateY(-paddle.rate);
} else if (Key.isDown(Key.LEFT)) {
paddle.rotateY(paddle.rate);
}
if (Key.isDown(Key.UP)) {
paddle.rotateX(-paddle.rate);
} else if (Key.isDown(Key.DOWN)) {
paddle.rotateX(paddle.rate);
}
plane.render();
paddle.render();
ball.render();
sortDepths();
};
Пример
11.11.
Этот блок
кода выполняется в каждом кадре для вызова
всех функций и методов, которые вы только
что создали. Вы можете видеть, что код в
большей части блока связан с отслеживанием
того, какие клавиши были нажаты, и с
обработкой соответствующих событий.
Первые два
выражения if/else
работают с клавишами A, S, D и W,
поворачивая пространство по определенной оси
при нажатии клавиши. Выполнение кода затем
продолжается вызовом метода move шарика и
поворотом плоскости по двум осям. Далее в
коде есть еще два выражения
if/else,
предназначенные для проверки того, нажаты ли
в данный момент клавиши стрелок, и, если это
так, ракетка будет двигаться.
Наконец,
вызывается метод render для каждой модели, и
осуществляется сортировка глубин для
соответствующих фильмов. Вот и все! Большая
работа, проделанная с объектами, позволила
нам сократить количество вводимого здесь
кода.
- Следующая
функция (а также следующая за ней) длинная,
но относительно простая. Большая часть кода
обрабатывает расположение и форматирование
текстовых полей во всплывающих окнах.
startGamePopUp =
function () {
this.createEmptyMovieClip("dim", 200000);
dim.beginFill(0x656565, 80);
dim.lineTo(stageWidth, 0);
dim.lineTo(stageWidth, stageHeight);
dim.1ineTo(0, stageHeight);
dim.endFill();
var popUpWidth = stageWidth/4;
var popUpHeight = stageHeight/4;
this.createEmptyMovieClip("sg", 200001);
sg._x = stageWidth/2;
sg._y = stageHeight/2;
startGame = new PopUp(popUpWidth, popUpHeight);
startGame.clip = sg;
startGame.render();
sg.createTextField("title", 0, 0, -popUpHeight, 0, 0);
statsTitle.size = stageHeight/10;
sg.title.setNewTextFormat(statsTitle);
sg.title.embedFonts = 1;
sg.title.autoSize = "center";
sg.title.text = "SphereCage";
sg.createTextField("message" , 1, -popUpWidth*.9, -popUpHeight*.4,
КpopUpWidth*1.8, popUpHeight);
stats.align = "center";
stats.size = stageHeight*.035;
sg.message.setNewTextFormat(stats);
sg.message.embedFonts = 1;
sg.message.wordwrap = 1;
sg.message.text = "use your arrows keys to maneuver the
Кpaddle\n\nuse the a, s, d, w keys\nto spin the sphere";
sg.createTextField("replay" , 3, 0, popUpHeight*.6, 0, 0);
stats.align = "left";
stats.size = stageHeight*.05;
sg.replay.setNewTextFormat(stats);
sg.replay.embedFonts = 1;
sg.replay.textColor = 0x808080;
sg.replay.autoSize = "center";
sg.replay.text = "click to start";
sg.col = 80;
sg.direction = 1;
sg.onEnterFrame = function() {
this.col += 5*this.direction;
if (this.col>254 | this.col<80) {
this.direction *= -1;
}
var r = this.col << 16;
var g = this.col << 8;
var b = this.col;
this.replay.textColor = r | g | b;
};
sg.onPress = function() {
stats.align = "left";
dim.removeMovieClip();
initGame();
this.removeMovieClip();
};
};
Пример
11.12.
Я не буду
разбирать каждую строку этого кода, так как
он довольно прост и говорит сам за себя.
Функция создает исходное всплывающее окно
при загрузке игры. Она создает серый,
частично прозрачный квадрат для
"затуманивая" интерфейса, и затем создается
новый инстанс класса
PopUp, над которым мы работали ранее.
Функция
onEnterFrame,
расположенная ближе к концу блока кода,
выполняет цветовой переход для закрашивания
сообщения о начале игры. При этом, значение
параметра textColor текстового поля replay
изменяется от серого до белого цвета.
onPress
ожидает щелчок мыши пользователя. Как только
он происходит, "размытый" фильм удаляется
вместе со всплывающим окном, и вызывается
управляющий элемент
initGame.
- Это
аналогичный код, но для другого всплывающего
окна.
gameOverPopUp =
function () {
this.createEmptyMovieClip("dim", 200000);
dim.beginFill(0x656565, 80);
dim.lineTo(stageWidth, 0);
dim.lineTo(stageWidth, stageHeight);
dim.lineTo(0, stageHeight);
dim.endFill() ;
var popUpWidth = stageWidth/4;
var popUpHeight = stageHeight/4;
this.createEmptyMovieClip("gm", 200001);
gm._x = stageWidth/2;
gm._y = stageHeight/2;
gameOver = new PopUp(popUpWidth, popUpHeight);
gameOver.clip = gm;
gameOver.render();
gm.createTextField("title", 0, 0, -popUpHeight*.9, 0, 0);
gm.title.setNewTextFormat (statsTitle);
gm.title.embedFonts = 1;
gm.title.autoSize = "center";
gm.title.text = "Game Over";
gm.createTextField("message", 1, 0, -popUpHeight*.2, 0, 0);
gm.message.setNewTextFormat(stats);
gm.message.embedFonts = 1;
gm.message.autoSize = "center";
gm.createTextField("score", 2, 0, popUpHeight*.1, 0, 0);
gm.score.setNewTextFormat(stats);
gm.score.embedFonts = 1;
gm.score.autoSize = "center";
var score = parselnt(scoreTF.text, 10);
gm.score.text = score;
if (score>parselnt(highTF.text, 10)) {
gm.message.text = "You made high score!";
highTF.text = scoreTF.text;
} else {
gm.message.text = "You scored:";
}
gm.createTextField("replay", 3, 0, popUpHeight*.5, 0, 0);
gm.replay.setNewTextFormat(stats);
gm.replay.embedFonts = 1;
gm.replay.textColor = 0x808080;
gm.replay.autoSize = "center";
gm.replay.text = "click to play again";
gm.col = 80;
gm.direction = 1;
gm.onEnterFrame = function() {
this.col += 5*this.direction;
if (this.col>254 | | this.col<80) {
this.direction *= -1;
}
var r = this.col << 16;
var g = this.col << 8;
var b = this.col;
this.replay.textColor = r | g | b;
};
gm.onPress = function() {
dim.removeMovieClip();
initGame();
this.removeMovieClip();
};
};
Пример
11.13.
Здесь
содержится другой текст и форматирование для
всплывающего окна с сообщением о завершении
игры, но в целом этот код ничем не
отличается от предыдущей функции.
- Мы уже на
финишной прямой! Все функции и классы
определены. Теперь нужно просто создать
инстансы наших объектов.
paddle = new
PlaneModel();
paddle.clip = paddleMC;
paddle.rate = 8;
paddle.vertexList = [];
paddle.vertexList.push({x:-planeSize,
y:-planeSize, z:radAdj, w:1});
paddle.vertexList.push({x:-planeSize,
y:planeSize, z:radAdj, w:1});
paddle.vertexList.push({x:planeSize,
y:planeSize, z:radAdj, w:1});
paddle.vertexList.push({x-.planeSize,
y:-planeSize, z:radAdj, w:1});
paddle.side = [];
paddle.side.push({vertices:[0, 1, 2, 3],
sideColor:"550055"});
plane = new PlaneModel();
plane.clip = planeMC;
plane.vertexList = [];
plane.vertexList.push({x:-planeSize,
y:-planeSize, z:0, w:1});
plane.vertexList.push({x:-planeSize,
y:planeSize, z:0, w:1});
plane.vertexList.push({x: planeSize,
y:planeSize, z:0, w:1});
plane.vertexList.push({x:planeSize,
y:-planeSize, z:0, w:1});
plane.side = [];
plane.side.push({vertices:[0, 1, 2, 3],
sideColor:"234523"});
ball = new BallModel();
ball.clip = [ballMC.innerBall];
ball.rad = ballMC._width/2;
ball.vertexList = [];
ball.vertexlist.push({x:0, y:0, z:0,
w:l});
target = new Model();
target.clip = [targetMC.topTarget,
targetMC.bottomTarget];
target.vertexList = [];
target.vertexList.push({x:0, y:0, z:0,
w:l});
target.vertexList.push({x:0, y:0, z:0,
w:l});
light = new LightSource(-20000, -20000,
20000, 100);
Пример
11.14.
Теперь это
все уже вам знакомо, не так ли?
paddle,
plane,
ball и
target
являются инстансами наших классов. Мы
предоставляем им все vertexList с вершинами,
а также стороны для плоскостей. Имейте в
виду, что нам нужно хранить наш шарик и
целевой фильм в параметре массива из-за
подхода, использованного при написании кода.
Мы также создаем новый инстанс источника
света в конце кода.
Осталось
всего лишь десять строк, и работа будет
завершена!
- Следующие
восемь строк рисуют сферические объекты в
правой части.
sphereHL =
{colors:[OxFFFFFF, OxFFFFFF],
alphas:[20, 0], ratios:[0,
К225], hotspot:radAdj};
sphereShadow = {colors:[0x000000,
0x000000], alphas:[0, 30],
Кratios:[100,155], hotspot:radius*3.6};
sphereBackShadow = {colors:[0x090909,
0x000000], alphas:[20, 0],
Кratios:[80,140], hotspot:radius*4};
sphereRim = {colors:[0xFFFFFF,
0xFFFFFF], alphas:[0, 2],
Кratios:[230,230], hotspot:radius*2};
drawSphereAssets(backShadow,
sphereBackShadow, {x:50, y:40},
Кsmoothness);
drawSphereAssets(sphere.shadow,
sphereShadow, {x:-radius/4,
Кy: -radius/4}, smoothness);
drawSphereAssets(sphere.highlight,
sphereHL, {x:-radius/3,
Кy: -radius/3}, smoothness);
drawSphereAssets(sphere.rim, sphereRim,
{x:0, y:0}, smoothness);
Сначала
создаются отдельные объекты для наших
градиентов, затем вызывается созданная нами
функция
drawSphereAssets. Обратитесь к коду
этой функции и посмотрите на соотношение
данных значений.
sphereHL окрашивает верх сферы белым
цветом, а
sphereShadow и
sphereBackShadow создают тень в
правой нижней части сферы.
sphereRim
представляет собой границу сферы, придавая
ей некоторую толщину.
- Вы,
наверное, не поверите, если я скажу, что
ниже приведены две последние строки
программы. Да, последние! Введите их в ваш
код.
padle.rotateY(45);
startGamePopUp();
В первой
строке мы поворачиваем ракетку в начале
таким образом, что она не закрывает собой
шарик и плоскость в начале игры. Наконец,
вызывается функция
startGamePopUp, рисующая стартовое
всплывающее окно и претворяющая в жизнь весь
созданный нами код. Соглашусь, вам пришлось
работать с большим количеством кода, однако
созданная программа представляет собой
красивую и интересную трехмерную игру.
Тестирование игры на моем компьютере
показало, что установленная частота кадров
24 fps редко опускается ниже 22 fps.
- Запустите
фильм, чтобы увидеть результаты ваших
стараний.
Дальнейшая
разработка SphereCage
На
компакт-диск я записал другую версию
SphereCage с
некоторыми дополнительными улучшениями. Наиболее
заметно добавление звуковой дорожки к игре
(предоставлено Imagescore
Music), а также то, что шарик "подается"
в каждом раунде со специальным эффектом
градиента. Начало каждой игры изменяет цвет
интерфейса, а также происходит небольшой "танец"
ракеток, управляемый прекрасным новым параметром
Sound.duration. У
самой ракетки пониженное затемнение, поэтому вы
сможете видеть шарик через нее. Я также добавил
функцию паузы, активизирующуюся при нажатии
пользователем клавиши (пробел). Последним и
наиболее сложным дополнением является такое
расположение целей траектории, что внутренняя
цель располагается именно на том месте, где
шарик ударится о стенку периметра сферы.
Откройте этот вариант игры, чтобы посмотреть на
код, необходимый для данных улучшений.
Конечно, существует еще очень много возможностей
для оптимизации и дальнейшего совершенствования
игры. Это может быть какой-либо эффект при
открытии игры, изменение размеров ракеток для
повышения сложности, или введение уровней
сложности установкой конечного числа, которое
необходимо достигнуть в каждом раунде. Вы можете
также использовать некоторые сценарии для
создания доски с рекордами по количеству очков,
или использовать новую возможность Flash MX,
связанную с общими объектами для хранения
информации о рекордном количестве очков
пользователей. Также можно расширить возможности
игры с помощью добавления ООП-кода. При этом
сначала рекомендую создать класс Target.
|