Статьи

Сделайте игру Tower Defense в AS3: враги и базовый AI

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


Это игра, которую мы собираемся создать в этом уроке:

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


В предыдущем уроке мы разработали игру, в которой были заполнители для турелей. Мы могли бы развернуть турели, щелкнув эти метки-заполнители, и турели нацеливались на указатель мыши и стреляли пулями в направлении точки, где щелкнул пользователь.

Мы закончили с Main классом, который имел игровой цикл и игровую логику. Помимо этого у нас был класс Turret котором не было ничего, кроме функции update которая заставляла турель вращаться.


Ранее мы создали ENTER_FRAME в классе Main и ENTER_FRAME слушатель ENTER_FRAME для его перемещения. У пули не было достаточно свойств, чтобы считать ее отдельным классом. Но в такой игре маркеры могут иметь много разновидностей, таких как скорость, урон и т. Д., Поэтому будет полезно вытащить код Bullet и инкапсулировать его в отдельный класс Bullet . Давай сделаем это.

Создайте новый класс с именем Bullet , расширяющий класс Sprite . Основной код для этого класса должен быть:

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

Затем мы помещаем код для рисования графического Bullet , взятого из Main , в Bullet . Как и в случае с классом Turret , мы создаем функцию с именем draw в классе Bullet :

1
2
3
4
5
6
private function draw():void {
    var g:Graphics = this.graphics;
    g.beginFill(0xEEEEEE);
    g.drawCircle(0, 0, 5);
    g.endFill();
}

И мы вызываем эту функцию из конструктора Bullet :

1
2
3
public function Bullet() {
    draw();
}

Теперь добавим некоторые свойства к пуле. Добавьте четыре переменные: speed , speed_x , speed_y и damage перед конструктором Bullet :

1
2
3
4
private var speed:Number;
private var speed_x: Number;
private var speed_y:Number;
public var damage:int;

Для чего эти переменные?

  • speed : эта переменная хранит скорость пули.
  • speed_x и speed_y : они хранят компоненты скорости x и y соответственно, так что вычисление разбиения скорости на ее компоненты не нужно делать снова и снова.
  • damage : это количество урона, которое пуля может нанести врагу. Мы оставляем эту переменную общедоступной, поскольку нам это потребуется в нашем игровом цикле в классе Main .

Мы инициализируем эти переменные в конструкторе. Обновите конструктор Bullet :

1
2
3
4
5
6
7
public function Bullet(angle:Number) {
    speed = 5;
    damage = 1;
    speed_x = Math.cos(angle * Math.PI / 180) * speed;
    speed_y = Math.sin(angle * Math.PI / 180) * speed;
    draw();
}

Обратите внимание на переменную angle мы получаем в конструкторе. Это направление (в градусах), в котором будет двигаться пуля. Мы просто разбиваем speed на ее компоненты x и y и кешируем их для будущего использования.

Последнее, что остается в классе Bullet это наличие функции update которая будет вызываться из игрового цикла для обновления (перемещения) маркера. Добавьте следующую функцию в конец класса Bullet :

1
2
3
4
public function update():void {
    x += speed_x;
    y += speed_y;
}

Бинго! Мы закончили с нашим классом Bullet .


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

Сначала удалите функции createBullet() и moveBullet() . Также удалите переменную bullet_speed .

Затем перейдите к функции shoot и обновите ее следующим кодом:

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

Мы больше не используем функцию createBullet для создания createBullet а используем конструктор Bullet и передаем ему rotation турели, которое является направлением движения пули, и поэтому нам не нужно сохранять его в свойстве rotation пули, как мы делали ранее. Также мы не привязываем слушателя к пуле, поскольку пул будет обновляться из цикла игры далее.


Теперь, когда нам нужно обновить маркеры из игрового цикла, нам нужна ссылка на них, чтобы где-то их хранить. Решение такое же, как и для турелей: создайте новый Array именем bullets и вставьте пули в него по мере их создания.

Сначала объявите массив чуть ниже объявления массива turrets:

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

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

1
2
3
4
5
var new_bullet:Bullet = new Bullet(turret.rotation);
new_bullet.x = turret.x + Math.cos(turret.rotation * Math.PI / 180) * 25;
new_bullet.y = turret.y + Math.sin(turret.rotation * Math.PI / 180) * 25;
bullets.push(new_bullet);
addChild(new_bullet);

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

