О чем этот пример
Визуальные эффекты, такие как извивающиеся щупальца, змеи или механические цепи, могут оживить вашу игру. Обычно их реализация требует сложных математических расчетов. Однако в Phaser есть мощный и малоизвестный инструмент — система трансформаций объектов, которая позволяет создавать подобные связи между спрайтами буквально в несколько строк кода. Эта статья покажет, как использовать `transform.add()` для построения иерархических цепочек объектов, где движение и вращение родителя автоматически влияют на всех его потомков. Мы разберем пример, создающий динамичный рой вращающихся «пил», и поймем принципы, которые можно применить для создания щупалец, хвостов или гибкого оружия.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
var config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
scene: {
preload: preload,
create: create,
update: update
},
width: 1024,
height: 768
};
var game = new Phaser.Game(config);
function preload() {
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('saw', 'assets/sprites/saw.png');
}
var images = [];
var factor = 0;
function addIKDemo(parent, scene)
{
var lastImage = parent;
images.push(lastImage);
for (var i = 0; i < 10; ++i)
{
var newImage = scene.add.image(64, 0, 'saw');
lastImage.transform.add(newImage.transform);
lastImage = newImage;
images.push(lastImage);
}
}
function create() {
for (var i = 0; i < 100; ++i)
{
var rootImage = this.add.image(Math.random() * 1024, Math.random() * 768, 'saw');
addIKDemo(rootImage, this);
}
}
function update()
{
for (var i = 0, l = images.length; i < l; ++i)
{
images[i].transform.rotation += 0.01;
images[i].transform.scaleX = Math.min(0.2 + Math.abs(Math.sin(factor * 0.1)), 1);
images[i].transform.scaleY = images[i].transform.scaleX;
}
factor += 0.01;
}
Основная идея: иерархия трансформаций
Каждый игровой объект в Phaser (например, Image или Sprite) обладает свойством transform. Это свойство хранит информацию о положении, масштабе и повороте объекта в мире.
Ключевой метод — transform.add(childTransform). Он связывает трансформацию одного объекта с трансформацией другого. После вызова этого метода объект-child начинает использовать свою локальную систему координат (позицию, поворот) относительно родительского объекта parent.
lastImage.transform.add(newImage.transform);
В этом коде трансформация newImage становится дочерней по отношению к трансформации lastImage. Теперь любые изменения lastImage.transform (перемещение, вращение) будут автоматически применены к newImage, к которым добавятся её собственные локальные трансформации.
Создание цепочки объектов
В примере функция addIKDemo создаёт цепочку из 10 спрайтов, где каждый последующий является «ребёнком» предыдущего. Это формирует иерархическую структуру, похожую на звенья цепи.
function addIKDemo(parent, scene) {
var lastImage = parent;
images.push(lastImage);
for (var i = 0; i < 10; ++i) {
var newImage = scene.add.image(64, 0, 'saw');
lastImage.transform.add(newImage.transform);
lastImage = newImage;
images.push(lastImage);
}
}
Обратите внимание на параметры при создании newImage: (64, 0, 'saw'). Координаты (64, 0) задаются в локальном пространстве относительно родителя (lastImage). Это означает, что каждое новое звено цепи будет появляться на 64 пикселя правее своего предка, формируя горизонтальную линию в её собственном локальном виде.
Инициализация множества цепочек
В функции create создаётся 100 независимых цепочек, каждая со своим случайным корневым положением на экране.
function create() {
for (var i = 0; i < 100; ++i) {
var rootImage = this.add.image(Math.random() * 1024, Math.random() * 768, 'saw');
addIKDemo(rootImage, this);
}
}
Здесь rootImage — это первое, корневое звено каждой цепочки. Его позиция задаётся случайным образом в пределах сцены. Затем эта картинка передаётся в addIKDemo как родительский объект, к которому будут «привязываться» остальные 10 звеньев. Глобальный массив images хранит ссылки на все созданные спрайты для последующей анимации.
Анимация всей системы
Цикл update выполняет две основные операции для каждого спрайта в массиве images: вращение и пульсацию масштаба.
function update() {
for (var i = 0, l = images.length; i < l; ++i) {
images[i].transform.rotation += 0.01;
images[i].transform.scaleX = Math.min(0.2 + Math.abs(Math.sin(factor * 0.1)), 1);
images[i].transform.scaleY = images[i].transform.scaleX;
}
factor += 0.01;
}
1. **Вращение (rotation += 0.01)**: Каждое звено плавно вращается вокруг своей собственной точки привязки (origin). Благодаря иерархии трансформаций, вращение родителя заставляет всё его «потомство» вращаться вместе с ним, что создаёт сложное волнообразное движение всей цепочки.
2. **Пульсация масштаба**: Масштаб по оси X (scaleX) вычисляется на основе синусоиды от глобальной переменной factor. Math.abs(sin(...)) создаёт эффект «дыхания» или пульсации от 0 до 1. Math.min(..., 1) ограничивает максимальное значение, а 0.2 + задаёт минимальный размер. Значение scaleY просто копируется из scaleX, сохраняя пропорции спрайта.
Что попробовать дальше
Система трансформаций Phaser предоставляет элегантный способ создания сложных связанных движений без необходимости вручную пересчитывать мировые координаты для каждого объекта. Связывая объекты через transform.add(), вы делегируете движку задачу распространения трансформаций по иерархии.
**Идеи для экспериментов:**
1. Измените локальные координаты при создании звеньев (например, scene.add.image(0, 64, 'saw')) чтобы строить вертикальные или диагональные цепочки.
2. Добавьте интерактивность: заставьте корневой объект (rootImage) следовать за курсором мыши. Вся цепочка будет изящно тянуться за ним.
3. Используйте разные спрайты для разных звеньев цепи или меняйте их свойства (например, tint) в зависимости от позиции в цепочке (`i`).
4. Поэкспериментируйте с формулой для scaleX и scaleY, чтобы создать более хаотичное или упорядоченное движение.
