Очки и деньги

Для подсчета набранных очков, лучше всего суммировать maxHP уничтоженных астероидов. В игре должно быть ограничение на количество добавляемых турелей, для этого введем деньги. Игроку изначально дается некоторая сумма монет, за каждый уничтоженный астероид так же добавляются монеты. У каждого вида турели будет своя стоимость.

Добавим на панель два текстовых поля для очков score_label и для монет money_label. В класс panel добавляем две переменных

public var score:Number;    // сколько игрок набрал очков
public var money:Number;    // сколько игрок имеет денег

И в функции обновления статистики Update выводим значения монет и очков.

public function Update(event:Event):void {
     . . .
     // Счет
     this.score_label.text = Math.floor(score);
     // деньги
     this.money_label.text = money;
}

Для наглядности справа от каждого вида турели на панели управления разместим текстовые поля, стоимости этой турели, поля будем называть money_t1, money_t2, … В базовый класс турелей добавляем новое поле cost – стоимость турели. Во всех потомках, в конструкторе прописываем стоимости. Например, для лазерной (класс turret_laser) cost=20, для ракетной 40, для бастиона 70. Цифры навскидку, их потом нужно будет балансировать при окончательном тесте игры.

В функцию Update добавим подсветку красным цветом турелей и их стоимости, на установку которых в данный момент недостаточно монет.