1
2
var turret:Turret;
var bullet:Bullet;

Идите дальше и добавьте следующий код в конце игрового цикла:

1
2
3
4
5
for (var i:int = bullets.length — 1; i >= 0; i—) {
    bullet = bullets[i];
    if (!bullet) continue;
    bullet.update();
}

Здесь мы просматриваем все маркеры на сцене каждый кадр и вызываем их функцию update которая заставляет их двигаться. Обратите внимание, что мы перебираем массив bullets в обратном порядке. Почему? Мы увидим это впереди.

Теперь, когда у нас уже есть переменная turret объявленная снаружи, нам не нужно объявлять ее снова внутри for...each цикла турели. Изменить его на:

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

Наконец, мы добавляем условие проверки границы; Ранее это было в ENTER_FRAME пули, но теперь мы проверяем это в цикле игры:

1
2
3
4
5
if (bullet.x < 0 || bullet.x > stage.stageWidth || bullet.y < 0 || bullet.y > stage.stageHeight) {
    bullets.splice(i, 1);
    bullet.parent.removeChild(bullet);
    continue;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
private function gameLoop(e:Event):void {
    var turret:Turret;
    var bullet:Bullet;
     
    for each(turret in turrets) {
        turret.update();
    }
     
    for (var i:int = bullets.length — 1; i >= 0; i—) {
        bullet = bullets[i];
        if (!bullet) continue;
        bullet.update();
    }
}

Если вы сейчас запускаете игру, вы должны иметь ту же функциональность, что и в части 1, с кодом, который намного более чист и организован.


Теперь добавим один из самых важных элементов игры: Враг. Прежде всего, создайте новый класс Enemy расширяющий класс Sprite :

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

Теперь мы добавим некоторые свойства в класс. Добавьте их перед вашим Enemy конструктор:

1
2
private var speed_x:Number;
private var speed_y:Number;

Мы инициализируем эти переменные в конструкторе Enemy :

1
2
3
4
5
public function Enemy()
{
    speed_x = -1.5;
    speed_y = 0;
}

Затем мы создаем функции draw и update для класса Enemy . Они очень похожи на те, что у Bullet . Добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
private function draw():void {
    var g:Graphics = this.graphics;
    g.beginFill(0xff3333);
    g.drawCircle(0, 0, 15);
    g.endFill();
}
 
public function update():void {
    x += speed_x;
    y += speed_y;
}

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

Разница заключается в том, что игра, основанная на единицах шага во времени, основана на реальном времени (т. Е. Количестве пройденных миллисекунд), но в игре, основанной на кадрах, единица шага основана на единицах кадров (т. Е. Количестве пройденных кадров).

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

1
2
3
4
private var ghost_turret:Turret;
private var turrets:Array = [];
private var bullets:Array = [];
private var global_time:Number = 0;

Мы увеличиваем эту переменную в игровом цикле вверху:

1
global_time++;

Теперь на основе этого счетчика мы можем делать такие вещи, как создание врагов, что мы будем делать дальше.


Теперь мы хотим создавать врагов на поле каждые две секунды. Но мы имеем дело с кадрами здесь, помните? Итак, через сколько кадров мы должны создавать врагов? Что ж, наша игра работает со скоростью 30 FPS, увеличивая счетчик global_time 30 раз каждую секунду. Простой расчет говорит нам, что 3 секунды = 90 кадров.

В конце игрового цикла добавьте следующий блок if :

1
2
if (global_time % 90 == 0) {
}

Что это за условие? Мы используем оператор по модулю (%), который дает остаток от деления — поэтому global_time % 90 дает нам остаток, когда global_time делится на 90 . Мы проверяем, равен ли остаток 0 , поскольку это будет только в случае, когда global_time кратно 90 то есть условие возвращает true когда global_time равно 0 , 90 , 180 и т. Д. Таким образом, мы достигаем запускать каждые 90 кадров или 3 секунды.

Прежде чем мы создадим врага, объявите еще один массив, называемый enemies чуть ниже массива turrets и bullets . Это будет использоваться для хранения ссылок на врагов на сцене.

1
2
3
4
5
private var ghost_turret:Turret;
private var turrets:Array = [];
private var bullets:Array = [];
private var enemies:Array = [];
private var global_time:Number = 0;

Также объявите переменную enemy в верхней части игрового цикла:

1
2
3
4
global_time++;
var turret:Turret;
var bullet:Bullet;
var enemy:Enemy;

Наконец добавьте следующий код внутри блока if который мы создали ранее:

1
2
3
4
5
enemy = new Enemy();
enemy.x = 410;
enemy.y = 30 + Math.random() * 370;
enemies.push(enemy);
addChild(enemy);

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


Точно так же, как мы обновляем пули в игровом цикле, мы обновляем врагов. Поместите следующий код под турелью for...each цикла:

1
2
3
4
5
6
7
8
9
for (var j:int = enemies.length — 1; j >= 0; j—) {
    enemy = enemies[j];
    enemy.update();
    if (enemy.x < 0) {
        enemies.splice(j, 1);
        enemy.parent.removeChild(enemy);
        continue;
    }
}

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


У каждого врага есть жизнь / здоровье, и у нас тоже. Мы также покажем оставшееся здоровье на врагах. Давайте объявим некоторые переменные в классе Enemy для здоровья:

1
2
3
4
private var health_txt:TextField;
private var health:int;
private var speed_x:Number;
private var speed_y:Number;

Затем мы инициализируем переменную health в конструкторе. Добавьте следующее в конструктор Enemy :

1
health = 2;

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

1
2
3
4
5
6
7
health_txt = new TextField();
health_txt.height = 20;
health_txt.textColor = 0xffffff;
health_txt.x = -5;
health_txt.y = -8;
health_txt.text = health + «»;
addChild(health_txt);

Все, что мы делаем, это создаем новое TextField , устанавливаем его цвет, позиционируем его и устанавливаем его текст на текущее значение health Наконец, мы добавляем функцию для обновления здоровья врага:

1
2
3
4
5
public function updateHealth(amount:int):int {
    health += amount;
    health_txt.text = health + «»;
    return health;
}

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


Сначала давайте немного изменим нашу функцию shoot . Замените существующую функцию shoot следующим образом:

1
2
3
4
5
6
7
8
9
private function shoot(turret:Turret, enemy:Enemy):void {
    var angle:Number = Math.atan2(enemy.y — turret.y, enemy.x — turret.x) / Math.PI * 180;
    turret.rotation = angle;
    var new_bullet:Bullet = new Bullet(angle);
    new_bullet.x = turret.x + Math.cos(turret.rotation * Math.PI / 180) * 25;
    new_bullet.y = turret.y + Math.sin(turret.rotation * Math.PI / 180) * 25;
    bullets.push(new_bullet);
    addChild(new_bullet);
}

Функция shoot теперь принимает два параметра. Первая ссылка на башню, которая будет стрелять; вторая ссылка на врага, к которому он будет стрелять.

Новый код здесь похож на тот, который присутствует в функции update класса Turret , но вместо позиции мыши мы теперь используем вражеские кординаты. Теперь вы можете удалить весь код из функции update класса Turret .

Как заставить турели стрелять по врагам? Ну, логика проста для нашей игры. Мы заставляем все башни стрелять первым врагом в массиве enemies . Какая? Давайте поместим некоторый код и затем попытаемся понять. Добавьте в конец строки for...each строку, используемую для обновления турелей:

1
2
3
4
5
6
7
for each(turret in turrets) {
    turret.update();
    for each(enemy in enemies) {
        shoot(turret, enemy);
        break;
    }
}

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

Но подождите, что это за поток пули? Похоже, они стреляют слишком быстро. Посмотрим почему.


Как мы знаем, игровой цикл запускается каждый кадр, т.е. в нашем случае 30 раз в секунду, поэтому оператор стрельбы, который мы добавили на предыдущем шаге, вызывается со скоростью нашего игрового цикла, и, следовательно, мы видим поток пуль. Похоже, нам нужен механизм синхронизации внутри турелей. Переключитесь на класс Turret и добавьте следующий код:

1
2
private var local_time:Number = 0;
private var reload_time:int;
  1. local_time : наш счетчик называется local_time в отличие от global_time в классе Main . Это по двум причинам: во-первых, потому что эта переменная является локальной для класса Turret ; во-вторых, потому что он не всегда идет вперед, как наша переменная global_time — он будет сбрасываться много раз в течение игры.
  2. reload_time : это время, необходимое башне для перезагрузки после выстрела пулями. В основном это разница во времени между двумя выстрелами пули турелью. Помните, что все единицы времени в нашей игре указаны в кадрах.

local_time переменную local_time в функции update и инициализируйте reload_time в конструкторе:

1
2
3
public function update():void {
    local_time++;
}
1
2
3
4
public function Turret() {
    reload_time = 30;
    draw();
}

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

1
2
3
4
5
6
7
public function isReady():Boolean {
    return local_time > reload_time;
}
 
public function reset():void {
    local_time = 0;
}

isReady возвращает true, только когда текущее local_time больше, чем reload_time , то есть когда турель перезагрузилась. А функция reset просто сбрасывает переменную local_time , чтобы снова запустить ее.

Теперь вернитесь в Main класс и измените код стрельбы в игровом цикле, который мы добавили на предыдущем шаге, следующим образом:

1
2
3
4
5
6
7
8
9
for each(turret in turrets) {
    turret.update();
    if (!turret.isReady()) continue;
    for each(enemy in enemies) {
        shoot(turret, enemy);
        turret.reset();
        break;
    }
}

Поэтому, если турель не готова ( isReady() возвращает false ), мы продолжаем следующую итерацию цикла турели. Вы увидите, что турели стреляют с интервалом 30 кадров или 1 секунда. Здорово!


Все еще что-то не так. Турели стреляют по врагам независимо от расстояния между ними. Чего здесь не хватает, так это диапазона башни. У каждой башни должна быть своя дальность, внутри которой можно стрелять по врагу. Добавьте еще одну переменную в класс Turret именем range и установите ее 120 внутри конструктора:

1
2
3
private var reload_time:int;
private var local_time:Number = 0;
private var range:int;
1
2
3
4
5
public function Turret() {
    reload_time = 30;
    range = 120;
    draw();
}

Также добавьте функцию canShoot в конец класса:

1
2
3
4
5
6
public function canShoot(enemy:Enemy):Boolean {
    var dx:Number = enemy.x — x;
    var dy:Number = enemy.y — y;
    if (Math.sqrt(dx * dx + dy * dy) <= range) return true;
    else return false;
}

Каждая башня может стрелять по врагу только тогда, когда он отвечает определенным критериям — например, вы можете позволить башне стрелять только по красным врагам, у которых меньше половины времени жизни и расстояние не более 30 пикселей. Вся такая логика, чтобы определить, может ли турель стрелять в противника или нет, перейдет в функцию canShoot , которая возвращает true или false соответствии с логикой.

Наша логика проста. Если враг находится в пределах досягаемости, верните true ; в противном случае верните false. Поэтому, когда расстояние между турелью и врагом ( Math.sqrt(dx * dx + dy * dy) ) меньше или равно range , возвращается значение true . Еще немного модификаций в разделе «Съемка» игрового цикла:

01
02
03
04
05
06
07
08
09
10
11
for each(turret in turrets) {
    turret.update();
    if (!turret.isReady()) continue;
    for each(enemy in enemies) {
        if (turret.canShoot(enemy)) {
            shoot(turret, enemy);
            turret.reset();
            break;
        }
    }
}

Теперь, только если враг находится в пределах досягаемости башни, она будет стрелять.


Очень важной частью каждой игры является обнаружение столкновений. В нашей игре проверка столкновений осуществляется между пулями и врагами. Мы будем добавлять код обнаружения столкновений внутри цикла for...each который обновляет маркеры в цикле игры.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for (i = bullets.length — 1; i >= 0; i—) {
    bullet = bullets[i];
    // if the bullet isn’t defined, continue with the next iteration
    if (!bullet) continue;
    bullet.update();
    if (bullet.x < 0 || bullet.x > stage.stageWidth || bullet.y < 0 || bullet.y > stage.stageHeight) {
        bullets.splice(i, 1);
        bullet.parent.removeChild(bullet);
        continue;
    }
     
    for (var k:int = enemies.length — 1; k >= 0; k—) {
        enemy = enemies[k];
        if (bullet.hitTestObject(enemy)) {
            bullets.splice(i, 1);
            bullet.parent.removeChild(bullet);
            if (enemy.updateHealth(-1) == 0) {
                enemies.splice(k, 1);
                enemy.parent.removeChild(enemy);
            }
            break;
        }
    }
}

Мы используем функцию ActionScript hitTestObject для проверки столкновения пули с врагом. Если происходит столкновение, пуля удаляется так же, как когда она покидает сцену. Здоровье врага затем обновляется с updateHealth метода updateHealth , которому передается свойство damage bullet . Если функция updateHealth возвращает целое число, меньшее или равное 0 , это означает, что враг мертв, и поэтому мы удаляем его таким же образом, как и пуля.

И наше обнаружение столкновений сделано!


Помните, что мы пересекаем врагов и пули в обратном направлении в нашем игровом цикле. Давайте поймем почему. Предположим, мы использовали восходящий цикл for . Мы находимся с индексом i=3 и удаляем пулю из массива. После удаления предмета в позиции 3 его пространство заполняется предметом, затем в позиции 4 . Так что теперь предмет, ранее находившийся в позиции 4 находится в позиции 3 После итерации i увеличивается на 1 и становится равным 4 поэтому проверяется позиция в позиции 4 .

Ой, вы видите, что случилось только сейчас? Мы просто пропустили элемент сейчас в позиции 3 которая сместилась назад в результате сращивания. И поэтому мы используем обратный цикл for который устраняет эту проблему. Вы можете понять почему.


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

1
2
3
4
5
private var range:int;
private var reload_time:int;
private var local_time:Number = 0;
private var body:Sprite;
private var range_circle:Sprite;

Затем обновите функцию draw следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
private function draw():void {
    range_circle = new Sprite();
    g = range_circle.graphics;
    g.beginFill(0x00D700);
    g.drawCircle(0, 0, range);
    g.endFill();
    range_circle.alpha = 0.2;
    range_circle.visible = false;
    addChild(range_circle);
     
    body = new Sprite();
    var g:Graphics = body.graphics;
    g.beginFill(0xD7D700);
    g.drawCircle(0, 0, 20);
    g.beginFill(0x800000);
    g.drawRect(0, -5, 25, 10);
    g.endFill();
    addChild(body);
}

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

1
2
3
4
5
6
7
private function onMouseOver(e:MouseEvent):void {
    range_circle.visible = true;
}
 
private function onMouseOut(e:MouseEvent):void {
    range_circle.visible = false;
}

Теперь присоедините слушателей к соответствующим событиям в конце конструктора:

1
2
body.addEventListener(MouseEvent.MOUSE_OVER, onMouseOver);
body.addEventListener(MouseEvent.MOUSE_OUT, onMouseOut);

Если вы запустите игру и попытаетесь развернуть турель, вы увидите мерцание при наведении указателя мыши на заполнители. Это почему?

Видите мерцание?

Помните, мы установили свойство mouseEnabled башни-призрака в false ? Мы сделали это, потому что призрачная башня захватывала события мыши, проходя между мышью и заполнителем. Снова возникла та же ситуация, поскольку у самой башни теперь двое детей — ее тело и спрайт дальности — которые фиксируют события мыши между ними.

Решение то же самое. Мы можем установить их отдельные свойства mouseEnabled в false . Но лучшее решение — установить для свойства mouseChildren в ghost turret значение false . То, что это делает, ограничивает всех детей призрачной башни от получения событий мыши. Аккуратно, а? Идите дальше и установите его в false в конструкторе Main :

1
2
3
4
5
6
ghost_turret = new Turret();
ghost_turret.alpha = 0.5;
ghost_turret.mouseEnabled = false;
ghost_turret.mouseChildren = false;
ghost_turret.visible = false;
addChild(ghost_turret);

Проблема решена.

Мы могли бы расширить эту демонстрацию, включив в нее гораздо более продвинутые функции и превратить ее в играбельную игру. Некоторые из которых могут быть:

  1. Лучшая AI логика для выбора и отстрела врагов.
  2. Различный тип турелей, пуль и врагов в игре.
  3. Сложные пути противника вместо прямых.

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