Статьи

Сделайте игру Tower Defense в AS3: цель и огонь

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


Как только мы закончим этот урок, у нас будет следующее:

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


Определение Википедии хорошо подводит итог:

Цель игр защиты башни — попытаться остановить врагов от пересечения карты, строя башни, которые стреляют в них, когда они проходят.

По сути, это то, что мы будем развивать в этой серии уроков. Помните, что в этом уроке мы называем башни башенками.

Cursed Treasure — отличный пример TD игры, если вы все еще не уверены.


Прежде чем приступить к разработке игры, нам нужно настроить проект в нашей IDE. Я буду использовать FlashDevelop здесь. Если вы хотите прочитать о том, как настроить проект в Flash Develop, прочитайте Шаги 1 и 2 этого руководства или это полное руководство по FlashDevelop .

Теперь у вас должен быть главный класс Main.as со следующим кодом:

01
02
03
04
05
06
07
08
09
10
11
package
{
    import flash.display.Sprite;
 
    public class Main extends Sprite
    {
        public function Main():void
        {
        }
    }
}

Для этой части наша игра будет иметь следующие игровые элементы:

  1. Игровое поле: область, где будут размещены все игровые элементы.
  2. Заполнитель башни: это место на игровом поле, предназначенное для размещения башни.
  3. Turret: Наше оружие в игре, которое можно разместить на турель-заполнителях.
  4. Пуля: И наконец, частицы, которые стреляют из турелей.

Все вышеупомянутые элементы будут созданы в Main.as кроме турелей, которые будут отдельным классом турели.

Давайте начнем кодировать сейчас!


Сначала мы создадим функцию с именем createTurretPlaceholder() которая создаст и вернет нам заполнитель-спрайт. Добавьте следующее в Main класс:

01
02
03
04
05
06
07
08
09
10
11
private function createTurretPlaceholder():Sprite {
    var placeholder:Sprite = new Sprite();
     
    // draw the graphics
    var g:Graphics = placeholder.graphics;
    g.beginFill(0xCE7822);
    g.drawCircle(0, 0, 20);
    g.endFill();
 
    return placeholder;
}

Эта функция просто создает placeholder переменной Sprite . Затем, используя API-интерфейс рисования Actionscript, мы создаем графику, которая представляет собой простой круг. Наконец он возвращает этот спрайт.


Теперь мы создадим три заполнителя, используя предыдущую функцию, и разместим их в разных местах поля. Добавьте следующий код в конструктор Main() :

1
var placeholder1:Sprite = createTurretPlaceholder();

В приведенном выше заявлении мы создаем переменную placeholder1 типа Sprite которая получает заполнитель из вышеупомянутой функции createTurretPlaceholder() .

1
placeholder1.x = 200;

Мы позиционируем заполнитель на поле.

1
addChild(placeholder1);

А затем мы добавляем заполнитель на сцену.

Используя тот же код, мы добавим в поле еще два заполнителя — поэтому ваша функция Main() должна выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public function Main() {
    var placeholder1:Sprite = createTurretPlaceholder();
    placeholder1.x = 200;
 
    var placeholder2:Sprite = createTurretPlaceholder();
    placeholder2.x = 60;
 
    var placeholder3:Sprite = createTurretPlaceholder();
    placeholder3.x = 350;
 
    addChild(placeholder1);
    addChild(placeholder2);
    addChild(placeholder3);
}

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

Перейдите к созданию нового класса Turret , производного от Sprite , в файле с именем Turret.as . Он должен иметь следующий основной код:

01
02
03
04
05
06
07
08
09
10
11
12
13
package
{
    import flash.display.Sprite;
 
 
    public class Turret extends Sprite
    {
         
        public function Turret()
        {
        }
    }
}

Теперь, когда у нас есть базовая структура класса Turret , следующий шаг — дать башне немного графики. Для этого мы создаем новую функцию draw() в классе. Поэтому поместите следующую функцию чуть ниже конструктора:

1
2
3
4
5
6
7
8
private function draw():void {
    var g:Graphics = this.graphics;
    g.beginFill(0xD7D700);
    g.drawCircle(0, 0, 20);
    g.beginFill(0x800000);
    g.drawRect(0, -5, 25, 10);
    g.endFill();
}

Как вы могли заметить в коде, мы рисуем на нем круг и прямоугольник. Вот так будет выглядеть наша турель. Теперь мы вызываем эту функцию из самого конструктора.

1
2
3
4
public function Turret()
{
    draw();
}

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

Для начала нам нужна турель-призрак. Идем дальше и объявляем переменную для него в классе Main .

1
private var ghost_turret:Turret;

Теперь создайте новую турель в конструкторе Main() :

1
ghost_turret = new Turret();