public function Update(event:Event):void {
     . . .
     // подсветим красным турели, которые сейчас нельзя поставить из-за нехватки денег
     var i:Number = 1;
     var ok:Boolean;
     while (this["t"+i]){ // есть такая турель
           ok = this["t"+i].cost <= money;
           this["t"+i].transform.colorTransform = new ColorTransform(1,1,1, 1, ok?0:200,0,0, 0);
           this["money_t"+i].textColor = ok ? 0xFFFFFF : 0xFF0000;
           i++;
     }

Если игрок пытается поставить на поле турель, на покупку которой недостаточно монет, надо дать ему понять, почему турель не добавилась. Для этого в течение секунды включим мерцание текстового поля стоимости турели и поля общего числа монет.

Добавим две переменных: какое поле мерцает и сколько еще мерцать.

var twinkling_cnt:Number;   // счетчик мерцания стоимости
var twinkling_nm:String;    // для какой турели мерцает стоимость

Для мерцания по событию ENTER_FRAME будем включать и выключать видимость нужных полей.

// Мерцает стоимость указанной турели
function Twinkling(nm:String):void {
     if (twinkling_cnt){ // уже что-то мерцает
           stopTwinkling(); // прекращаем
     }
     twinkling_nm=nm;
     twinkling_cnt=21;      // мерцаем секунду
     // добавляем обработку события по ENTER_FRAME
     addEventListener(Event.ENTER_FRAME, doTwinkling);
}
// функция мерцания
function doTwinkling(event:Event):void {
     if (--twinkling_cnt){ // еще мерцаем
           this["money_"+twinkling_nm].visible =
                this.money_label.visible = !this.money_label.visible;
     } else { // закончили
           stopTwinkling();
     }
}
// функция окончания мерцания
function stopTwinkling():void {
     // возвращаем видимость полей
     this.money_label.visible = this["money_"+twinkling_nm].visible = true;
     // удаляем событие
     removeEventListener(Event.ENTER_FRAME, doTwinkling);
}

В функции добавления новой турели проверяем, достаточно ли монет и если нет, то турель не добавляем, а включаем мерцание стоимости.

Осталось добавить увеличение счета и монет. Пишем функцию:

// Добавляет очки
public function addPoints(asteroid_HP:Number):void {
     score += asteroid_HP /10;    // добавляем очки
     money += Math.floor(asteroid_HP /10);      // и монеты
     Update()// обновить статистику
}

А в классе sky, при удалении астероида вызываем эфу функцию, передавая maxHP астероида.

root.panel.addPoints(obj.maxHP);
 

Game over

Осталось добавить начало игры и окончание игры. Делаем следующую раскадровку флешки:

1-й кадр – прелоадер

2-й кадр – кнопка «Start game» и за пределами видимости размещаем все мувиклипы с классами, которые аттачатся в игре (т.к. мы убирали у всех галочку «Export in first frame», то их необходимо разместить на timeline).

3-й кадр – собственно основные классы игры: sky и panel

На втором кадре пишем следующий код:

stop();
play_btn.addEventListener(MouseEvent.CLICK, doPlay);
function doPlay(e:MouseEvent):void {
     play();
}

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

И создаем новый мувиклип в котором крупно пишем статичный текст «Game over», ниже разместим динамическое текстовое поле score_label в которое запишем набранные очки. Сделаем, чтобы при добавлении этого класса экран плавно затемнялся и по окончании флешка переходит на второй кадр, т.е. к кнопке «Start game»

Класс GameOver выглядит так:

package main {
     import flash.display.Sprite;
     import flash.events.*;

     dynamic public class GameOver extends Sprite {
           var bg:Sprite;
           public function init() {
                var dx:Number = stage.stageWidth/2;
                var dy:Number = stage.stageHeight/2;
                // размещаемся в центре экрана
                x = dx;
                y = dy;
                // отобразим набранные очки
                this.score_label.text = "Score: "+Math.floor(root.panel.score);
                // создадим Sprite под текстом
                bg = new Sprite();
                addChildAt(bg, 0);
                // рисуем на нем черный прямоугольник размером с экран
                bg.graphics.beginFill(0);
                bg.graphics.drawRect(-dx,-dy, dx*2,dy*2);
                bg.graphics.endFill();
                // ставим полную прозрачность
                bg.alpha=0;
                // по ENTER_FRAME будем уменьшать прозрачность
                addEventListener(Event.ENTER_FRAME, Update);
           }
           function Update(e:Event):void {
                // уменьшаем прозрачность
                bg.alpha+=0.005;
                if (bg.alpha>=1){ // если экран полностью затемнился
                     root.sky.Done();    // у sky вызываем удаление всех объектов
                     root.gotoAndStop(2);       // переходим на второй кард флешки
                     removeEventListener(Event.ENTER_FRAME, Update); // удаляем обработку события
                     parent.removeChild(this)// удаляем себя
                }
           }
     }
}

Затемнение сделано уменьшением прозрачности черного прямоугольника во весь экран. В базовый класс sky добавим функцию Done, в которой удалим все созданные и летающие объекты, а так же обработку события ENTER_FRAME:

// закончили
public function Done():void {
     removeEventListener(Event.ENTER_FRAME, Update);
     for each (var obj:basic_object in all_moving){
           removeChild(obj);
     }
}

По окончанию игры все.

Звуки

Пора оживить игру звуковыми эффектами. На самом деле из-за вакуума космоса ничего там услышать нельзя, но нас это не волнует, игра должна звучать! Из звуков нужно подобрать:
1. что-то нейтральное для фоновой музыки
2. столкновение астероидов
3. взрыв астероида
4. падение астероида на землю
5. выстрел лазерной турели
6. выстрел ракетной турели
7. взрыв ракеты
8. взрыв планеты и game-over

Источник звука необходимо правильно позиционировать (звук слева или справа) и откорректировать громкость по удаленности от центра экрана.

Начнем с создания регулятора громкости. Рисуем мувиклип в виде полоски (мувик длиной 100 пикселей с названием "bg"), изображения динамика (преобразуем в мувик и даем название "off" – клик по динамику выключает звук) и ползунка (мувик с названием " track"), размещаем на панели управления.

Создаем класс volume для регулятора. Здесь нам необходимо отловить клик по динамику (off), чтобы выключать/включать звук, клик по фоновой полоске, чтобы передвинуть туда ползунок, и реализовать перетаскивание ползунка. Примерно так:

// регулятор громкости звуков
package main {
     import flash.display.Sprite;
     import flash.events.*;
     import flash.geom.Rectangle;

     dynamic public class volume extends Sprite {
           static var Volume:Number = 0.7;       // громкость звуков
           var oldVolume:Number;          // громкость звуков до выключения

           public function volume() {
                // клик по фоновой полоске
                this.bg.addEventListener(MouseEvent.CLICK, handleBgClick);
                // клик по изображению динамика
                this.off.addEventListener(MouseEvent.CLICK, handleOffClick);
                this.off.buttonMode=true;
                // для перетаскивания ползунка перехватываем нажатие и отпускание кнопки мышки
                this.track.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseDown);
                this.track.addEventListener(MouseEvent.MOUSE_MOVE, handleMouseMove);
                this.track.addEventListener(MouseEvent.MOUSE_UP, handleMouseUp);
                // Установить ползунок
                setVolume(Volume);
           }

           // Установить громкость
           public function setVolume(newVolume:Number):void {
                Volume=newVolume;
                this.track.x = Volume*100; // предвигаем ползунок
           }

           // клик по фоновой полоске
           function handleBgClick(event:Event):void {
                setVolume(this.bg.mouseX/100);
           }
           // клик по изображению динамика
           function handleOffClick(event:Event):void {
                if (this.off.currentFrame==1){ // выключаем звук
                     // запомним текущую громкость
                     oldVolume = Volume;
                     // уберем ползунок
                     this.track.visible=false;
                     setVolume(0);
                     // переходим на кадр, где перечеркнутый динамик
                     this.off.gotoAndStop(2);
                } else { // восстанавливаем звук
                     setVolume(oldVolume);
                     // восстановим ползунок
                     this.track.visible=true;
                     // переходим на кадр, где нормальный динамик
                     this.off.gotoAndStop(1);
                }
           }
           // начало перетаскивания ползунка
           function handleMouseDown(event:Event):void {
                var r:Rectangle = new Rectangle(0,0, 100,0);
                // начнем перетаскивание
                this.track.startDrag(true, r);
           }
           // при перемещении ползунка меняем громкость
           function handleMouseMove(event:Event):void {
                setVolume(this.track.x/100);
           }
           // окончание перетаскивания ползунка
           function handleMouseUp(event:Event):void {
                // прекратили перетаскивание
                this.track.stopDrag();
           }

     }
}

