О чем этот пример
В играх важна предсказуемость. Если физика или логика игры привязаны к частоте кадров (FPS), то на мощном ПК объекты могут двигаться слишком быстро, а на слабом — чересчур медленно. В статье разберем пример из официальной коллекции Phaser, который демонстрирует решение этой проблемы. Вы узнаете, как использовать класс `Phaser.Core.TimeStep` для создания обновления игры с фиксированным шагом, независимого от производительности системы. Этот подход полезен для детерминированной физики, сетевых игр и точной игровой логики.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bullet', 'assets/tests/timer/bullet-bill.png');
this.load.image('cannon', 'assets/tests/timer/cannon.png');
this.load.image('ground', 'assets/tests/timer/ground.png');
}
create ()
{
this.add.image(0, 200, 'ground').setOrigin(0);
this.add.image(0, 500, 'ground').setOrigin(0);
this.bullet1 = this.add.image(0, 76, 'bullet').setOrigin(0);
this.bullet2 = this.add.image(0, 376, 'bullet').setOrigin(0);
this.speed = 0.5;
this.timestep = new Phaser.Core.TimeStep(this.game, {
forceSetTimeOut: true,
target: 30
});
// You can also optionally set these in the config above:
// deltaHistory: 0,
// smoothStep: false
this.timestep.start((time, delta) => this.steppedUpdate(time, delta));
}
update (time, delta)
{
this.bullet1.x += this.speed * delta;
if (this.bullet1.x > 800)
{
this.bullet1.x = -100;
}
}
// Fixed at 30fps, regardless of monitor speed
steppedUpdate (time, delta)
{
this.bullet2.x += this.speed * delta;
if (this.bullet2.x > 800)
{
this.bullet2.x = -100;
}
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
backgroundColor: '#9adaea',
scene: Example
};
const game = new Phaser.Game(config);
Проблема: зависимость от delta в методе update
По умолчанию в Phaser основная игровая логика выполняется в методе update(scene, delta). Параметр delta — это время в миллисекундах, прошедшее с предыдущего кадра. Умножая скорость на delta, мы делаем движение плавным и независимым от частоты кадров.
Однако, если FPS нестабилен (например, 60 на одном компьютере и 144 на другом), значение delta будет разным. Объект, движущийся со скоростью 0.5 пикселя в миллисекунду, за одну секунду (1000 мс) пройдет 500 пикселей. Но если кадров меньше, а delta больше, то за тот же реальный промежуток времени объект может пройти большее расстояние за меньшее количество вызовов update. Это может нарушить баланс или синхронизацию.
В примере первая пуля (bullet1) управляется именно так, и её движение зависит от текущего FPS.
Решение: создание отдельного таймстепа с фиксированным FPS
Для создания логики, работающей с постоянной частотой, в Phaser есть класс Phaser.Core.TimeStep. Он позволяет запустить отдельный цикл обновления, который вызывается с заданной фиксированной частотой, независимо от основного цикла рендеринга.
В методе create() сцены создается и настраивается этот таймстеп:
this.timestep = new Phaser.Core.TimeStep(this.game, {
forceSetTimeOut: true,
target: 30
});
* forceSetTimeOut: true — заставляет использовать setTimeout вместо requestAnimationFrame для вызова шагов. Это необходимо, чтобы цикл работал с фиксированной частотой, даже если браузерная вкладка неактивна.
* target: 30 — целевая частота обновления в FPS. В данном случае логика будет пытаться выполняться 30 раз в секунду.
После создания таймстеп нужно запустить, передав ему функцию обратного вызова:
this.timestep.start((time, delta) => this.steppedUpdate(time, delta));
Функция steppedUpdate теперь будет вызываться с фиксированным интервалом, стремясь к 30 FPS.
Сравнение двух подходов в действии
В примере реализованы оба подхода для наглядности:
* **Пуля 1 (bullet1)**: обновляется в основном методе update(). Её скорость зависит от реального delta между кадрами рендеринга.
* **Пуля 2 (bullet2)**: обновляется в методе steppedUpdate(). Она получает delta, рассчитанный исходя из фиксированного целевого FPS (в данном случае ~33.33 мс для 30 кадров).
Код обновления для обеих пуль идентичен, но результаты будут разными:
// В update() - зависит от FPS рендеринга
this.bullet1.x += this.speed * delta;
// В steppedUpdate() - зависит от фиксированного FPS (30)
this.bullet2.x += this.speed * delta;
Если основная частота кадров игры выше или ниже 30 FPS, первая пуля будет двигаться с переменной скоростью, а вторая — с постоянной. Это отлично видно, если искусственно замедлить или ускорить браузер в инструментах разработчика.
Настройка и тонкости работы с TimeStep
При создании TimeStep можно передать дополнительные параметры конфигурации:
{
forceSetTimeOut: true,
target: 30,
deltaHistory: 0, // Размер истории для вычисления среднего delta
smoothStep: false // Интерполяция для сглаживания
}
* **deltaHistory**: если задать значение больше 0 (например, 10), класс будет хранить историю последних delta и использовать их среднее значение. Это помогает сгладить резкие скачки производительности.
* **smoothStep**: если установить в true, система будет пытаться интерполировать состояние между фиксированными шагами для более плавного рендеринга. Это полезно, когда частота рендеринга (например, 60 FPS) выше частоты логики (30 FPS).
Важно помнить, что TimeStep создает дополнительную нагрузку, так как запускает параллельный цикл. Его следует использовать только для критически важной логики, которая требует фиксированного шага.
Что попробовать дальше
Использование Phaser.Core.TimeStep — это мощный прием для отделения игровой логики от частоты рендеринга. Он обеспечивает детерминированность и стабильность, что особенно важно для физических симуляций, пошаговых стратегий или сетевой синхронизации. Для экспериментов попробуйте: изменить target на 60 или 10 FPS и понаблюдать за пулями; привязать к фиксированному обновлению не движение, а стрельбу или спавн врагов; использовать smoothStep: true вместе с интерполяцией позиций для сверхплавной анимации.
