Перед началом работы с этой лекцией вы можете загрузить 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 впервые была представлена общественности около года назад, когда я практиковался с вариацией классической игры
Я хотел использовать рисование API для трехмерной игры, но, как я говорил в предыдущей лекции, скорость Flash при работе с трехмерной графикой, иногда слишком мала. Разумеется, вы смогли создать трехмерный куб, однако создание полноценной трехмерной игры, которая не "подвесит" ваш компьютер, является очень трудоемкой задачей.
Я очень люблю простые захватывающие игры, как следствие увлечения детскими аркадными (теперь уже классическими) играми 80-х годов, и мне не доставляет никакого удовольствия запоминать 50 различных комбинаций клавиш для стрельбы из того или иного оружия. Одна из самых замечательных игр, по-моему - Tetris, а прапрадед всех увлекательных игр - Pong.
Может, взять классическую игру и модернизировать ее, используя наши новые навыки? Вместо оригинального двухмерного настольного тенниса, мы могли бы создать игру с третьим измерением и обеспечить абсолютно новые ощущения от игры. Две ракетки - пользователя и компьютера - будут прорисовываться с помощью нашего 3D-кода в режиме run-time. Внешняя сфера и сам шарик будут имитацией трехмерной графики, что делается с помощью симуляции глубины, однако шарик должен будет двигаться в трехмерном пространстве. Мы будем иметь дело лишь с 9 изменяющимися вершинами (четыре для каждой из ракеток и одна для шарика, хотя, в конце концов, я решил добавить еще две вершины в траекторию шарика, поэтому общее количество вершин стало равно 11). Все это обеспечит быструю и красивую трехмерную графику.
Я начал с разработки концепции отражения шарика, находящегося между центром и периметром сферы, с ракеткой компьютера, случайным образом вращающейся в центре (что убирает все препятствия с пути построения AI). Если шарик достигает периметра, игрок проигрывает одну "жизнь". Игроки могут не только управлять ракеткой, но также вращать сферу (а следовательно, и все трехмерное пространство), чтобы лучше наблюдать траекторию шарика. Самым трудным моментом в коде будет определение угла отражения при ударении шарика о движущуюся ракетку компьютера. Вся остальная программа будет использовать код, разработанный нами в предыдущей лекции. Мы четко обрисовали идею игры, теперь давайте перейдем к делу!
Перед тем, как открывать Flash, позаботимся об элементах, которые нельзя создать с помощью кода. Первым из них будет неэстетичный и необязательный элемент, но, мне кажется, это будет неплохим дополнением к игре.
Если вы посмотрите на законченную игру, то увидите "стеклянную сферу" с примененной к ней картой отражения, что добавляет большую реалистичность графике.

Я подскажу вам, как я реализовал этот объект, чтобы вы смогли создать его сами. Я использовал для этого Adobe Photoshop 7.0, однако вы можете использовать другую, возможно более знакомую программу для работы с графикой, если в ней есть фильтр, аналогичный фильтру Photoshop Spherize. (Если хотите, можете использовать законченную карту, находящуюся на компакт-диске в файле reflect.png.)


Теперь, перейдя во Flash, импортируем все необходимые нам данные (имеются в виду звуки и карта отражения), для управления ими с помощью ActionScript.


statsTitleFont.Я обычно использую информативные имена для экспортированных шрифтов. Так, Courier New, находясь в Library, сообщает мне кое-что о том, как мой код использует данный шрифт. Кроме этого, я могу изменить текущий выбранный шрифт в любое время, не беспокоясь об изменении ссылок, имен символов или кода.
statsFont или statsTitleFont ). Теперь мы сможем заполнять Мы почти готовы к началу работы с кодом. Последнее, что нужно сделать - это создать шарик. Я изначально планировал использовать для этого также ActionScript, но выяснил, что анимация шарика при применении градиента дает нежелательные эффекты типа смещения пикселей в растровом изображении.
Здесь использован вложенный фильм для облегчения управления innerBall в трехмерном пространстве. Мы установим внешний фильм ball на центр нашего трехмерного пространства и в течение всей игры будем двигать фильм innerBall, точка регистрации которого (внутри ball ) будет всегда соответствовать центру пространства. Это предотвратит необходимость использования переменной сдвига для каждого фильма, что очень неудобно.
Ну что ж! Рабочее место пусто, а в Library десять пунктов - настало время для написания кода...
Хочу сразу вас предупредить - вас ждет работа над достаточно большим количеством кода, поэтому будет лучше сейчас потратить некоторое время и разобраться в том, что мы будем писать, чтобы в дальнейшем не запутаться. Я разделил код на четыре логических части. Рассмотрим, что будет реализовываться в каждой из них.

