О чем этот пример
В реальных играх идеальная производительность — редкость. Сборка мусора, сетевые запросы, тяжёлые вычисления могут вызывать кратковременные "фризы". Этот пример наглядно показывает, как система твинов 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 мс.
- Твин b2 (с delay: 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 в конфиге игры, чтобы увидеть, как они влияют на восприятие анимации после задержки.