Здесь мы впервые применили статичную переменную

           static var Volume:Number = 0.7;       // громкость звуков

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

trace(volume.Volume);

Теперь займемся непосредственно звуками. Управление звуками в AS3.0 сильно переделали, поэтому сначала знакомимся с документацией. Класс Sound, здесь нам нужно только функция play, видим, что для управления трансформацией звука (громкость и положение левый/правый канал) используется класс SoundTransform. Там есть нужные нам свойства:

pan : Number
The left-to-right panning of the sound, ranging from -1 (full pan left) to 1 (full pan right).
volume : Number
The volume, ranging from 0 (silent) to 1 (full volume).

Вроде бы все просто.

Для того чтобы начать проигрывание какого-то звука его нужно загрузить (внешние файлы мы не используем), значит, ставим в свойствах звуков «Export for ActionScript», класс пусть генерится автоматически, писать там нечего. Убираем галочку с «Export in first frame», нам нужно, чтобы звуки грузились во втором кадре флешки, а не в первом, где у нас прелоадер (первый кадр должен быть максимально легким). Для этого создаем пустой мувиклип, в первом кадре пишем stop(), создаем несколько пустых ключевых кадров и в каждый ставим проигрывание звука, см. скриншот:


Рис. 5

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

Теперь создаем класс sounds, который будет отвечать за проигрывание всех звуков в игре.

// Управление пространственными звуками в игре
package main {
     import flash.media.SoundTransform;
     import main.Vector;

     dynamic public class sounds extends Object {
           var snd:Object// хэш со звуками
           var stageRadius:Number; // примерный радиус видимой области

           public function init(stageWidth:Number, stageHeight:Number) {
                // создадим все звуки
                snd = new Object();
                snd['asteroid_explosion'] = new asteroid_explosion();
                snd['asteroids_clash'] = new asteroids_clash();
                snd['EarthCrash'] = new EarthCrash();
                snd['explode_rocket'] = new explode_rocket();
                snd['explode_to_earth'] = new explode_to_earth();
                snd['lasersound'] = new lasersound();
                snd['launch_rocket'] = new launch_rocket();
                // вычислим примерный радиус (в квадрате) видимой области экрана
                stageRadius = Math.pow((stageWidth+stageHeight)/4, 2);
           }

           // Воспроизводит звук с учетом пространства
           // dx,dy - смещение источника звука относительно центра экрана
           // dv - множитель для громкости
           public function Play2DSnd(snd_name:String, dx:Number, dy:Number):void {
                var t:SoundTransform, v:Vector;
                if (! volume.Volume) return;    // запрет звуков
                v = new Vector(dx, dy);
                t = new SoundTransform();
                // Ставим громкость в зависимости от расстояния
                // Делаем так, чтобы громкость в углу экрана (stageRadius) была 50% от общей
                t.volume = volume.Volume / (1 + v.magnitude2()/stageRadius);
                // стерео в зависимости от угла до источника звука
                t.pan = 1 - Math.abs(v.getDirection())/90;
                // воспроизводим
                snd[snd_name].play(0, 0, t);
           }
     }
}

