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

Создание плавных и отзывчивых анимаций — ключевая задача для игрового UX. Этот пример наглядно демонстрирует, как искусственно созданная нагрузка на основной поток (блокировка CPU) влияет на работу системы твинов Phaser и общую частоту кадров. Понимание этой взаимосвязи поможет вам диагностировать и предотвращать "подтормаживания" в своих проектах, особенно при работе с большим количеством одновременных анимаций.

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

Живой запуск

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

Исходный код


let shouldRun = true;

function blockCpuFor(ms) {
	var now = new Date().getTime();
    console.log('start blocking');
    var result = 0;
	while(shouldRun) {
		result += Math.random() * Math.random();
		if (new Date().getTime() > now +ms)
        {
            console.log('end blocking');
			return;
        }
	}
}

class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('chunk', 'assets/sprites/chunk.png');
    }

    create ()
    {
        const targets = [];

        let x = 0;
        let y = 0;

        for (let i = 0; i < 2560; i++)
        {
            targets.unshift(this.add.image(x, y, 'chunk').setOrigin(0, 0));

            x += 5;

            if (x >= 800)
            {
                x = 0;
                y += 5;
            }
        }

        this.input.once('pointerdown', () => {

            this.tweens.add({
                targets,
                y: '+=500',
                duration: 2000,
                ease: 'Linear',
                delay: this.tweens.stagger(1)
            });

            setTimeout(() => {
                blockCpuFor(1500)
            }, 1000);

        });
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example,
    fps: {
        // smoothStep: false
        // limit: 30
    }
};

const game = new Phaser.Game(config);

Суть эксперимента: нагрузка и анимации

Пример создает 2560 спрайтов, выстроенных в сетку, и запускает их одновременное движение вниз с небольшой задержкой между каждым (stagger). Ключевой момент — через секунду после начала анимации искусственно вызывается блокировка основного потока на 1.5 секунды с помощью функции blockCpuFor. Это имитирует тяжелые синхронные вычисления (например, сложную генерацию, парсинг большого JSON) в вашей игре.

Цель — увидеть, как система твинов Phaser ведет себя при резком падении FPS: продолжится ли анимация с правильной скоростью, "наверстает" ли она отставание или зависнет.

Разбор кода: создание сцены и твинов

В методе create создается массив targets, куда помещаются все спрайты. Обратите внимание на использование unshift — это добавляет каждый новый спрайт в начало массива, что в сочетании с stagger задает порядок анимации.

Запуск анимации привязан к событию pointerdown. Твин перемещает все цели (targets) на 500 пикселей вниз за 2 секунды с линейной интерполяцией.

this.tweens.add({
    targets,
    y: '+=500',
    duration: 2000,
    ease: 'Linear',
    delay: this.tweens.stagger(1)
});

Важная деталь: delay: this.tweens.stagger(1) устанавливает задержку в 1 миллисекунду между началом анимации для каждого последующего спрайта в массиве. Это создает "волновой" эффект.

Искусственная блокировка CPU

Функция blockCpuFor — это простая, но эффективная симуляция тяжелой задачи. Она входит в цикл while и выполняет бессмысленные вычисления до тех пор, пока не истечет переданное количество миллисекунд.

function blockCpuFor(ms) {
    var now = new Date().getTime();
    console.log('start blocking');
    var result = 0;
    while(shouldRun) {
        result += Math.random() * Math.random();
        if (new Date().getTime() > now +ms)
        {
            console.log('end blocking');
            return;
        }
    }
}

Вызов этой функции запланирован через setTimeout на 1000 мс после клика, то есть когда анимация уже идет полным ходом.

setTimeout(() => {
    blockCpuFor(1500)
}, 1000);

В консоли вы увидите сообщения 'start blocking' и 'end blocking', которые отмечают период простоя.

Наблюдаемый эффект и поведение твинов

Во время блокировки рендеринг и обновление игрового мира (включая расчет твинов) приостанавливаются, так как JavaScript работает в одном потоке. Однако, когда основной поток освобождается, происходит интересное: твины Phaser пытаются "догнать" упущенное время.

Система твинов учитывает прошедшее время с момента последнего кадра. После снятия блокировки она видит, что с момента последнего апдейта прошло, например, 1500 мс, и вычисляет, где должны находиться все анимируемые объекты *в этот текущий момент*, согласно их заданной длительности (duration) и функции плавности (ease). В результате спрайты не продолжают плавно двигаться с места остановки, а "перескакивают" на позиции, соответствующие моменту снятия блокировки. Это может выглядеть как резкий рывок.

Конфигурация FPS и её влияние

В объекте конфигурации игры есть закомментированный раздел fps. По умолчанию Phaser использует внутренний временной цикл, который старается достичь целевого FPS (обычно 60).

const config = {
    // ...
    fps: {
        // smoothStep: false
        // limit: 30
    }
};

* limit: 30 — принудительно ограничивает максимальный FPS. Это может снизить нагрузку на CPU/GPU, но сделает анимации менее плавными. * smoothStep: false — отключает алгоритм сглаживания (smoothStep) в игровом цикле. Этот алгоритм помогает компенсировать небольшие колебания времени между кадрами, делая движение более плавным. В контексте нашего примера его отключение может сделать реакцию на блокировку еще более резкой.

Вы можете раскомментировать эти настройки и запустить пример с блокировкой снова, чтобы увидеть, как меняется поведение.

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

Анимации в Phaser привязаны к игровому времени, а не к реальному. Это позволяет им корректно "наверстывать" упущенное при кратковременных лагах, но при длительных блокировках это приводит к визуальным рывкам. Для экспериментов попробуйте

  1. Увеличить время блокировки до 3-5 секунд
  2. Заменить ease: 'Linear' на 'Power2' и посмотреть, как нелинейная интерполяция ведет себя после паузы
  3. Раскомментировать limit: 30 в конфиге и сравнить ощущения от анимации до и во время блокировки. Главный вывод: для сохранения плавности критически важно выносить тяжелые синхронные задачи из основного потока (например, с помощью Web Workers) или разбивать их на мелкие части, выполняемые в течение нескольких кадров