О чем этот пример
Создание плавных и интерактивных анимаций — ключевая часть игрового процесса. Часто нужно не просто запустить анимацию, но и синхронизировать с ней другие элементы игры, например, начать движение фона. Событие `ANIMATION_START` в Phaser позволяет точно отследить момент старта анимации на спрайте и активировать связанную логику. В этой статье мы разберем практический пример, где клик мыши запускает анимацию бега, а фон начинает прокручиваться именно с этого момента.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
constructor ()
{
super();
this.isRunning = false;
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('knight', 'assets/animations/knight.png', 'assets/animations/knight.json');
this.load.image('bg', 'assets/skies/clouds.png');
this.load.spritesheet('tiles', 'assets/tilemaps/tiles/fantasy-tiles.png', { frameWidth: 64, frameHeight: 64 });
}
create ()
{
// The background and floor
this.bg = this.add.tileSprite(0, 16, 800, 600, 'bg').setOrigin(0);
this.ground = this.add.tileSprite(0, 536, 800, 64, 'tiles', 1).setOrigin(0);
this.add.text(400, 8, 'Click to start running animation', { color: '#ffffff' }).setOrigin(0.5, 0);
// Our animations
const idleConfig = {
key: 'idle',
frames: this.anims.generateFrameNames('knight', { prefix: 'idle/frame', start: 0, end: 5, zeroPad: 4 }),
frameRate: 14,
repeat: -1
};
this.anims.create(idleConfig);
const runConfig = {
key: 'run',
frames: this.anims.generateFrameNames('knight', { prefix: 'run/frame', start: 0, end: 7, zeroPad: 4 }),
frameRate: 12,
repeat: -1
};
this.anims.create(runConfig);
const lancelot = this.add.sprite(400, 536, 'knight');
lancelot.setOrigin(0.5, 1);
lancelot.setScale(8);
lancelot.play('idle');
// Event handler for when the animation completes on our sprite
lancelot.on(Phaser.Animations.Events.ANIMATION_START, function () {
this.isRunning = true;
}, this);
// And a click handler to stop the animation
this.input.once('pointerdown', function () {
lancelot.play('run');
});
}
update ()
{
if (this.isRunning)
{
this.bg.tilePositionX += 4;
this.ground.tilePositionX += 8;
}
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
backgroundColor: '#026bc6',
pixelArt: true,
scene: Example
};
const game = new Phaser.Game(config);
Подготовка анимаций: idle и run
В методе create() создаются две анимации для рыцаря: покой (idle) и бег (run). Каждая анимация конфигурируется через объект с ключевыми параметрами: уникальный key, кадры, частота смены кадров (frameRate) и режим повтора (repeat). Кадры генерируются из атласа knight с помощью метода this.anims.generateFrameNames, который формирует имена кадров по заданному шаблону.
const idleConfig = {
key: 'idle',
frames: this.anims.generateFrameNames('knight', { prefix: 'idle/frame', start: 0, end: 5, zeroPad: 4 }),
frameRate: 14,
repeat: -1
};
this.anims.create(idleConfig);
После создания конфигураций, они регистрируются в менеджере анимаций сцены через this.anims.create. Спрайту lancelot изначально проигрывается анимация idle с помощью метода play.
Обработка события старта анимации
Чтобы отреагировать на начало проигрывания конкретной анимации, используется событие Phaser.Animations.Events.ANIMATION_START. Это событие генерируется на самом спрайте, когда его анимация начинает воспроизводиться. В примере на это событие подписываются с помощью метода on, передавая функцию-обработчик.
lancelot.on(Phaser.Animations.Events.ANIMATION_START, function () {
this.isRunning = true;
}, this);
Обработчик устанавливает флаг this.isRunning в true. Важно отметить третий аргумент this в вызове on — он задает контекст выполнения обработчика, чтобы внутри функции this ссылался на экземпляр сцены, а не на спрайт. Это позволяет изменять свойства сцены, такие как isRunning.
Запуск анимации по клику и обновление мира
Анимация бега запускается по первому клику мыши. Для этого используется обработчик события pointerdown на системe ввода сцены (this.input.once). Метод once гарантирует, что обработчик сработает только один раз.
this.input.once('pointerdown', function () {
lancelot.play('run');
});
В методе update() происходит проверка флага isRunning. Если анимация бега началась (флаг true), то начинается движение фона (this.bg) и земли (this.ground) через увеличение их свойства tilePositionX. Это создает иллюзию перемещения рыцаря вправо.
if (this.isRunning)
{
this.bg.tilePositionX += 4;
this.ground.tilePositionX += 8;
}
Таким образом, движение фона жестко привязано к моменту старта анимации, а не к клику напрямую, что обеспечивает точную синхронизацию визуала.
Почему именно ANIMATION_START, а не ANIMATION_UPDATE или ANIMATION_COMPLETE?
В Phaser есть несколько событий, связанных с анимацией. Выбор ANIMATION_START в данном случае обусловлен задачей: нам нужно запустить фоновые процессы ровно один раз в момент начала бега. Событие ANIMATION_UPDATE срабатывало бы на каждом кадре анимации, что привело бы к избыточным вычислениям. ANIMATION_COMPLETE здесь не подходит, так как анимация бега зациклена (repeat: -1) и не завершится сама. Использование ANIMATION_START — это идиоматичный способ реагировать на переход спрайта в новое анимационное состояние.
Что попробовать дальше
Событие ANIMATION_START — мощный инструмент для синхронизации игровой логики с визуальными изменениями. В рассмотренном примере оно позволило связать начало движения фона со стартом анимации бега, создав целостный игровой момент. Для экспериментов попробуйте
- Добавить звук шагов, который также запускается по событию старта анимации
- Использовать
ANIMATION_COMPLETEдля одноразовых анимаций (например, атаки) и обработки их окончания - Комбинировать несколько событий для управления сложными state-машинами персонажа
