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

При разработке игр с прокруткой или большими уровнями вы можете столкнуться с неожиданным поведением: спрайты с физикой начинают двигаться медленнее обычных, хотя их скорость задана одинаково. Эта статья разбирает конкретный пример (баг #6293), который демонстрирует, как настройки камеры и мира могут влиять на работу Arcade Physics. Понимание этой проблемы поможет вам избежать багов с движением и правильно настраивать большие игровые пространства.

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

Живой запуск

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

Исходный код


var config = {
  type: Phaser.AUTO,
  width: 1500,
  height: 500,
  parent: 'phaser-example',
  physics: {
    default: "arcade",
    arcade: {
        fixedStep: false,
        useTree: false,
      //debug: true,
      debugShowBody: true
    }
  },
  scene: {
    preload: preload,
    create: create,
    update: update
  }
};

var game = new Phaser.Game(config);

var speed = 250;
var ship;
const W = 10000;
const H = 1000;

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

function create() {
  this.cameras.main.setBounds( 0, 0, W, H );
  bg = this.add.tileSprite( W/2, H/2, W, H, 'ship' );
  bg.alpha = 0.3;
  ship = this.physics.add.image(200, 120, 'ship');
  ship.scale = 5;
  ship.setVelocity( speed * 2, 0 );

	ship2 = this.add.sprite(200, 300, 'ship');
  ship2.scale = 5;
}

let lastX = 0;
function update( timeMS, dt ) {
  //const dX = Math.abs( ship.x - lastX );
  //console.log( `dX = ${ dX.toFixed( 2 ) }, FPS = ${ this.game.loop.actualFps.toFixed( 4 ) }` );
  //lastX = ship.x;

  const speedAccel = 2.0;
  //const dir = this.input.activePointer.x > config.width/2 ? 1 : -1;
  //ship.setVelocity( speed * speedAccel, 0 );
  if ( ship.x > config.width ) ship.x = 0;

  ship2.x += 0.25025 * speedAccel * dt;
  if ( ship2.x > config.width ) ship2.x = 0;

  //this.cameras.main.centerOn( ship.x, ship.y );
}

Суть проблемы: мир больше камеры

В примере создаётся огромный игровой мир шириной 10 000 пикселей (W = 10000), но физический движок Arcade по умолчанию оптимизирован для работы только с областью, которая находится в пределах камеры. Это сделано для производительности. Однако, когда объект с физикой (ship) выходит за границы видимой области камеры (которая по умолчанию равна размеру холста — 1500x500), движок может некорректно обрабатывать его положение и скорость.

Обычный спрайт (ship2) не зависит от физического движка, поэтому его движение рассчитывается напрямую в update и работает стабильно, даже если он далеко от камеры.

Ключевые настройки в коде

В конфигурации сцены важно обратить внимание на два момента: установку границ мира для камеры и создание объектов.

this.cameras.main.setBounds( 0, 0, W, H );

Этот метод задаёт границы, в пределах которых может двигаться камера. Но важно: он **не** изменяет границы для расчётов физического движка Arcade по умолчанию.

ship = this.physics.add.image(200, 120, 'ship');
ship2 = this.add.sprite(200, 300, 'ship');

Первый объект создаётся через фабрику физики (this.physics.add.image), второй — как обычный спрайт. Это коренное отличие в их поведении.

Как движется каждый объект

Обновление позиции объектов происходит в функции update. Разница в подходе критична.

ship.setVelocity( speed * 2, 0 );

Скорость физическому телу задаётся один раз в create. Дальше движок Arcade должен сам обновлять позицию объекта каждому кадру, но его вычисления могут давать сбой, если объект далеко от камеры.

ship2.x += 0.25025 * speedAccel * dt;
if ( ship2.x > config.width ) ship2.x = 0;

Обычный спрайт движется за счёт прямого изменения свойства .x с учётом дельты времени (dt). Это предсказуемо и не зависит от системы физики или положения камеры.

Решение: настройка физического мира

Чтобы физический движок корректно работал с огромным миром, ему тоже нужно явно указать границы. Это делается через свойство world физики.

// В функции create()
this.physics.world.setBounds(0, 0, W, H);

Установка этих границ сообщает Arcade Physics, что он должен учитывать объекты во всей этой области, а не только в зоне видимости камеры. После этой настройки физический корабль (ship) будет двигаться с постоянной ожидаемой скоростью.

Также стоит убедиться, что для отладки включена опция debugShowBody: true, чтобы видеть хитбоксы физических тел.

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

Основной вывод: когда вы работаете с миром, размеры которого превышают видимую область камеры, необходимо вручную задавать границы (setBounds) не только для камеры, но и для физического мира. Иначе Arcade Physics, оптимизированный для производительности, может проигнорировать объекты за пределами кадра, что приведёт к их неправильному движению. Для экспериментов попробуйте

  1. отключить fixedStep в настройках физики и посмотреть на разницу
  2. активно двигать камеру, следуя за кораблём с помощью centerOn, и
  3. сравнить потребление CPU с включёнными и выключенными границами мира