О чем этот пример
Создание игр с большим количеством активных объектов — это вызов для производительности. В этой статье мы разберем пример Phaser, где 1000 физических спрайтов одновременно двигаются и сталкиваются с границами мира. Вы узнаете, как эффективно управлять тысячами тел, настраивать анимации и реализовывать плавное управление камерой для обзора огромной игровой сцены. Этот подход полезен для создания симуляций, аркадных игр с множеством частиц или стратегий с крупными картами.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
controls;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('gems', 'assets/tests/columns/gems.png', 'assets/tests/columns/gems.json');
}
create ()
{
this.physics.world.setBounds(0, 0, 800 * 8, 600 * 8);
const spriteBounds = Phaser.Geom.Rectangle.Inflate(Phaser.Geom.Rectangle.Clone(this.physics.world.bounds), -100, -100);
this.anims.create({ key: 'diamond', frames: this.anims.generateFrameNames('gems', { prefix: 'diamond_', end: 15, zeroPad: 4 }), repeat: -1 });
this.anims.create({ key: 'prism', frames: this.anims.generateFrameNames('gems', { prefix: 'prism_', end: 6, zeroPad: 4 }), repeat: -1 });
this.anims.create({ key: 'ruby', frames: this.anims.generateFrameNames('gems', { prefix: 'ruby_', end: 6, zeroPad: 4 }), repeat: -1 });
this.anims.create({ key: 'square', frames: this.anims.generateFrameNames('gems', { prefix: 'square_', end: 14, zeroPad: 4 }), repeat: -1 });
// Create loads of random sprites
const anims = [ 'diamond', 'prism', 'ruby', 'square' ];
for (let i = 0; i < 1000; i++)
{
const pos = Phaser.Geom.Rectangle.Random(spriteBounds);
const block = this.physics.add.sprite(pos.x, pos.y, 'gems');
block.setVelocity(Phaser.Math.Between(200, 400), Phaser.Math.Between(200, 400));
block.setBounce(1).setCollideWorldBounds(true);
if (Math.random() > 0.5)
{
block.body.velocity.x *= -1;
}
else
{
block.body.velocity.y *= -1;
}
block.play(Phaser.Math.RND.pick(anims));
}
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
this.add.text(0, 0, 'Use Cursors to scroll camera.\nQ / E to zoom in and out', { font: '18px Courier', fill: '#00ff00' });
}
update (time, delta)
{
this.controls.update(delta);
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
parent: 'phaser-example',
pixelArt: true,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 100 },
debug: true
}
},
scene: Example
};
const game = new Phaser.Game(config);
Настройка мира и границ
Перед созданием объектов необходимо определить игровое пространство. В примере мир физики значительно превышает размер видимой области окна.
this.physics.world.setBounds(0, 0, 800 * 8, 600 * 8);
Метод setBounds устанавливает границы физического мира. Здесь мир в 8 раз шире и выше окна игры (800x600). Это создает большую область для перемещения объектов.
Далее мы создаем внутреннюю область для спавна спрайтов, чтобы они не появлялись у самых границ:
const spriteBounds = Phaser.Geom.Rectangle.Inflate(Phaser.Geom.Rectangle.Clone(this.physics.world.bounds), -100, -100);
Phaser.Geom.Rectangle.Clone создает копию прямоугольника границ мира. Phaser.Geom.Rectangle.Inflate "сжимает" этот прямоугольник на 100 пикселей с каждой стороны. Таким образом, spriteBounds — это область для спавна, отстоящая от краев мира.
Создание анимаций из атласа
Вместо загрузки множества отдельных изображений используется один атлас (gems) с набором кадров. Это оптимизирует загрузку ресурсов.
Анимации создаются динамически из кадров атласа:
this.anims.create({ key: 'diamond', frames: this.anims.generateFrameNames('gems', { prefix: 'diamond_', end: 15, zeroPad: 4 }), repeat: -1 });
Метод this.anims.generateFrameNames генерирует массив кадров для анимации. Он ищет в атласе gems кадры с именами, начинающимися на prefix (например, 'diamond_') и заканчивающимися номером от 0 до end. Параметр zeroPad: 4 означает, что номер кадра дополняется нулями до 4 знаков (например, 'diamond_0000', 'diamond_0001'). Параметр repeat: -1 задает бесконечное повторение анимации. Таким образом создаются четыре вида анимаций драгоценных камней.
Массовое создание физических тел
Ключевая часть примера — создание 1000 физических спрайтов в цикле.
for (let i = 0; i < 1000; i++)
{
const pos = Phaser.Geom.Rectangle.Random(spriteBounds);
const block = this.physics.add.sprite(pos.x, pos.y, 'gems');
Phaser.Geom.Rectangle.Random(spriteBounds) возвращает случайную точку внутри области spriteBounds. this.physics.add.sprite создает спрайт с физическим тел Arcade в этой позиции.
Каждому телу задаются свойства движения:
block.setVelocity(Phaser.Math.Between(200, 400), Phaser.Math.Between(200, 400));
block.setBounce(1).setCollideWorldBounds(true);
setVelocity устанавливает случайную начальную скорость по осям X и Y (от 200 до 400). setBounce(1) делает упругость максимальной (тело идеально отскакивает от границ). setCollideWorldBounds(true) включает столкновение с границами мира, заданными ранее.
Для разнообразия траекторий направление скорости случайным образом инвертируется:
if (Math.random() > 0.5)
{
block.body.velocity.x *= -1;
}
else
{
block.body.velocity.y *= -1;
}
В конце для спрайта запускается случайная анимация из списка: block.play(Phaser.Math.RND.pick(anims)).
Управление камерой с плавным движением
Так как мир огромен, нам нужен способ его осмотра. Для этого используется Phaser.Cameras.Controls.SmoothedKeyControl.
Сначала создается объект конфигурации управления:
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
В конфиге указывается управляемая камера (this.cameras.main), клавиши для движения (стрелки) и зума (Q/E). Параметры acceleration (ускорение), drag (сопротивление) и maxSpeed (максимальная скорость) отвечают за плавность, инерционность движения камеры.
Экземпляр контрола создается и сохраняется: this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);.
Для работы контрола необходимо обновлять его в каждом кадре:
update (time, delta)
{
this.controls.update(delta);
}
Метод update контрола использует delta (время, прошедшее с предыдущего кадра) для расчета плавного движения.
Конфигурация игры и физики
Важную роль играет настройка игры в объекте config.
physics: {
default: 'arcade',
arcade: {
gravity: { y: 100 },
debug: true
}
},
Здесь активируется физический движок Arcade. Устанавливается гравитация по оси Y (100), из-за чего тела в примере падают вниз. Параметр debug: true включает отладочную отрисовку физических тел (показывает хитбоксы).
Обратите внимание, что type установлен в Phaser.WEBGL. WebGL-рендерер обычно производительнее Canvas при отрисовке большого количества спрайтов.
Параметр pixelArt: true включает специальный режим фильтрации текстур для пиксельной графики, что предотвращает размытие.
Что попробовать дальше
Пример демонстрирует, что Phaser Arcade Physics эффективно справляется с тысячами простых физических тел. Ключевые приемы: использование одного атласа для анимаций, задание границ мира больше видимой области и применение инерционного управления камерой для навигации. Для экспериментов попробуйте: изменить количество тел и проверьте производительность, добавить столкновения между самими телами с помощью this.physics.add.collider, заменить отскакивающие драгоценности на частицы из системы эмиттеров или реализовать разделение мира на чанки для еще большей оптимизации.
