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

В реальных играх идеальная производительность — редкость. Сборка мусора, сетевые запросы, тяжёлые вычисления могут вызывать кратковременные "фризы". Этот пример наглядно показывает, как система твинов Phaser 3 справляется с такими лагами, сохраняя плавность анимации и целостность временной логики. Понимание этого механизма критически важно для создания отзывчивого геймплея, не зависящего от мелких просадок FPS.

Версия 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('block', 'assets/sprites/steelbox.png');
    }

    create ()
    {
        const b1 = this.add.image(100, 300, 'block');
        const b2 = this.add.image(500, 300, 'block').setTint(0xff0000);

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

            this.tweens.add({
                targets: b1,
                x: 500-64,
                duration: 2000,
                ease: 'Linear',
            });

            this.tweens.add({
                targets: b1,
                x: 700-64,
                duration: 1000,
                ease: 'Linear',
                delay: 2000
            });

            this.tweens.add({
                targets: b2,
                x: 700,
                duration: 1000,
                ease: 'Linear',
                delay: 2000
            });

            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);

Суть примера: искусственный лаг и два спрайта

В сцене создаются два стальных блока (b1 и b2). По клику мыши для них запускается последовательность твинов, а через секунду искусственно вызывается задержка в полторы секунды с помощью функции blockCpuFor. Этот лаг имитирует тяжёлую операцию, которая "подвешивает" основной поток браузера.

Цель — увидеть, как система анимации Phaser ведёт себя при таком сценарии: будут ли твины "потеряны", выполнятся ли они мгновенно после лага или продолжат работу с учётом потерянного времени.

Механика твинов и понятие задержки (delay)

Ключевой элемент примера — свойство delay в конфигурации твина. Оно задаёт время в миллисекундах, которое должно пройти _после старта твина_ до начала непосредственной анимации.

Посмотрим на цепочку для красного блока b2:

this.tweens.add({
    targets: b2,
    x: 700,
    duration: 1000,
    ease: 'Linear',
    delay: 2000
});

Этот твин стартует сразу по клику, но 2000 мс (2 секунды) он будет ждать, и только потом за 1000 мс (1 секунду) переместит блок в позицию x: 700. Важно: отсчёт задержки начинается в момент создания твина, а не после окончания предыдущих.

Что происходит во время лага? Анализ выполнения

Искусственный лаг длиной 1500 мс запускается через 1000 мс после клика (setTimeout с задержкой 1000). Давайте смоделируем временную шкалу: 1. **T+0 мс:** Клик. Запускаются все твины. Начинается отсчёт их задержек. 2. **T+1000 мс:** Срабатывает setTimeout, вызывается blockCpuFor(1500). Основной поток блокируется на 1500 мс. 3. **T+2500 мс (1000 + 1500):** Поток разблокируется. За это время "реальное" время сдвинулось на 2500 мс, но игровой цикл Phaser не работал.

В этот момент (T+2500) для системы твинов ситуация выглядит так: с момента их старта прошло 2500 мс. Проверим состояние: - Твин b1 (первый, без задержки): Он должен был завершиться за 2000 мс (к T+2000). Система видит, что его время вышло, и мгновенно применяет конечное значение (x: 500-64). - Твин b1 (второй, с delay: 2000): Его задержка в 2000 мс только что истекла (T+2500 >= T+2000). Он _немедленно начинает_ свою анимацию длиной 1000 мс. - Твин b2delay: 2000): Аналогично, его задержка истекла. Он тоже начинает свою 1000-миллисекундную анимацию.

Таким образом, после лага оба блока начинают двигаться одновременно, но к разным целям.

Почему это работает? Внутренние часы твинов

Система твинов Phaser (this.tweens) не зависит от частоты кадров. Она привязана к внутренним игровым часам, которые отслеживают прошедшее время. Когда игровой цикл приостанавливается (лаг), накопленное время не сбрасывается. При возобновлении работы система проверяет: "Сколько времени прошло с момента старта каждого твина?"

На основе этого расчёта твин переходит в нужное состояние: - Если его общая длительность (delay + duration) уже прошла — он завершается. - Если задержка прошла, но длительность ещё нет — он начинает анимацию с той позиции, на которой должен был быть в этот момент. - Если задержка ещё не прошла — он продолжает её отсчёт.

Это делает твины устойчивыми к кратковременным простоям. Однако важно помнить: длительный лаг (например, на 10 секунд) может привести к тому, что все твины завершатся мгновенно при возврате, что может выглядеть как резкий скачок.

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

Твины в Phaser 3 используют время, а не кадры, что делает их надёжным инструментом для анимаций, устойчивым к небольшим лагам. Они гарантируют, что логическая временная цепочка событий будет выполнена, даже если рендеринг на время прервётся. **Идеи для экспериментов:** 1. Увеличьте время в blockCpuFor до 4000 мс. Оба блока мгновенно окажутся в конечных точках, так как все твины успеют завершиться "виртуально" за время лага. 2. Попробуйте добавить твину свойство paused: true при создании, а затем запустить его вручную через this.tweens.resume(tween) уже после лага. Это даст полный контроль над временем старта. 3. Поиграйте с настройками fps.limit и fps.smoothStep в конфиге игры, чтобы увидеть, как они влияют на восприятие анимации после задержки.