О чем этот пример
При разработке игр с физикой часто возникает ситуация, когда объекты ведут себя не так, как ожидалось: один спрайт не двигает другой, хотя коллайдер добавлен. Эта статья разбирает тонкости настройки взаимодействия тел в Arcade Physics через свойства `pushable` и `immovable`. Мы наглядно исследуем 8 тестовых сценариев, чтобы понять, как и когда объекты могут толкать друг друга, а когда это взаимодействие блокируется.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
info;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.setPath('assets/sprites');
this.load.image('blockAP');
this.load.image('blockBP');
this.load.image('blockANP');
this.load.image('blockBNP');
this.load.image('blockANM');
this.load.image('blockBNM');
}
create ()
{
let colliderSet = false;
const setVelocity = (body, v) =>
{
if (colliderSet)
{
body.setVelocityY(v);
}
else
{
this.info.setColor('#ff0000');
}
};
this.physics.world.setBounds(0, 0, 800 + 256, 600);
// Test 1
const test1A = this.physics.add.image(80, 500, 'blockAP').setCollideWorldBounds().setInteractive();
const test1B = this.physics.add.image(80, 150, 'blockBP').setCollideWorldBounds().setInteractive();
test1A.setBounce(0.5);
test1B.setBounce(0.5);
test1A.setCircle(32);
// test1B.setCircle(32);
test1A.on('pointerdown', () =>
{
setVelocity(test1A, -200);
});
test1B.on('pointerdown', () =>
{
setVelocity(test1B, 200);
});
// Test 2
const test2A = this.physics.add.image(208, 500, 'blockANP').setCollideWorldBounds().setInteractive();
const test2B = this.physics.add.image(208, 150, 'blockBNP').setCollideWorldBounds().setInteractive();
test2A.setBounce(0.5);
test2B.setBounce(0.5);
test2A.setPushable(false);
test2B.setPushable(false);
test2A.setCircle(32);
// test2B.setCircle(32);
test2A.on('pointerdown', () =>
{
setVelocity(test2A, -200);
});
test2B.on('pointerdown', () =>
{
setVelocity(test2B, 200);
});
// Test 3
const test3A = this.physics.add.image(336, 500, 'blockAP').setCollideWorldBounds().setInteractive();
const test3B = this.physics.add.image(336, 150, 'blockBNP').setCollideWorldBounds().setInteractive();
test3A.setBounce(0.5);
test3B.setBounce(0.5);
test3B.setPushable(false);
test3A.setCircle(32);
// test3B.setCircle(32);
test3A.on('pointerdown', () =>
{
setVelocity(test3A, -200);
});
test3B.on('pointerdown', () =>
{
setVelocity(test3B, 200);
});
// Test 4
const test4A = this.physics.add.image(464, 500, 'blockANP').setCollideWorldBounds().setInteractive();
const test4B = this.physics.add.image(464, 150, 'blockBP').setCollideWorldBounds().setInteractive();
test4A.setBounce(0.5);
test4B.setBounce(0.5);
test4A.setPushable(false);
test4A.setCircle(32);
// test4B.setCircle(32);
test4A.on('pointerdown', () =>
{
setVelocity(test4A, -200);
});
test4B.on('pointerdown', () =>
{
setVelocity(test4B, 200);
});
// Test 5
const test5A = this.physics.add.image(592, 500, 'blockAP').setCollideWorldBounds().setInteractive();
const test5B = this.physics.add.image(592, 150, 'blockBNM').setCollideWorldBounds().setInteractive();
test5A.setBounce(0.5);
test5B.setBounce(0.5);
test5B.setImmovable(true);
test5A.setCircle(32);
// test5B.setCircle(32);
test5A.on('pointerdown', () =>
{
setVelocity(test5A, -200);
});
test5B.on('pointerdown', () =>
{
setVelocity(test5B, 200);
});
// Test 6
const test6A = this.physics.add.image(720, 500, 'blockANP').setCollideWorldBounds().setInteractive();
const test6B = this.physics.add.image(720, 150, 'blockBNM').setCollideWorldBounds().setInteractive();
test6A.setBounce(0.5);
test6B.setBounce(0.5);
test6A.setPushable(false);
test6B.setImmovable(true);
test6A.setCircle(32);
// test6B.setCircle(32);
test6A.on('pointerdown', () =>
{
setVelocity(test6A, -200);
});
test6B.on('pointerdown', () =>
{
setVelocity(test6B, 200);
});
// Test 7
const test7A = this.physics.add.image(848, 500, 'blockANM').setCollideWorldBounds().setInteractive();
const test7B = this.physics.add.image(848, 150, 'blockBP').setCollideWorldBounds().setInteractive();
test7A.setBounce(0.5);
test7B.setBounce(0.5);
test7A.setImmovable(true);
test7B.setPushable(true);
test7A.setCircle(32);
// test7B.setCircle(32);
test7A.on('pointerdown', () =>
{
setVelocity(test7A, -200);
});
test7B.on('pointerdown', () =>
{
setVelocity(test7B, 200);
});
// Test 8
const test8A = this.physics.add.image(976, 500, 'blockANM').setCollideWorldBounds().setInteractive();
const test8B = this.physics.add.image(976, 150, 'blockBNP').setCollideWorldBounds().setInteractive();
test8A.setBounce(0.5);
test8B.setBounce(0.5);
test8A.setImmovable(true);
test8B.setPushable(false);
test8A.setCircle(32);
// test8B.setCircle(32);
test8A.on('pointerdown', () =>
{
setVelocity(test8A, -200);
});
test8B.on('pointerdown', () =>
{
setVelocity(test8B, 200);
});
// Runner
this.info = this.add.text(16, 16, 'Click the collider first:');
const atob = this.add.text(280, 16, 'A to B').setInteractive();
const btoa = this.add.text(400, 16, 'B to A').setInteractive();
atob.once('pointerdown', () =>
{
atob.setColor('#ffff00');
btoa.setAlpha(0.2);
colliderSet = true;
this.physics.add.collider(test1A, test1B);
this.physics.add.collider(test2A, test2B);
this.physics.add.collider(test3A, test3B);
this.physics.add.collider(test4A, test4B);
this.physics.add.collider(test5A, test5B);
this.physics.add.collider(test6A, test6B);
this.physics.add.collider(test7A, test7B);
this.physics.add.collider(test8A, test8B);
});
btoa.once('pointerdown', () =>
{
btoa.setColor('#ffff00');
atob.setAlpha(0.2);
colliderSet = true;
this.physics.add.collider(test1B, test1A);
this.physics.add.collider(test2B, test2A);
this.physics.add.collider(test3B, test3A);
this.physics.add.collider(test4B, test4A);
this.physics.add.collider(test5B, test5A);
this.physics.add.collider(test6B, test6A);
this.physics.add.collider(test7B, test7A);
this.physics.add.collider(test8B, test8A);
});
}
}
const config = {
type: Phaser.AUTO,
width: 800 + 256,
height: 600,
parent: 'phaser-example',
backgroundColor: '#2d2d2d',
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0 },
debug: true
}
},
scene: Example
};
const game = new Phaser.Game(config);
Суть проблемы и настройка сцены
Исходный код представляет собой демонстрацию для отладки (debug) взаимодействия физических тел. Основной вопрос: в каком порядке и с какими настройками нужно добавлять коллайдер, чтобы объекты могли толкаться?
Сцена загружает несколько спрайтов с разными именами (например, 'blockAP', 'blockBNP'), что намекает на их свойства: 'P' — pushable (толкаемый), 'NP' — non-pushable (нетолкаемый), 'NM' — non-movable (неподвижный).
Ключевая переменная colliderSet изначально равна false. Это блокирует применение скорости к объектам до тех пор, пока пользователь не выберет направление коллайдера (A к B или B к A), нажав на соответствующий текст. Функция setVelocity проверяет этот флаг.
let colliderSet = false;
const setVelocity = (body, v) =>
{
if (colliderSet)
{
body.setVelocityY(v);
}
else
{
this.info.setColor('#ff0000');
}
};
Также важно, что мир физики расширен за пределы видимой области (setBounds), чтобы объекты не вылетали, а у всех объектов включен отскок от границ мира (setCollideWorldBounds) и интерактивность (setInteractive).
Ключевые свойства: pushable и immovable
В Arcade Physics два основных свойства управляют реакцией тела на столкновения:
1. setPushable(value) — определяет, может ли данное тело быть сдвинуто другим телом при столкновении. Принимает true или false. Значение по умолчанию — true.
2. setImmovable(value) — если установлено в true, тело становится неподвижным. При столкновении с ним двигаться будет всегда другое тело (если оно не тоже immovable). Значение по умолчанию — false.
Важное уточнение: свойство pushable влияет только на то, может ли это *конкретное* тело быть оттолкнутым. Оно не влияет на его способность толкать другие объекты.
// Объект, который нельзя сдвинуть другими объектами
spriteA.setPushable(false);
// Абсолютно неподвижный объект (как стена)
spriteB.setImmovable(true);
Анализ тестов: когда толчок возможен
В сцене создается 8 пар объектов (A и B). Они отличаются настройками pushable и immovable. Объект A всегда находится внизу, B — вверху. По клику им задается вертикальная скорость навстречу друг другу.
Логика тестов раскрывается при выборе направления коллайдера. При выборе 'A to B' коллайдер создается в порядке (test1A, test1B). Это означает, что тело A рассматривается как 'активное' или 'первое' в паре для некоторых внутренних проверок движка.
Рассмотрим пару тестов 1 и 2:
// Test 1: Оба объекта толкаемые (pushable = true по умолчанию)
const test1A = this.physics.add.image(80, 500, 'blockAP').setPushable(true);
const test1B = this.physics.add.image(80, 150, 'blockBP').setPushable(true);
// Test 2: Оба объекта НЕ толкаемые (pushable = false)
const test2A = this.physics.add.image(208, 500, 'blockANP').setPushable(false);
const test2B = this.physics.add.image(208, 150, 'blockBNP').setPushable(false);
В тесте 1 объекты успешно столкнутся и оттолкнутся. В тесте 2 — нет. Почему? Если оба тела в паре имеют pushable: false, они не могут сдвинуть *друг друга*. Это своего рода тупиковая ситуация.
Роль порядка аргументов в коллайдере
Вот где становится важен порядок объектов при создании коллайдера. После настройки всех пар, пользователь выбирает, в каком порядке их связать: collider(A, B) или collider(B, A).
// Вариант 1: Коллайдер A -> B
this.physics.add.collider(test1A, test1B);
// Вариант 2: Коллайдер B -> A
this.physics.add.collider(test1B, test1A);
Этот порядок влияет на внутреннюю логику разрешения столкновений, особенно когда у тел разные флаги pushable. Движок, по сути, проверяет: может ли *первый* объект в вызове коллайдера сдвинуть *второй*? Если у второго pushable: false, толчка не произойдет.
Таким образом, в тестах 3-8, где у объектов A и B разные комбинации pushable/immovable, результат столкновения будет разным в зависимости от выбранного пользователем направления ('A to B' или 'B to A'). Это наглядно показывает асимметричность свойства pushable.
Immovable как абсолютный приоритет
Свойство immovable сильнее, чем pushable. Если тело помечено как immovable(true), оно не будет двигаться при столкновении ни при каких обстоятельствах, независимо от порядка в коллайдере и настройки pushable. Двигаться будет всегда другой объект.
Посмотрите на тесты 5-8, где один из объектов имеет setImmovable(true):
// Test 5: A — толкаемый, B — неподвижный (immovable)
const test5B = this.physics.add.image(592, 150, 'blockBNM').setImmovable(true);
// Test 7: A — неподвижный (immovable), B — толкаемый
const test7A = this.physics.add.image(848, 500, 'blockANM').setImmovable(true);
В этих случаях объект с immovable: true ведет себя как стена или платформа. Он никогда не сдвинется от удара, а весь импульс передастся второму телу (которое отскочит). Порядок аргументов в коллайдере здесь уже не имеет значения.
Что попробовать дальше
Взаимодействие pushable и immovable в Phaser Arcade Physics — мощный, но требующий понимания инструмент. Запомните: pushable контролирует, может ли объект быть сдвинут, а порядок объектов в collider() влияет на направление проверки этого свойства. Immovable же — это абсолютный запрет на движение.
Для экспериментов: попробуйте изменить исходный код, убрав комментарий со строк test1B.setCircle(32) и др. Посмотрите, как изменение формы тела (с квадрата на круг) влияет на физику столкновений. Также создайте сцену, где несколько цепочек объектов с разными свойствами должны толкать друг друга по принципу домино, чтобы на практике освоить эти настройки.