Дайте призрачной башне некоторые свойства, чтобы она выглядела как призрак:

1
2
3
ghost_turret.alpha = 0.5;
ghost_turret.mouseEnabled = false;
ghost_turret.visible = false;

В приведенном выше коде мы уменьшаем непрозрачность башни до половины (0,5) и устанавливаем для свойства mouseEnabled башни значение false чтобы призрачная башня не получала никаких событий мыши. Почему? Мы увидим это позже. И поскольку турель-призрак по умолчанию будет невидима, мы ее скрываем.

Наконец, добавьте турель в список отображения:

1
addChild(ghost_turret);

Ваш Main конструктор должен выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public function Main() {
    var placeholder1:Sprite = createTurretPlaceholder();
    placeholder1.x = 200;
 
    var placeholder2:Sprite = createTurretPlaceholder();
    placeholder2.x = 60;
 
    var placeholder3:Sprite = createTurretPlaceholder();
    placeholder3.x = 350;
 
    addChild(placeholder1);
    addChild(placeholder2);
    addChild(placeholder3);
     
    ghost_turret = new Turret();
    ghost_turret.alpha = 0.5;
    ghost_turret.mouseEnabled = false;
    ghost_turret.visible = false;
     
    addChild(ghost_turret);
}

Если вы запустите фильм сейчас (Ctrl + Enter), все, что вы увидите, это три заполнителя на сцене. Скучно, а? Давайте добавим немного интерактивности.


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

01
02
03
04
05
06
07
08
09
10
11
12
13
private function createTurretPlaceholder():Sprite {
    var placeholder:Sprite = new Sprite();
     
    // draw the graphics
    var g:Graphics = placeholder.graphics;
    g.beginFill(0xCE7822);
    g.drawCircle(0, 0, 20);
    g.endFill();
     
    placeholder.addEventListener(MouseEvent.MOUSE_OVER, showGhostTurret, false, 0, true);
    placeholder.addEventListener(MouseEvent.MOUSE_OUT, hideGhostTurret, false, 0, true);
    return placeholder;
}

Код присоединяет слушателей к MOUSE_OVER и MOUSE_OUT .

Далее мы определяем две функции-обработчики для одного и того же. Добавьте следующие две функции ниже функции createTurretPlaceholder() :

01
02
03
04
05
06
07
08
09
10
private function showGhostTurret(e:MouseEvent = null):void {
    var target_placeholder:Sprite = e.currentTarget as Sprite;
    ghost_turret.x = target_placeholder.x;
    ghost_turret.y = target_placeholder.y;
    ghost_turret.visible = true;
}
 
private function hideGhostTurret(e:MouseEvent = null):void {
    ghost_turret.visible = false;
}

hideGhostTurret() просто скрывает турель-призрак, но что происходит в функции showGhostTurret ? Посмотрим.

1
var target_placeholder:Sprite = e.currentTarget as Sprite;

Мы получаем ссылку на местозаполнитель, в котором присутствует мышь, используя MouseEvent currentTarget MouseEvent , типизированное для Sprite .

1
2
3
ghost_turret.x = target_placeholder.x;
ghost_turret.y = target_placeholder.y;
ghost_turret.visible = true;

Это просто … мы помещаем призрачную башню в кординаты заполнителя и делаем ее видимой. Запустите фильм, и вы должны увидеть призрачную башню при наведении курсора на заполнители. Ницца!


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

1
private var turrets:Array = [];

Затем присоедините другого слушателя чуть ниже двух предыдущих слушателей, которые мы добавили в createTurretPlaceholder() :

1
2
3
placeholder.addEventListener(MouseEvent.MOUSE_OVER, showGhostTurret, false, 0, true);
placeholder.addEventListener(MouseEvent.MOUSE_OUT, hideGhostTurret, false, 0, true);
placeholder.addEventListener(MouseEvent.CLICK, addTurret, false, 0, true);

addTurret() функцию-обработчик addTurret() функцией hideGhostTurret() :

1
2
private function addTurret(e:MouseEvent):void {
}

Теперь давайте напишем код для функции. Добавьте следующий код в addTurret() .

1
var target_placeholder:Sprite = e.currentTarget as Sprite;

Сначала мы получаем ссылку на заполнитель, по которому щелкали, как мы делали в функции showGhostTurret() .

1
var turret:Turret = new Turret();

Мы создаем новую башню в переменной с именем turret .

1
2
turret.x = target_placeholder.x;
turret.y = target_placeholder.y;

Далее мы позиционируем turret по координатам target_placeholder .

1
2
addChild(turret);
turrets.push(turret);

Когда турель создана, мы добавляем ее на сцену и помещаем в массив.

Ваша функция addTurret должна выглядеть примерно так:

