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

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

Версия 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 * 4, 600 * 4);

        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 < 100; 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 * 4, 600 * 4);

Здесь границы мира устанавливаются в четыре раза больше размеров окна игры (800x600). Это создает обширную область для движения объектов. Далее, чтобы спрайты не появлялись слишком близко к краям этой области, мы создаем уменьшенную копию границ.

const spriteBounds = Phaser.Geom.Rectangle.Inflate(Phaser.Geom.Rectangle.Clone(this.physics.world.bounds), -100, -100);

Метод Phaser.Geom.Rectangle.Clone создает копию прямоугольника границ, а Inflate "сжимает" его со всех сторон на 100 пикселей. Полученный spriteBounds будет использоваться как область для случайного появления объектов.

Создание анимаций и управляемой камеры

Перед созданием спрайтов необходимо определить их анимации. В примере используется атлас gems, из которого генерируются кадры для четырех типов анимаций.

this.anims.create({ key: 'diamond', frames: this.anims.generateFrameNames('gems', { prefix: 'diamond_', end: 15, zeroPad: 4 }), repeat: -1 });

Ключевые параметры: key (уникальное имя анимации), frames (массив кадров, сгенерированных по именам из атласа) и repeat: -1 для зацикливания.

Для навигации по большому миру реализовано плавное управление камерой с помощью 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.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

Обновление состояния управления происходит в методе update. Это позволяет камере плавно ускоряться и замедляться.

Массовое создание физических спрайтов

Сердце примера — цикл, который создает 100 физических спрайтов со случайными параметрами.

for (let i = 0; i < 100; 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);
    // ... разворот скорости и воспроизведение анимации
}

1. Phaser.Geom.Rectangle.Random(spriteBounds) возвращает случайную точку внутри заданной области появления. 2. this.physics.add.sprite создает спрайт, которому сразу же добавляется физическое тело Arcade. 3. setVelocity задает случайную начальную скорость по осям X и Y. 4. setBounce(1) делает отскок абсолютно упругим (коэффициент восстановления 1). setCollideWorldBounds(true) включает столкновение с границами мира, что заставляет спрайты отскакивать от краев. 5. Последующий условный блок случайным образом инвертирует одну из компонент скорости (block.body.velocity.x или `y`), добавляя разнообразия в начальное движение. 6. block.play(Phaser.Math.RND.pick(anims)) запускает случайную анимацию из подготовленного массива.

Конфигурация игры и физики

Ключевые настройки физической системы и рендеринга задаются в объекте конфигурации игры.

const config = {
    type: Phaser.WEBGL,
    pixelArt: true,
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 100 },
            debug: true
        }
    },
    scene: Example
};

- pixelArt: true включает сглаживание текстур для пиксель-арта. - В разделе physics активируется система arcade. Здесь задана гравитация по оси Y и включен debug-режим, который отрисовывает контуры физических тел и векторы скорости, что невероятно полезно при отладке. Обратите внимание, что в данном примере гравитация действительно влияет на тела, но эффект отскока (bounce: 1) и постоянная скорость делают её влияние не столь очевидным на первый взгляд.

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

Этот пример служит отличной основой для создания динамичных сцен с множеством взаимодействующих объектов, таких как поле астероидов, стая птиц или коллекция падающих предметов. Для экспериментов попробуйте: изменить количество спрайтов и их физические параметры (массу, трение); добавить столкновения между самими спрайтами с помощью this.physics.add.collider(); реализовать взаимодействие с игроком (например, клик мышью для удаления или отталкивания объекта); или отключить гравитацию, чтобы увидеть чисто инерционное движение.