3.1. Спрайт врага
Создадим одного врага в случайном месте карты. Тайлы врагов входят в общий атлас tilemap.png; у каждого есть свой номер кадра. Добавим эти данные в общую конфигурацию игры:
export const Config = {
// ...
enemies: {
"unit1": {
"id": 246,
"coins": 20,
"velocity": 75,
"hp": 1
},
"unit2": {
"id": 247,
"coins": 30,
"velocity": 100,
"hp": 2
},
"unit3": {
"id": 248,
"coins": 40,
"velocity": 125,
"hp": 3
},
"unit4": {
"id": 249,
"coins": 50,
"velocity": 150,
"hp": 4
}
}
};
Так каждый тип врага получает собственную конфигурацию, отличающую его от остальных. Зададим следующие параметры:
- id кадра в tilemap
- скорость движения
- здоровье
- награда за убийство
Для вывода одного тайла — кадра атласа — у нас уже есть класс Tile. Изображение врага тоже является тайлом, поэтому создадим Enemy, наследующийся от Tile. Нужно передать правильный индекс кадра в конструктор базового класса. Теперь эти номера можно взять из конфигурации:
import { Tile } from "./Tile";
import { App } from "../system/App";
export class Enemy extends Tile {
constructor(config, path) {
super(config.id);
this.config = config;
this.sprite.anchor.set(0.5);
}
}
В конструктор Enemy передаём конфигурацию врага. По id получаем нужный тайл. Сохраняем конфигурацию в this.config, чтобы затем читать особые параметры врага.
Создадим врага в случайном месте карты в классе Game:
// ...
import { Enemy } from './Enemy';
export class GameScene extends Scene {
create() {
this.createMap();
this.createEnemies();
}
createEnemies() {
const enemy = new Enemy(App.config.enemies.unit1);
this.container.addChild(enemy.sprite);
enemy.sprite.x = 130;
enemy.sprite.y = 530;
}
// ...
}
3.2. Движение врага
Реализуем движение созданного врага. Он должен идти по дороге карты уровня. При создании карты в Tiled мы задали на дороге специальные точки направления. Враги проходят их последовательно до последней. Эти метки находятся на отдельном слое path. В нём нет поля data, поскольку на слое нет тайлов. Вместо него поле objects содержит точки с координатами карты. Сохраним эти данные в this.path класса LevelMap:
//...
export class LevelMap {
constructor() {
// ...
this.path = {};
}
render() {
//...
if (layerData.data) {
//...
} else if (layerData.objects) {
this.path = layerData.objects;
}
}
}
При создании врага нужно сообщить ему точки маршрута. Передадим поле path объекта карты объекту врага:
// ...
export class GameScene extends Scene {
//...
createEnemies() {
const enemy = new Enemy(App.config.enemies.unit1, this.map.path);
// ...
enemy.move();
}
}
Здесь мы сразу вызвали move, который должен запустить анимацию движения врага по карте. Теперь реализуем его в классе Enemy.
- Сохраним объект маршрута
pathв полеthis.pathклассаEnemy. - Добавим номер текущей точки: поле
pathIndexпокажет, какую точку враг прошёл последней.
//...
export class Enemy extends Tile {
constructor(config, path) {
super(config.id);
this.config = config;
this.sprite.anchor.set(0.5);
this.pathIndex = 1;
this.path = path;
}
//...
}
Метод getNextPoint вернёт текущую цель врага по номеру последней пройденной точки. Зная её номер, враг получает следующую точку для анимации движения:
//...
getNextPoint() {
const nextPoint = this.pathIndex + 1;
return this.path.find(point => point.name === nextPoint.toString());
}
//...
Теперь полностью реализуем move, запускающий анимацию перемещения между точками. Для этого нужен такой алгоритм:
- Получить следующую точку движения
- Увеличить
pathIndex, обозначающий очередную пройденную точку - Получить текущие координаты спрайта врага
- Получить координаты цели движения
- Вычислить расстояние между целью и текущей позицией врага
- Вычислить длительность движения по расстоянию и скорости из конфигурации юнита
- Запустить анимацию движения через
gsap - После завершения анимации снова вызвать
moveдля движения к следующей точке
Реализуем этот алгоритм:
move() {
// get the next point
const point = this.getNextPoint();
if (!point) {
// if there is no such thing, then there is nowhere to move
return;
}
//increase the index of the last point passed
++this.pathIndex;
// current coordinates
const sourceX = this.sprite.x;
const sourceY = this.sprite.y;
// target coordinates
const targetX = point.x / 2;
const targetY = point.y / 2;
// distance between target and current position
const diffX = Math.abs(targetX - sourceX);
const diffY = Math.abs(targetY - sourceY);
const diff = Math.max(diffX, diffY);
// duration of movement
const duration = diff / this.config.velocity;
// movement animation
gsap.to(this.sprite, {
onComplete: () => {
// repeat the process for the next point
this.move();
},
pixi: { x: point.x / 2, y: point.y / 2 },
duration,
ease: "none",
});
}
}
3.3. Установка угла
Враг уже проходит всю карту от точки к точке, но всегда смотрит в одну сторону. Исправим это: пусть он поворачивается к следующей точке маршрута.
Создадим getAngle, возвращающий угол поворота спрайта по координатам целевой точки. Угол можно получить функцией Math.atan2:
Math.atan2 Статический метод Math.atan2(y, x) возвращает угол на плоскости в радианах между положительной полуосью x и лучом из (0, 0) в точку (x, y).
Поскольку метод возвращает угол в радианах, переведём его в градусы:
// Enemy.js
getAngle(target) {
const sourceX = this.sprite.x;
const sourceY = this.sprite.y;
const dy = target.y - sourceY;
const dx = target.x - sourceX;
return 180 * Math.atan2(dy, dx) / Math.PI;
}
Теперь можно использовать этот угол в методе move перед запуском анимации движения:
// Enemy.js
move() {
//...
this.sprite.angle = this.getAngle({x: targetX, y: targetY});
}