← На главную
Серия руководств

Шаг 7. Стрельба

Создание Tower Defense с PIXI • Шаг 7 из 12

7.1. Создание снаряда

Башни стреляют по врагам снарядами, представленными спрайтом fire.png. Создадим класс Bullet, создающий снаряд для заданной башни:


import { EventEmitter } from "events";
import { App } from '../system/App';

export class Bullet extends EventEmitter {
    constructor(tower, enemy) {
        super();
        this.tower = tower;
        this.enemy = enemy;

        this.sprite = App.sprite("fire");
        this.sprite.anchor.set(0.5, 1);
        this.sprite.x = this.tower.sprite.x;
        this.sprite.y = this.tower.sprite.y;

        this.sprite.angle = this.tower.sprite.angle;
    }
}

Унаследуем его от EventEmitter, чтобы отправлять события снаряда. Например, позже нам понадобится знать момент его уничтожения — при попадании во врага или выходе за экран — по соответствующему событию.

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

Создадим спрайт снаряда и поместим его в координаты башни с тем же углом поворота.

За выстрел отвечает shoot класса Tower. В нём нужно создать экземпляр Bullet:

export class Tower extends Tile {
    constructor(config) {
        // ...
        this.bullets = [];
        this.active = true;
    }
    // ...
    shoot(enemy) {
        const bullet = new Bullet(this, enemy);
        this.bullets.push(bullet);
        this.sprite.parent.addChild(bullet.sprite);
    }
}

Метод также принимает объект врага, по которому стреляет башня. Все созданные снаряды будем сохранять в this.bullets, чтобы затем отслеживать их и проверять столкновения с врагами.

Изменим attack в Tower, добавив вызов shoot при активной башне:

    attack(enemy) {
        this.rotateToEnemy(enemy);

        if (this.active) {
            this.active = false;
            this.shoot(enemy);
            window.setTimeout(() => this.active = true, this.config.cooldown);
        }
    }

Флаг active показывает, может ли башня стрелять сейчас. Tower.attack вызывается в Game.update, то есть постоянно, на каждом кадре. Но мы хотим выпускать снаряды только с частотой, заданной для башни.

Поэтому для перезарядки нужно отключать активность башни сразу после выстрела и включать после тайм-аута. Запустим тайм-аут с временем перезарядки из конфигурации, затем снова активируем башню флагом this.active = true;

7.2. Полёт снаряда

Чтобы снаряд двигался, на каждом кадре нужно менять координаты его спрайта на заданное смещение. Определим смещение по x и y с учётом угла поворота спрайта. Сделаем это в Bullet.init:

export class Bullet extends EventEmitter {
    constructor(tower, enemy) {
        // ...
        this.init();
    }

    init() {
        const speed = this.tower.config.bullet.speed;
        const azimuth = (this.sprite.angle) * (Math.PI / 180) - Math.PI / 2;
        this.velocity = {x: Math.cos(azimuth) * speed, y: Math.sin(azimuth) * speed};
        App.app.ticker.add(this.update, this);
    }

    update() {
        this.sprite.x += this.velocity.x;
        this.sprite.y += this.velocity.y;
    }
}

Берём скорость снаряда из заданной ранее конфигурации башни. Затем определяем азимут по углу поворота спрайта после создания. Зная азимут и скорость, вычисляем смещение по x и y и записываем его в this.velocity.

Создаём callback update, в котором прибавляем полученное смещение к координатам спрайта снаряда.

Добавим функцию в PIXI.Ticker, чтобы она выполнялась на каждом кадре игры.

7.3. Уничтожение снаряда

Вышедший за экран снаряд нужно уничтожить по двум причинам:

  1. неиспользуемый объект не должен занимать память
  2. ненужные проверки не должны выполняться, а update снаряда не должен продолжать бесполезные вычисления

Сделаем это сразу, чтобы избежать возможных проблем с производительностью игры.

Проверим координаты снаряда. Если он вышел за экран, уничтожим спрайт и удалим update из ticker:

    update() {
        this.sprite.x += this.velocity.x;
        this.sprite.y += this.velocity.y;

        let position = this.sprite.getGlobalPosition()

        if (position.x < 0 || position.x > App.app.view.width) {
            this.remove();
        }
    }

    remove() {
        App.app.ticker.remove(this.update, this);
        this.sprite.destroy();
        this.sprite = null;
        this.emit("removed");
    }

Достаточно проверить координату x спрайта. Для получения глобальной позиции используем метод getGlobalPosition класса PIXI.Sprite. Левая граница экрана имеет x = 0. Координату правой границы получим из ширины canvas через App.app.view.width. Если x снаряда меньше 0 или больше правой границы, уничтожим его спрайт, удалим callback из ticker и отправим соответствующее событие.

Теперь можно отслеживать событие уничтожения снаряда. Изменим Tower.shoot, удаляя уничтоженный снаряд из пула башни:

shoot(enemy) {
    // ...
    bullet.once("removed", () => this.bullets = this.bullets.filter(item => item !== bullet));
}