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

Освоив основы загрузки ресурсов и создания спрайтов, разработчик сталкивается с ключевым элементом любого платформера: интерактивным миром. В этой части мы добавим в игру физическое взаимодействие, управление персонажем, анимации и систему сбора очков. Вы научитесь создавать статические платформы, динамические группы объектов и обрабатывать столкновения между ними — фундамент для большинства 2D-игр.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    scoreText;
    score = 0;
    cursors;
    platforms;
    stars;
    player;

    preload ()
    {
        // this.load.setBaseURL('https://cdn.phaserfiles.com/v385');
        this.load.image('sky', 'src/games/firstgame/assets/sky.png');
        this.load.image('ground', 'src/games/firstgame/assets/platform.png');
        this.load.image('star', 'src/games/firstgame/assets/star.png');
        this.load.image('bomb', 'src/games/firstgame/assets/bomb.png');
        this.load.spritesheet('dude', 'src/games/firstgame/assets/dude.png', { frameWidth: 32, frameHeight: 48 });
    }

    create ()
    {
        this.add.image(400, 300, 'sky');

        this.platforms = this.physics.add.staticGroup();

        this.platforms.create(400, 568, 'ground').setScale(2).refreshBody();

        this.platforms.create(600, 400, 'ground');
        this.platforms.create(50, 250, 'ground');
        this.platforms.create(750, 220, 'ground');

        this.player = this.physics.add.sprite(100, 450, 'dude');

        this.player.setBounce(0.2);
        this.player.setCollideWorldBounds(true);

        this.anims.create({
            key: 'left',
            frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
            frameRate: 10,
            repeat: -1
        });

        this.anims.create({
            key: 'turn',
            frames: [ { key: 'dude', frame: 4 } ],
            frameRate: 20
        });

        this.anims.create({
            key: 'right',
            frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }),
            frameRate: 10,
            repeat: -1
        });

        this.cursors = this.input.keyboard.createCursorKeys();

        this.stars = this.physics.add.group({
            key: 'star',
            repeat: 11,
            setXY: { x: 12, y: 0, stepX: 70 }
        });

        this.stars.children.forEach(child =>
        {

            child.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));

        });

        this.scoreText = this.add.text(16, 16, 'score: 0', { fontSize: '32px', fill: '#000' });

        this.physics.add.collider(this.player, this.platforms);
        this.physics.add.collider(this.stars, this.platforms);

        this.physics.add.overlap(this.player, this.stars, this.collectStar, null, this);
    }

    update ()
    {
        if (this.cursors.left.isDown)
        {
            this.player.setVelocityX(-160);

            this.player.anims.play('left', true);
        }
        else if (this.cursors.right.isDown)
        {
            this.player.setVelocityX(160);

            this.player.anims.play('right', true);
        }
        else
        {
            this.player.setVelocityX(0);

            this.player.anims.play('turn');
        }

        if (this.cursors.up.isDown && this.player.body.touching.down)
        {
            this.player.setVelocityY(-330);
        }
    }

    collectStar (player, star)
    {
        star.disableBody(true, true);

        this.score += 10;
        this.scoreText.setText(`Score: ${this.score}`);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 300 },
            debug: false
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Настройка физического мира и статических платформ

Phaser Arcade Physics предоставляет простой и быстрый движок для 2D-игр. В методе create() мы настраиваем игровой мир, начиная с фона.

this.add.image(400, 300, 'sky');

Затем создаётся группа статических физических тел this.platforms. Статические тела не подвержены силам вроде гравитации и идеально подходят для земли и платформ.

this.platforms = this.physics.add.staticGroup();
this.platforms.create(400, 568, 'ground').setScale(2).refreshBody();
this.platforms.create(600, 400, 'ground');

Метод refreshBody() необходимо вызывать после изменения масштаба (setScale) или позиции статического тела, чтобы физический движок корректно пересчитал его границы.

Группы (Group) в Phaser — это мощный инструмент для управления коллекциями похожих объектов с общим поведением.

Создание и анимация игрока