В панель управления добавляем переменную типа sounds и новую функцию

var game_snd:sounds;        // звуки
 . . .
// проигрывает указанный звук, местоположение определяется по объекту
public function PlaySnd(snd_name:String, obj:Sprite):void {
     var p:Point = obj.localToGlobal(new Point(0,0));
     game_snd.Play2DSnd(snd_name, p.x-stage.stageWidth/2, p.y-stage.stageHeight/2);
}

Далее во всех классах, где нужно включать какой-то звук вызываем root.panel.PlaySnd. Например, в лазерной турели в функции Fire в конец добавляем

root.panel.PlaySnd('lasersound', this);

По звукам все.

Тестирование и балансировка

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

Рисуем мувиклип, состоящий из черного полупрозрачного прямоугольника, размещаем на втором кадре флешки, там будут динамически созданы все настраиваемые поля, даем имя params и пишем простой класс, основная задача которого возвращать основные игровые константы.

// Настройка игровых констант
package main {
     import flash.display.Sprite;
     import flash.text.*;

     dynamic public class base_params extends Sprite {
           // Описание всех коснтант
           var all_const:Array = [
                {name:'moving_object_MIN_SPEED', txt:'Мин. скорость астероидов', value:2},
                {name:'moving_object_MAX_SPEED', txt:'Макс. скорость астероидов', value:7},
. . .
           ];
           var input_fields:Object;

           public function base_params() {
                var i:Number, y:Number, tf:TextField;
                input_fields = new Object();
                // динамически создаем все поля
                y=1;
                for (i=0; i<all_const.length; i++){
                     // создаем описание параметра
                     tf = new TextField();
                     tf.y=y;
                     tf.width=240;
                     tf.height=18;
                     tf.autoSize=TextFieldAutoSize.RIGHT;
                     tf.selectable=false;
                     tf.textColor=0xFFFFFF;
                     tf.text = all_const[i].txt;
                     addChild(tf);
                     // создаем редактируемое поле
                     tf = new TextField();
                     tf.y=y;
                     tf.x=240;
                     tf.width=56;
                     tf.height=18;
                     tf.type=TextFieldType.INPUT;
                     tf.background = tf.border = true;
                     tf.text = all_const[i].value;
                     addChild(tf);
                     // запоминаем ссылку на текстовое поле
                     input_fields[ all_const[i].name ] = tf;
                     y+=19;
                }
           }

           // Возвращает значение указанной константы
           public function getConst(const_name:String):String {
                return input_fields[const_name].text;
           }

     }
}

Получается вот такая картина:


Рис. 6

Далее во всех классах, вместо констант временно подставляем функции, которые возвращают значение, запрашивая его у класса params. Например, в классе астероидов:

     dynamic public class moving_object extends basic_object {
//         const MIN_SPEED:Number = 2;    // раброс начальной скорости
//         const MAX_SPEED:Number = 7;
function get MIN_SPEED():Number {
     return Number(root.params.getConst("moving_object_MIN_SPEED"));
}
function get MAX_SPEED():Number {
     return Number(root.params.getConst("moving_object_MAX_SPEED"));
}

И раздаем флешку всем своим друзьям с просьбой «протестируй, попробуй поменять параметры»…

Вариант для тестирования и балансировки: перед стартом игры можно изменить игровые константы подбирая оптимальные значения. На панели управления добавлена статистика и регулятор громкости. В игру добавлены еще два вида турелей.

По окончании тестирования прописываем полученные константы вместо класса params.

Заключение

Игра написана. Окончательный вариант можно посмотреть здесь.
Скачать исходные коды с описанием процесса создания: AsteroidStormSourceCode.zip (3.4 Мб)

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

На примере созданной игры при большом игровом поле 800x440 и большом количестве одновременно отображаемых и обрабатываемых объектов (более 200), Flash показывает приличную производительность, что было нереально в предыдущих версиях. Полный переход к объектно-ориентированному программированию упрощает написание скриптов, они становятся более структурированные и понятны.

Полагаю, в ближайшем будущем следует ожидать появления больших и серьезных игр, полностью написанных на Flash, таких как онлайн RPG www.timezero.ru

Данную статью и исходные коды разрешаю (и даже настаиваю :) совершенно свободно распространять в интернете.  

Июль 2006г.
Алексей (Merlin)


Страницы: 1 2 3 4 [5]