1
2
3
4
5
6
7
8
private function addTurret(e:MouseEvent):void {
    var target_placeholder:Sprite = e.currentTarget as Sprite;
    var turret:Turret = new Turret();
    turret.x = target_placeholder.x;
    turret.y = target_placeholder.y;
    addChild(turret);
    turrets.push(turret);
}

Здесь нужно отметить одну вещь. Помните, мы установили свойство mouseEnabled башни-призрака в false ? Если бы мы этого не сделали, то призрачная башня между заполнителем и мышью захватила бы событие щелчка, тем самым предотвратив попадание события в заполнитель. В результате прослушиватель CLICK прикрепленный к заполнителю, не будет вызван.

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

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


Для этого урока мы заставим турель вращаться лицом к мыши. Мы будем хранить функциональность вращения в отдельном методе класса Turret . Этот метод будет называться update() .

Давайте добавим этот метод в Turret сейчас:

1
2
3
4
public function update():void {
    var angle:Number = Math.atan2(stage.mouseY — this.y, stage.mouseX — this.x) / Math.PI * 180;
    this.rotation = angle;
}

Все, что мы делаем в этой функции — это вычисляем угол наклона мыши от башни и устанавливаем для нее угол наклона башни. (Посмотрите Trigonometry для разработчиков Flash, если вы не уверены, как это работает.)

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


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

Создайте функцию с именем gameLoop() под конструктором Main() которая будет прослушивать событие ENTER_FRAME фильма:

1
2
private function gameLoop(e:Event):void {
}

Теперь, когда мы определили функцию слушателя, нам нужно присоединить ее к соответствующему событию. Мы делаем это в конструкторе Main() . Добавьте следующую строку в функцию Main() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public function Main() {
    var placeholder1:Sprite = createTurretPlaceholder();
    placeholder1.x = 200;
 
    var placeholder2:Sprite = createTurretPlaceholder();
    placeholder2.x = 60;
 
    var placeholder3:Sprite = createTurretPlaceholder();
    placeholder3.x = 350;
 
    addChild(placeholder1);
    addChild(placeholder2);
    addChild(placeholder3);
     
    ghost_turret = new Turret();
    ghost_turret.alpha = 0.5;
    ghost_turret.mouseEnabled = false;
     
    addChild(ghost_turret);
    stage.addEventListener(Event.ENTER_FRAME, gameLoop);
}

Давайте поместим некоторый код в нашу gameLoop() .

1
2
for each(var turret:Turret in turrets) {
}

Мы перебираем массив turrets , который имеет ссылки на все турели на сцене, используя for each...in цикле.

1
2
3
for each(var turret:Turret in turrets) {
    turret.update();
}

И вызвать функцию update() каждой башни. И мы сделали. Если вы запустите фильм сейчас, вы сможете развернуть турели, которые всегда обращены к мышке. Что-то вроде этого:


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

  1. Добавьте слушателя щелчка к стадии.
  2. Переберите все турели в вышеупомянутом слушателе.
  3. Рассчитайте угол от точки нажатия до башни.
  4. Создайте новую пулю и переместите ее в соответствующем направлении.

Давайте объявим функцию слушателя по имени shoot Добавьте функцию в класс Main :

1
2
private function shoot(e:MouseEvent):void {
}

А затем присоедините указанный выше слушатель к событию CLICK stage в конструкторе Main() :

1
2
3
stage.addEventListener(MouseEvent.CLICK, shoot);
stage.addEventListener(Event.ENTER_FRAME, gameLoop);
}

Перед тем, как приступить к написанию кода для стрельбы в функции shoot() , мы определим новую функцию для создания маркера, как мы делали для создания заполнителя. Поэтому поместите следующую функцию ниже createTurretPlaceholder() :

1
2
3
4
5
6
7
8
9
private function createBullet():Sprite {
    var bullet:Sprite = new Sprite();
    // draw the graphics
    var g:Graphics = bullet.graphics;
    g.beginFill(0xEEEEEE);
    g.drawCircle(0, 0, 5);
    g.endFill();
    return bullet;
}

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


Пора добавить код в shoot() .

1
2
for each(var turret:Turret in turrets) {
}

Сначала мы перебираем все турели на сцене, используя цикл for each...in .

1
var new_bullet:Sprite = createBullet();

Теперь для каждой башни мы создаем пулю, используя функцию, которую создали ранее.

1
new_bullet.rotation = turret.rotation;

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

1
2
new_bullet.x = turret.x + Math.cos(new_bullet.rotation * Math.PI / 180) * 25;
new_bullet.y = turret.y + Math.sin(new_bullet.rotation * Math.PI / 180) * 25;

