О чем этот пример

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

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


var config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#010101',
    parent: 'phaser-example',
    scene: {
        preload: preload,
        create: create
    }
};

var game = new Phaser.Game(config);

function preload ()
{
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
}

function create ()
{
    var container1 = this.add.container(400, 300);
    var container2 = this.add.container(0, 0);

    container1.setScale(0.5);

    var sprite = this.add.image(0, 0, 'eye').setInteractive();

    container1.add(container2);
    container2.add(sprite);

    this.input.setDraggable(sprite);

    sprite.on('drag', (pointer, dragX, dragY) => {

        sprite.x = dragX;
        sprite.y = dragY;

    });
}

Подготовка сцены и загрузка ресурсов

В начале кода задается стандартная конфигурация игры Phaser. Ключевой момент здесь — объявление функций preload и create непосредственно в описании сцены. Это классический подход для простых примеров.

var config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#010101',
    parent: 'phaser-example',
    scene: {
        preload: preload,
        create: create
    }
};

var game = new Phaser.Game(config);

Функция preload отвечает за загрузку изображения. Обратите внимание на использование this.load.setBaseURL. Этот метод устанавливает базовый URL, к которому будет добавлен путь из this.load.image. Такой подход удобен, если все ресурсы хранятся в одном месте.

function preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
}

Создание иерархии контейнеров и спрайта

Вся логика примера сосредоточена в функции create. Первым делом создаются два контейнера и один спрайт. Контейнеры (Phaser.GameObjects.Container) — это мощный инструмент для группировки игровых объектов.

var container1 = this.add.container(400, 300);
var container2 = this.add.container(0, 0);

Контейнер container1 размещается в центре экрана (координаты 400, 300). Контейнер container2 создается в точке (0, 0) относительно своего будущего родителя. Далее для container1 устанавливается масштаб 0.5 с помощью метода setScale. Это значит, что все его дочерние объекты будут отрисовываться вполовину меньше.

container1.setScale(0.5);

Затем создается спрайт с загруженным изображением 'eye' и делается интерактивным с помощью setInteractive(). Это обязательное условие для работы перетаскивания.

var sprite = this.add.image(0, 0, 'eye').setInteractive();

Далее выстраивается иерархия: container2 добавляется в container1, а sprite — в container2. Таким образом, спрайт является «внуком» для container1.

container1.add(container2);
container2.add(sprite);

Настройка перетаскивания и обработка событий

Чтобы объект можно было перетаскивать, его нужно пометить как драггабельный с помощью this.input.setDraggable(). Система ввода Phaser теперь будет отслеживать события мыши/касания для этого спрайта.

this.input.setDraggable(sprite);

Самая важная часть — обработчик события drag. Это событие генерируется непрерывно при перемещении мыши или пальца с зажатым спрайтом.

sprite.on('drag', (pointer, dragX, dragY) => {
    sprite.x = dragX;
    sprite.y = dragY;
});

Внутри обработчика координаты спрайта напрямую присваиваются параметрам dragX и dragY. Важно понимать, что система ввода Phaser передает координаты dragX/dragY уже **в системе координат родительского контейнера спрайта**. Поскольку спрайт находится внутри container2, а тот — внутри масштабированного container1, эти координаты автоматически учитывают преобразования (позицию и масштаб) всех родителей в цепочке. В данном случае dragX/dragY будут в координатах container2. Это приводит к визуальному эффекту: при масштабе контейнера 0.5, чтобы переместить спрайт на экране на 10 пикселей, мышь нужно переместить на 20 пикселей, так как движение спрайта в локальных координатах компенсирует масштаб родителя.

Почему возникает "запаздывание" курсора?

Эффект, при котором спрайт движется медленнее курсора, — это не баг, а прямое следствие архитектуры. Рассмотрим цепочку преобразований: 1. Спрайт имеет локальные координаты (sprite.x, sprite.y) внутри container2. 2. container2 имеет свои координаты внутри container1. 3. container1 имеет глобальные координаты (400, 300) и масштаб 0.5.

Когда вы перемещаете спрайт, система ввода вычисляет точку, в которую вы целитесь, **в глобальных координатах сцены**. Затем она преобразует эти глобальные координаты обратно в локальную систему координат родителя спрайта (container2), учитывая всю цепочку преобразований, включая масштаб container1. Поскольку container1 уменьшает всё в 2 раза, одна и та же дистанция на экране требует вдвое большего изменения локальных координат спрайта. Поэтому кажется, что спрайт «отстает» от мыши. Если бы масштаб container1 был 2.0, спрайт, наоборот, «обгонял» бы курсор.

Что попробовать дальше

Масштабирование контейнера-родителя напрямую влияет на воспринимаемую скорость перетаскивания его дочерних элементов. Это происходит из-за автоматического преобразования координат системой ввода Phaser. Для экспериментов попробуйте: изменить масштаб container1 на 2.0; добавить вращение контейнеру с помощью setRotation и посмотреть, как это повлияет на перетаскивание; или обрабатывать координаты не в локальной системе спрайта, а перемещать сам контейнер container2, чтобы увидеть разницу в поведении.