Class LightSource для информации об источнике света.Class Model для информации о нашей трехмерной модели.BallModel, подкласса Model, который будет содержать информацию о шарике.PlaneModel, подкласса Model, который будет содержать информацию о ракетках.PopUp, подкласса Model, который будет содержать информацию о всплывающих окнах.По мере работы с кодом возвращайтесь время от времени к этому списку. Вы также можете распечатать полный код FLA, имеющийся на компакт-диске. Я настоятельно рекомендую сделать это, так как вам будет удобнее разбираться в некоторых частях кода.
Сначала нужно настроить элементы нашей игры. Мы создадим все необходимые фильмы для дальнейшего использования, а также нарисуем фоновые изображения на рабочем месте.
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");
В этом коде нет ничего особо удивительного. Сначала устанавливаем число переменных, включая размеры рабочего места. Заметьте, что все основано на высоте рабочего места, даже переменная 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);
Первые два блока кода рисуют границы (как внешние, так и внутренние) и черную заливку. Обратите внимание, что сначала я использовал более толстую границу, а затем границу с меньшей шириной и меньшей степенью затемнения. Это обеспечивает довольно интересный эффект границ с помощью минимального количества кода.
paintMask - это фильм, который будет маскировать наше градиентное закрашивание внутри темной области с левой стороны. Последняя строка устанавливает маску (хотя мы еще должны нарисовать что-либо в фильме painting).
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";
Здесь мы создаем два объекта TextFormat: stats и statsTitle. Устанавливаем их размер, цвет и стиль, а также шрифт (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);
};
Этот код - начало нашей собственной модели 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;
}
};
rotateX и rotateY являются тем же, чем и в предыдущей лекции, хотя вы можете видеть, что матрицы имеют дополнительные значения. Функция translate - новая, однако станет совершенно понятной для вас, как только вы разберетесь в использовании матриц. Это метод, который будет перемещать наш шарик через сферу. Здесь мы устанавливаем параметры d, h и l нашей матрицы со сдвигом координат (x, y, z). Функция transform (мы ее также использовали в предыдущей лекции) стала несколько больше из-за дополнительных параметров матрицы. При детальном рассмотрении, вы заметите, что нам не нужно вычислять последнюю строку матрицы, так она всегда будет равна (0,0,0,1).
С помощью метода render мы сначала применяем текущее преобразование модели и затем обрабатываем циклом ее vertexList для установки экранных координат. Эта функция нужна для этой игры и ее структуры, однако она была бы бесполезна при применении в этом же виде к другим объектам, поскольку только две из наших моделей непосредственно используют этот метод - шарик и цель. Шарик имеет только одну вершину, тогда как у цели их две (две мини-цели, каждая с одной вершиной). Поэтому нам не нужно беспокоиться о сторонах, цвете и т.д., и мы проходим циклом вершины только из-за того, что у нас есть две цели.
В остальном, этот код в точности повторяет программу для куба в предыдущей лекции (рассматриваемый параметр clip содержит ссылку на один из объектов innerBall, topTarget или bottomTarget ), с добавленной строкой для непосредственного изменения размеров фильмов по мере их движения около сферы. В случае с кубом мы этого не делали, но при работе с моделями с единственными вершинами физический фильм должен увеличиваться для моделирования глубины.
Создание Model завершено, однако этот код не обеспечивает все, что необходимо для трехмерного пространства. На самом деле, это лишь методы, необходимые для всех моделей на нашем рабочем месте.
С этого момента ракетки, шарик и цели будут иметь различные функциональности, с которыми мы будем иметь дело с помощью наследственности. Не страшно, если вы не работали с наследственностью ранее. Если вы разберетесь с данной темой, вы будете знать почти все, что вам понадобится.
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();
}
}
};
Итак, что же такое 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, написанному в последнем параграфе кода. Здесь мы снчала перемещаем нашу модель с использованием четырех переменных - трех измерений пространства (представлены векторами) и скорость шарика (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;
};
Настало время рассмотреть математику трехмерного пространства - давно пора! Метод 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 ), устанавливает обратно на 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! Да, это так, однако давайте вернемся к обсуждению цепочек наследования. При использовании , 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();
Да, всплывающие окна также являются моделями. Здесь видно, что конструктору будут отправлены переменные ширины (w) и высоты (h) для изменения размера всплывающего окна, однако я оставил размер таким же. Почти весь этот блок кода вам знаком из работы с предыдущей лекцией. Обратите внимание на присутствие дополнительного параметра w для каждой вершины так, чтобы можно было использовать матрицы размером 4x4. Также видно, что я оптимизировал 3D-подсветку модели, добавив свои собственные настройки применительно к sideColor освещенных и затемненных сторон.
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 для объекта frameCode. frameCode содержит код, который будет выполняться в игре повсеместно.
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();
};
Изначально я задавался вопросом создания этих методов для нового класса целей, но в итоге пришел к выводу, что проще поддерживать их таким способом. 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();
};
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 определяет соответствующий звук для проигрывания в зависимости от параметра шарика: 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();
};
Этот блок кода выполняется в каждом кадре для вызова всех функций и методов, которые вы только что создали. Вы можете видеть, что код в большей части блока связан с отслеживанием того, какие клавиши были нажаты, и с обработкой соответствующих событий.
Первые два выражения 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();
};
};
Я не буду разбирать каждую строку этого кода, так как он довольно прост и говорит сам за себя. Функция создает исходное всплывающее окно при загрузке игры. Она создает серый, частично прозрачный квадрат для "затуманивая" интерфейса, и затем создается новый инстанс класса 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();
};
};
Здесь содержится другой текст и форматирование для всплывающего окна с сообщением о завершении игры, но в целом этот код ничем не отличается от предыдущей функции.
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);
Теперь это все уже вам знакомо, не так ли? , 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 с некоторыми дополнительными улучшениями. Наиболее заметно добавление звуковой дорожки к игре (предоставлено Imagescore Music ), а также то, что шарик "подается" в каждом раунде со специальным эффектом градиента. Начало каждой игры изменяет цвет интерфейса, а также происходит небольшой "танец" ракеток, управляемый прекрасным новым параметром Sound.duration. У самой ракетки пониженное затемнение, поэтому вы сможете видеть шарик через нее. Я также добавил функцию паузы, активизирующуюся при нажатии пользователем клавиши (пробел). Последним и наиболее сложным дополнением является такое расположение целей траектории, что внутренняя цель располагается именно на том месте, где шарик ударится о стенку периметра сферы. Откройте этот вариант игры, чтобы посмотреть на код, необходимый для данных улучшений.
Конечно, существует еще очень много возможностей для оптимизации и дальнейшего совершенствования игры. Это может быть какой-либо эффект при открытии игры, изменение размеров ракеток для повышения сложности, или введение уровней сложности установкой конечного числа, которое необходимо достигнуть в каждом раунде. Вы можете также использовать некоторые сценарии для создания доски с рекордами по количеству очков, или использовать новую возможность Flash MX, связанную с общими объектами для хранения информации о рекордном количестве очков пользователей. Также можно расширить возможности игры с помощью добавления ООП-кода. При этом сначала рекомендую создать класс Target.
Перед началом работы с этой лекцией вы можете загрузить 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 впервые была представлена общественности около года назад, когда я практиковался с вариацией классической игры
Я хотел использовать рисование API для трехмерной игры, но, как я говорил в предыдущей лекции, скорость Flash при работе с трехмерной графикой, иногда слишком мала. Разумеется, вы смогли создать трехмерный куб, однако создание полноценной трехмерной игры, которая не "подвесит" ваш компьютер, является очень трудоемкой задачей.
Я очень люблю простые захватывающие игры, как следствие увлечения детскими аркадными (теперь уже классическими) играми 80-х годов, и мне не доставляет никакого удовольствия запоминать 50 различных комбинаций клавиш для стрельбы из того или иного оружия. Одна из самых замечательных игр, по-моему - Tetris, а прапрадед всех увлекательных игр - Pong.
Может, взять классическую игру и модернизировать ее, используя наши новые навыки? Вместо оригинального двухмерного настольного тенниса, мы могли бы создать игру с третьим измерением и обеспечить абсолютно новые ощущения от игры. Две ракетки - пользователя и компьютера - будут прорисовываться с помощью нашего 3D-кода в режиме run-time. Внешняя сфера и сам шарик будут имитацией трехмерной графики, что делается с помощью симуляции глубины, однако шарик должен будет двигаться в трехмерном пространстве. Мы будем иметь дело лишь с 9 изменяющимися вершинами (четыре для каждой из ракеток и одна для шарика, хотя, в конце концов, я решил добавить еще две вершины в траекторию шарика, поэтому общее количество вершин стало равно 11). Все это обеспечит быструю и красивую трехмерную графику.
Я начал с разработки концепции отражения шарика, находящегося между центром и периметром сферы, с ракеткой компьютера, случайным образом вращающейся в центре (что убирает все препятствия с пути построения AI). Если шарик достигает периметра, игрок проигрывает одну "жизнь". Игроки могут не только управлять ракеткой, но также вращать сферу (а следовательно, и все трехмерное пространство), чтобы лучше наблюдать траекторию шарика. Самым трудным моментом в коде будет определение угла отражения при ударении шарика о движущуюся ракетку компьютера. Вся остальная программа будет использовать код, разработанный нами в предыдущей лекции. Мы четко обрисовали идею игры, теперь давайте перейдем к делу!
Перед тем, как открывать Flash, позаботимся об элементах, которые нельзя создать с помощью кода. Первым из них будет неэстетичный и необязательный элемент, но, мне кажется, это будет неплохим дополнением к игре.
Если вы посмотрите на законченную игру, то увидите "стеклянную сферу" с примененной к ней картой отражения, что добавляет большую реалистичность графике.