Эти две линии устанавливают начальную позицию пули, которая находится на расстоянии 25 пикселей от револьверной головки в направлении к лицу (помните свойство rotation ). Опять же, прочтите тригонометрию, если она вам незнакома.

1
addChild(new_bullet);

И как обычный последний шаг, мы добавляем маркер в список отображения сцены.

Вот как должна выглядеть ваша функция shoot() :

1
2
3
4
5
6
7
8
9
private function shoot(e:MouseEvent):void {
    for each(var turret:Turret in turrets) {
        var new_bullet:Sprite = createBullet();
        new_bullet.rotation = turret.rotation;
        new_bullet.x = turret.x + Math.cos(new_bullet.rotation * Math.PI / 180) * 25;
        new_bullet.y = turret.y + Math.sin(new_bullet.rotation * Math.PI / 180) * 25;
        addChild(new_bullet);
    }
}

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


Чтобы понять это, нам нужно понять распространение событий в AS3 .

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

У нас есть слушатель CLICK привязанный к сцене и заполнителям. Но заполнители также дети сцены. Поэтому, когда мы щелкаем местозаполнитель, генерируется событие CLICK с целью в качестве заполнителя. Событие распространяется от этапа к заполнителю — фазе захвата . Он достигает заполнителя и его обработчик CLICK , addTurret() , выполняется, и у нас есть турель на сцене. Теперь событие распространяется в обратном направлении — фаза барботирования — и когда оно снова достигает стадии, оно также находит слушателя CLICK , который исполняется. В результате выполняется функция shoot() и на сцену добавляется пуля.

Так вот в чем проблема, но как ее решить? Нам нужно остановить дальнейшее распространение события, когда оно достигнет цели. Это означает, что в addTurret() мы прекращаем распространение события. Итак, добавьте строку в конце addTurret() :

1
2
3
4
5
6
7
8
9
private function addTurret(e:MouseEvent):void {
    var target_placeholder:Sprite = e.currentTarget as Sprite;
    var turret:Turret = new Turret();
    turret.x = target_placeholder.x;
    turret.y = target_placeholder.y;
    addChild(turret);
    turrets.push(turret);
    e.stopPropagation();
}

Строка, которую мы добавили, останавливает распространение события и не достигает стадии. Для более полного понимания структуры событий в Actionscript 3.0 прочитайте пост AS3 101 . Давайте продолжим с игрой.


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

1
2
3
private var ghost_turret:Turret;
private var turrets:Array = [];
private var bullet_speed:uint = 3;

Затем добавьте следующую функцию слушателя в класс Main :

1
2
3
4
5
private function moveBullet(e:Event):void {
    var bullet:Sprite = e.currentTarget as Sprite;
    bullet.x += Math.cos(bullet.rotation * Math.PI / 180) * bullet_speed;
    bullet.y += Math.sin(bullet.rotation * Math.PI / 180) * bullet_speed;
}

Что мы делаем в этом слушателе:

  • Получить ссылку на пулю, чей слушатель был запущен.
  • bullet_speed положение пули на величину bullet_speed в направлении вращения пули.

Наконец, присоедините только что созданный слушатель к событию ENTER_FRAME пули в функции shoot() :

01
02
03
04
05
06
07
08
09
10
private function shoot(e:MouseEvent):void {
    for each(var turret:Turret in turrets) {
        var new_bullet:Sprite = createBullet();
        new_bullet.rotation = turret.rotation;
        new_bullet.x = turret.x + Math.cos(new_bullet.rotation * Math.PI / 180) * 25;
        new_bullet.y = turret.y + Math.sin(new_bullet.rotation * Math.PI / 180) * 25;
        new_bullet.addEventListener(Event.ENTER_FRAME, moveBullet, false, 0, true);
        addChild(new_bullet);
    }
}

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


Добавьте следующий код в конец функции moveBullet() :

01
02
03
04
05
06
07
08
09
10
11
private function moveBullet(e:Event):void {
    var bullet:Sprite = e.currentTarget as Sprite;
    bullet.x += Math.cos(bullet.rotation * Math.PI / 180) * bullet_speed;
    bullet.y += Math.sin(bullet.rotation * Math.PI / 180) * bullet_speed;
     
    if (bullet.x < 0 || bullet.x > stage.stageWidth || bullet.y < 0 || bullet.y > stage.stageHeight) {
        bullet.removeEventListener(Event.ENTER_FRAME, moveBullet);
        bullet.parent.removeChild(bullet);
        bullet = null;
    }
}

Здесь мы проверяем, находится ли пуля за пределами сцены. Если это так, мы удаляем его слушатель ENTER_FRAME и удаляем пулю со сцены. Мы также устанавливаем переменную bullet в значение null чтобы пуля не имела ссылки и была доступна для сборки мусора .


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

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