О чем этот пример
В процессе разработки игры часто возникает необходимость в создании динамических сцен, где множество объектов движется по заданным законам и взаимодействует друг с другом. Рассмотренный пример наглядно демонстрирует, как использовать физический движок Arcade в Phaser 3 для решения такой задачи. Мы разберем, как заставить группу объектов двигаться из кругового формации к цели, используя встроенные методы для расчета траектории и обработки столкновений. Этот паттерн полезен для создания атак врагов, систем частиц или любых других механик, требующих управляемого хаоса.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('ball', 'assets/sprites/balls.png', { frameWidth: 17, frameHeight: 17 });
this.load.image('rect', 'assets/sprites/128x128.png');
}
create ()
{
this.cameras.main.centerOn(0, 0);
const rect = this.physics.add.staticImage(0, 0, 'rect');
const balls = this.physics.add.group({
bounceX: 1,
bounceY: 1
});
balls.createMultiple({
frame: [ 0, 1, 2, 3 ],
key: 'ball',
quantity: 6
});
Phaser.Actions.PlaceOnCircle(balls.getChildren(), { x: 0, y: 0, radius: 200 });
for (const ball of balls.getChildren())
{
ball.setCircle(8);
this.physics.moveToObject(ball, rect.body.center, 100);
}
this.physics.add.collider(rect, balls);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
physics: {
default: 'arcade',
arcade: { debug: false }
},
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и загрузка ресурсов
Вся логика примера содержится в классе сцены Example. Метод preload() отвечает за загрузку графических ресурсов, необходимых для визуализации.
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('ball', 'assets/sprites/balls.png', { frameWidth: 17, frameHeight: 17 });
this.load.image('rect', 'assets/sprites/128x128.png');
}
Здесь setBaseURL задает базовый адрес для загрузки, что удобно при использовании удаленных ресурсов. Загружается спрайтшит ball, состоящий из нескольких кадров с мячами, и одиночное изображение rect, которое будет служить статичной мишенью. Важно указать корректный frameWidth и frameHeight для спрайтшита.
Создание мира: статичная цель и группа объектов
В методе create() происходит инициализация физических тел. Камера центрируется на точке (0,0), которая станет центром нашей сцены.
const rect = this.physics.add.staticImage(0, 0, 'rect');
Создается статичное физическое тело rect. Статичные тела не подвержены силам физики (гравитации, импульсам от столкновений), но с ними могут сталкиваться динамические тела.
const balls = this.physics.add.group({
bounceX: 1,
bounceY: 1
});
balls.createMultiple({
frame: [ 0, 1, 2, 3 ],
key: 'ball',
quantity: 6
});
Далее создается физическая группа balls. Параметры bounceX и bounceY задают упругость (коэффициент восстановления) для всех членов группы — значение 1 означает абсолютно упругие столкновения (потерь скорости нет). Метод createMultiple наполняет группу: создается 6 спрайтов (quantity: 6), которые поочередно берут кадры [0, 1, 2, 3] из загруженного спрайтшита ball.
Расстановка по окружности и настройка коллайдеров
Следующий шаг — красиво расположить созданные мячи.
Phaser.Actions.PlaceOnCircle(balls.getChildren(), { x: 0, y: 0, radius: 200 });
Утилита Phaser.Actions.PlaceOnCircle принимает массив объектов и параметры окружности (центр и радиус), равномерно распределяя их по этой окружности.
for (const ball of balls.getChildren())
{
ball.setCircle(8);
this.physics.moveToObject(ball, rect.body.center, 100);
}
this.physics.add.collider(rect, balls);
В цикле для каждого мяча выполняются две ключевые операции. Во-первых, ball.setCircle(8) задает физическую форму тела в виде круга радиусом 8 пикселей, центрированного относительно спрайта. Это критически важно для корректного расчета столкновений, так как по умолчанию тело имеет прямоугольную форму (хитбокс), соответствующую размеру текстуры. Во-вторых, this.physics.moveToObject(ball, rect.body.center, 100) заставляет мяч двигаться в сторону центра статичного прямоугольника (rect.body.center) со скоростью 100 пикселей в секунду. Наконец, this.physics.add.collider(rect, balls) регистрирует коллайдер, который будет обрабатывать столкновения между целью rect и любым мячом из группы balls. Благодаря заданной ранее упругости, мячи будут отскакивать от цели и друг от друга.
Конфигурация игры и запуск
Код инициализации игры вынесен за пределы класса сцены.
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
physics: {
default: 'arcade',
arcade: { debug: false }
},
scene: Example
};
const game = new Phaser.Game(config);
В конфигурационном объекте config определяется тип рендерера, размеры холста, родительский HTML-элемент и, что самое важное, настройки физики. Здесь активируется движок arcade. Параметр debug отключен, но его включение (true) позволило бы видеть контуры физических тел, что незаменимо при отладке. Экземпляр игры создается с этой конфигурацией, что автоматически запускает жизненный цикл сцены Example.
Что попробовать дальше
Пример является отличной основой для множества игровых механик. Экспериментируйте: измените radius в PlaceOnCircle для другого начального построения, задайте мячам случайную скорость вместо фиксированной 100, добавьте гравитацию в настройках arcade физики или используйте setVelocity() для придания мячам импульса в случайном направлении. Попробуйте заменить статичную цель на динамическое тело и наблюдайте за цепной реакцией столкновений. Понимание этих принципов открывает путь к созданию сложного и правдоподобного игрового мира.
