О чем этот пример
Создание плавных и отзывчивых анимаций — ключевая задача для игрового 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 привязаны к игровому времени, а не к реальному. Это позволяет им корректно "наверстывать" упущенное при кратковременных лагах, но при длительных блокировках это приводит к визуальным рывкам. Для экспериментов попробуйте
- Увеличить время блокировки до 3-5 секунд
- Заменить
ease: 'Linear'на'Power2'и посмотреть, как нелинейная интерполяция ведет себя после паузы - Раскомментировать
limit: 30в конфиге и сравнить ощущения от анимации до и во время блокировки. Главный вывод: для сохранения плавности критически важно выносить тяжелые синхронные задачи из основного потока (например, с помощью Web Workers) или разбивать их на мелкие части, выполняемые в течение нескольких кадров