Я подскажу вам, как я реализовал этот объект, чтобы вы смогли создать его сами. Я использовал для этого Adobe Photoshop 7.0, однако вы можете использовать другую, возможно более знакомую программу для работы с графикой, если в ней есть фильтр, аналогичный фильтру Photoshop Spherize. (Если хотите, можете использовать законченную карту, находящуюся на компакт-диске в файле reflect.png.)


Теперь, перейдя во Flash, импортируем все необходимые нам данные (имеются в виду звуки и карта отражения), для управления ими с помощью ActionScript.


statsTitleFont.Я обычно использую информативные имена для экспортированных шрифтов. Так, Courier New, находясь в Library, сообщает мне кое-что о том, как мой код использует данный шрифт. Кроме этого, я могу изменить текущий выбранный шрифт в любое время, не беспокоясь об изменении ссылок, имен символов или кода.
statsFont или statsTitleFont ). Теперь мы сможем заполнять Мы почти готовы к началу работы с кодом. Последнее, что нужно сделать - это создать шарик. Я изначально планировал использовать для этого также ActionScript, но выяснил, что анимация шарика при применении градиента дает нежелательные эффекты типа смещения пикселей в растровом изображении.
Здесь использован вложенный фильм для облегчения управления innerBall в трехмерном пространстве. Мы установим внешний фильм ball на центр нашего трехмерного пространства и в течение всей игры будем двигать фильм innerBall, точка регистрации которого (внутри ball ) будет всегда соответствовать центру пространства. Это предотвратит необходимость использования переменной сдвига для каждого фильма, что очень неудобно.
Ну что ж! Рабочее место пусто, а в Library десять пунктов - настало время для написания кода...
Хочу сразу вас предупредить - вас ждет работа над достаточно большим количеством кода, поэтому будет лучше сейчас потратить некоторое время и разобраться в том, что мы будем писать, чтобы в дальнейшем не запутаться. Я разделил код на четыре логических части. Рассмотрим, что будет реализовываться в каждой из них.