Персонаж создаётся как динамическое физическое тело (physics.add.sprite), которое будет падать под действием гравитации и сталкиваться с миром.

this.player = this.physics.add.sprite(100, 450, 'dude');
this.player.setBounce(0.2);
this.player.setCollideWorldBounds(true);

setBounce(0.2) задаёт упругость, а setCollideWorldBounds(true) не позволяет спрайту вылететь за границы игрового поля.

Далее создаются три анимации на основе спрайтшита 'dude'.

this.anims.create({
    key: 'left',
    frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
    frameRate: 10,
    repeat: -1
});

Ключевые параметры: key — имя для обращения, frames — массив кадров (генерируется автоматически), frameRate — скорость проигрывания, repeat: -1 — бесконечный цикл. Анимация 'turn' использует один кадр (№4), в котором персонаж смотрит в камеру.

Управление и логика в методе Update

Логика управления обрабатывается каждый кадр в методе update(). Мы проверяем состояние клавиш-стрелок, которые были получены через this.input.keyboard.createCursorKeys().

if (this.cursors.left.isDown) {
    this.player.setVelocityX(-160);
    this.player.anims.play('left', true);
}
else if (this.cursors.right.isDown) {
    this.player.setVelocityX(160);
    this.player.anims.play('right', true);
}
else {
    this.player.setVelocityX(0);
    this.player.anims.play('turn');
}

Метод setVelocityX() напрямую задаёт горизонтальную скорость. Одновременно с этим проигрывается соответствующая анимация. Второй аргумент true в anims.play() означает «принудительно запустить», даже если эта анимация уже играет.

Прыжок реализован через проверку this.player.body.touching.down, что гарантирует прыжок только с поверхности.

if (this.cursors.up.isDown && this.player.body.touching.down) {
    this.player.setVelocityY(-330);
}

Работа с группами объектов: звёзды и их сбор

Звёзды создаются не по одной, а целой физической группой с помощью this.physics.add.group. Это эффективно как для производительности, так и для управления.

this.stars = this.physics.add.group({
    key: 'star',
    repeat: 11,
    setXY: { x: 12, y: 0, stepX: 70 }
});

Параметр repeat: 11 создаст 12 объектов (1 исходный + 11 повторений). setXY автоматически расставит их в линию с заданным шагом. Затем для каждого ребёнка в группе задаётся случайный отскок по оси Y.

this.stars.children.forEach(child => {
    child.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
});

Ключевые взаимодействия настраиваются через collider и overlap.

this.physics.add.collider(this.player, this.platforms);
this.physics.add.collider(this.stars, this.platforms);
this.physics.add.overlap(this.player, this.stars, this.collectStar, null, this);

Коллайдер (collider) предотвращает взаимное проникновение тел. Оверлап (overlap) регистрирует факт пересечения и вызывает callback-функцию collectStar, не влияя на физику.

Обработка сбора и обновление счёта

Функция collectStar вызывается движком физики при каждом пересечении игрока и звезды. В неё автоматически передаются ссылки на оба участвующих спрайта.

collectStar (player, star) {
    star.disableBody(true, true);
    this.score += 10;
    this.scoreText.setText(`Score: ${this.score}`);
}

Метод disableBody(true, true) — стандартный способ «деактивации» физического тела в Arcade Physics. Первый параметр true отключает тело, второй — скрывает родительский игровой объект (звезду).

Текст счёта создаётся в create() через систему текста Phaser.

this.scoreText = this.add.text(16, 16, 'score: 0', { fontSize: '32px', fill: '#000' });

При каждом сборе переменная score увеличивается, а текст обновляется методом setText().

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

Вы создали полноценный игровой прототип с физикой, управлением и игровой механикой. Освоили ключевые концепции Phaser: группы объектов, коллайдеры, оверлапы и анимации. Для экспериментов попробуйте: изменить параметры гравитации в конфиге, добавить звук при сборе звезды, реализовать «опасные» объекты (используя созданный ресурс 'bomb'), которые отнимают очки при столкновении, или сделать звёзды появляющимися заново через некоторое время.