Class LightSource для информации об источнике света.Class Model для информации о нашей трехмерной модели.BallModel, подкласса Model, который будет содержать информацию о шарике.PlaneModel, подкласса Model, который будет содержать информацию о ракетках.PopUp, подкласса Model, который будет содержать информацию о всплывающих окнах.По мере работы с кодом возвращайтесь время от времени к этому списку. Вы также можете распечатать полный код FLA, имеющийся на компакт-диске. Я настоятельно рекомендую сделать это, так как вам будет удобнее разбираться в некоторых частях кода.
Сначала нужно настроить элементы нашей игры. Мы создадим все необходимые фильмы для дальнейшего использования, а также нарисуем фоновые изображения на рабочем месте.
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");
В этом коде нет ничего особо удивительного. Сначала устанавливаем число переменных, включая размеры рабочего места. Заметьте, что все основано на высоте рабочего места, даже переменная 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);
Первые два блока кода рисуют границы (как внешние, так и внутренние) и черную заливку. Обратите внимание, что сначала я использовал более толстую границу, а затем границу с меньшей шириной и меньшей степенью затемнения. Это обеспечивает довольно интересный эффект границ с помощью минимального количества кода.
paintMask - это фильм, который будет маскировать наше градиентное закрашивание внутри темной области с левой стороны. Последняя строка устанавливает маску (хотя мы еще должны нарисовать что-либо в фильме painting).
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";
Здесь мы создаем два объекта TextFormat: stats и statsTitle. Устанавливаем их размер, цвет и стиль, а также шрифт (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);
};
Этот код - начало нашей собственной модели 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;
}
};
rotateX и rotateY являются тем же, чем и в предыдущей лекции, хотя вы можете видеть, что матрицы имеют дополнительные значения. Функция translate - новая, однако станет совершенно понятной для вас, как только вы разберетесь в использовании матриц. Это метод, который будет перемещать наш шарик через сферу. Здесь мы устанавливаем параметры d, h и l нашей матрицы со сдвигом координат (x, y, z). Функция transform (мы ее также использовали в предыдущей лекции) стала несколько больше из-за дополнительных параметров матрицы. При детальном рассмотрении, вы заметите, что нам не нужно вычислять последнюю строку матрицы, так она всегда будет равна (0,0,0,1).
С помощью метода render мы сначала применяем текущее преобразование модели и затем обрабатываем циклом ее vertexList для установки экранных координат. Эта функция нужна для этой игры и ее структуры, однако она была бы бесполезна при применении в этом же виде к другим объектам, поскольку только две из наших моделей непосредственно используют этот метод - шарик и цель. Шарик имеет только одну вершину, тогда как у цели их две (две мини-цели, каждая с одной вершиной). Поэтому нам не нужно беспокоиться о сторонах, цвете и т.д., и мы проходим циклом вершины только из-за того, что у нас есть две цели.
В остальном, этот код в точности повторяет программу для куба в предыдущей лекции (рассматриваемый параметр clip содержит ссылку на один из объектов innerBall, topTarget или bottomTarget ), с добавленной строкой для непосредственного изменения размеров фильмов по мере их движения около сферы. В случае с кубом мы этого не делали, но при работе с моделями с единственными вершинами физический фильм должен увеличиваться для моделирования глубины.
Создание Model завершено, однако этот код не обеспечивает все, что необходимо для трехмерного пространства. На самом деле, это лишь методы, необходимые для всех моделей на нашем рабочем месте.
С этого момента ракетки, шарик и цели будут иметь различные функциональности, с которыми мы будем иметь дело с помощью наследственности. Не страшно, если вы не работали с наследственностью ранее. Если вы разберетесь с данной темой, вы будете знать почти все, что вам понадобится.
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();
}
}
};
Итак, что же такое 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, написанному в последнем параграфе кода. Здесь мы снчала перемещаем нашу модель с использованием четырех переменных - трех измерений пространства (представлены векторами) и скорость шарика (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;
};
Настало время рассмотреть математику трехмерного пространства - давно пора! Метод 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 ), устанавливает обратно на 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! Да, это так, однако давайте вернемся к обсуждению цепочек наследования. При использовании , 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();
Да, всплывающие окна также являются моделями. Здесь видно, что конструктору будут отправлены переменные ширины (w) и высоты (h) для изменения размера всплывающего окна, однако я оставил размер таким же. Почти весь этот блок кода вам знаком из работы с предыдущей лекцией. Обратите внимание на присутствие дополнительного параметра w для каждой вершины так, чтобы можно было использовать матрицы размером 4x4. Также видно, что я оптимизировал 3D-подсветку модели, добавив свои собственные настройки применительно к sideColor освещенных и затемненных сторон.
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 для объекта frameCode. frameCode содержит код, который будет выполняться в игре повсеместно.
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();
};
Изначально я задавался вопросом создания этих методов для нового класса целей, но в итоге пришел к выводу, что проще поддерживать их таким способом. 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();
};
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 определяет соответствующий звук для проигрывания в зависимости от параметра шарика: 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();
};
Этот блок кода выполняется в каждом кадре для вызова всех функций и методов, которые вы только что создали. Вы можете видеть, что код в большей части блока связан с отслеживанием того, какие клавиши были нажаты, и с обработкой соответствующих событий.
Первые два выражения 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();
};
};
Я не буду разбирать каждую строку этого кода, так как он довольно прост и говорит сам за себя. Функция создает исходное всплывающее окно при загрузке игры. Она создает серый, частично прозрачный квадрат для "затуманивая" интерфейса, и затем создается новый инстанс класса 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();
};
};
Здесь содержится другой текст и форматирование для всплывающего окна с сообщением о завершении игры, но в целом этот код ничем не отличается от предыдущей функции.
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);
Теперь это все уже вам знакомо, не так ли? , 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 с некоторыми дополнительными улучшениями. Наиболее заметно добавление звуковой дорожки к игре (предоставлено Imagescore Music ), а также то, что шарик "подается" в каждом раунде со специальным эффектом градиента. Начало каждой игры изменяет цвет интерфейса, а также происходит небольшой "танец" ракеток, управляемый прекрасным новым параметром Sound.duration. У самой ракетки пониженное затемнение, поэтому вы сможете видеть шарик через нее. Я также добавил функцию паузы, активизирующуюся при нажатии пользователем клавиши (пробел). Последним и наиболее сложным дополнением является такое расположение целей траектории, что внутренняя цель располагается именно на том месте, где шарик ударится о стенку периметра сферы. Откройте этот вариант игры, чтобы посмотреть на код, необходимый для данных улучшений.
Конечно, существует еще очень много возможностей для оптимизации и дальнейшего совершенствования игры. Это может быть какой-либо эффект при открытии игры, изменение размеров ракеток для повышения сложности, или введение уровней сложности установкой конечного числа, которое необходимо достигнуть в каждом раунде. Вы можете также использовать некоторые сценарии для создания доски с рекордами по количеству очков, или использовать новую возможность Flash MX, связанную с общими объектами для хранения информации о рекордном количестве очков пользователей. Также можно расширить возможности игры с помощью добавления ООП-кода. При этом сначала рекомендую создать класс Target.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.