2D-физика
Физика на Box2D как аспекты на Node2D. Четыре части, зеркалящие
3D-семейство:
Shape2D— чистая геометрия (аксессорnode.shape). Сама по себе ничего не делает; две ниже превращают её в фикстуру.Physics2D— твёрдое тело (node.physics): static, kinematic или dynamic.Trigger2D— статическая сенсорная зона (node.trigger): эмититenter/exit, ничего не блокирует.CharacterController2D— помощник для платформеров (node.controller) поверх динамического тела.
Обзор
Physics2D.configure({ gravity: [0, -980] }) // один раз, ДО создания тел
const ground = new Node2D()
.aspect(Shape2D, { segment: { from: [-500, 0], to: [500, 0] } })
.aspect(Physics2D, { motion: 'static' })
const hero = new Sprite({ texture: tex, anchor: [0.5, 1] })
.aspect(Shape2D, { capsule: { from: [0, 6], to: [0, 26], radius: 6 } })
.aspect(Physics2D, { motion: 'dynamic', fixedRotation: true })
hero.addEventListener('enter', other => console.log('touched', other.id))
hero.physics.applyImpulse([0, 400]) // прыжок
scene.add(ground, hero)Гейтинг хоста
Physics2D.supported // статик: поставляет ли эта сборка 2D-физику?На сборке без поддержки физики всё семейство молча инертно: тела получают id 0, каждый вызов —
no-op, события не срабатывают — и пикинг узлов указателем (ниже) никогда не попадает. Оберни
зависящий от физики геймплей в Physics2D.supported.
Мир: Physics2D.configure
Physics2D.configure(config?: {
gravity?: Vec2Like // мировые единицы/с², Y вверх (вниз = отрицательный Y); по умолчанию [0, -980]
pixelsPerMeter?: number // подстраивает внутренние допуски Box2D; ты всё равно задаёшь в мировых единицах; по умолчанию 64
subSteps?: number // под-шаги солвера на фиксированный шаг; по умолчанию 4
})Создаёт — или сбрасывает — мир физики. Вызывай один раз, до создания любых тел; повторный вызов сбрасывает мир, осиротив существующие тела.
Симуляция шагает с фиксированными 60 Гц. Отрисованные трансформы интерполируются между шагами:
Physics2D.interpolation = false // статик, глобально; по умолчанию trueВыключи интерполяцию, чтобы сэкономить покадровые записи трансформа при множестве движущихся тел (тогда они движутся дискретными шагами по 60 Гц).
Shape2D — геометрия
node.aspect(Shape2D, {
box?: [hw, hh] // ПОЛУ-экстенты, мировые единицы
circle?: number // радиус
capsule?: { from: Vec2Like, to: Vec2Like, radius: number } // между двумя локальными точками
segment?: { from: Vec2Like, to: Vec2Like } // тонкая грань — статичная земля/стены/склоны
polygon?: Vec2Like[] // выпуклый, до 8 локальных точек (оболочка вычисляется)
offset?: Vec2Like // локальное смещение от начала узла (box/circle)
})Задай ровно один из box / circle / capsule / segment / polygon. Без единого форма
выводит бокс из size спрайта узла (полуширина × полувысота; запасной 16×16 для голого Node2D).
boxпринимает полу-экстенты:box: [12, 20]— это бокс 24×40.- Точки
capsule,segmentиpolygon— в локальном пространстве узла и игнорируютoffset. segment— только для статичной твёрдой геометрии — у него нет площади (нет плотности) и он никогда не сенсор; не используй его сTrigger2Dили на динамических телах.
формы никогда не читают scale отрисовки узла — все экстенты это сырые мировые единицы.
Масштабирование спрайта 2× не масштабирует его коллайдер.
Physics2D — твёрдые тела
node.aspect(Physics2D, {
motion?: 'static' | 'kinematic' | 'dynamic' // по умолчанию 'dynamic'
density?: number // по умолчанию 1
friction?: number // по умолчанию 0.3
restitution?: number // упругость; по умолчанию 0
fixedRotation?: boolean // блокировать вращение (персонажи платформеров); по умолчанию false
bullet?: boolean // непрерывная коллизия для быстрых объектов; по умолчанию false
gravityScale?: number // множитель гравитации на тело; по умолчанию 1
})Требует Shape2D на том же узле — прикрепи её первой (иначе Physics2D бросает).
node.physics.velocity = [vx, vy] // линейная скорость, мировые единицы/с (геттер: свежий Vec2)
node.physics.angularVelocity = 90 // градусы/с (только запись)
node.physics.gravityScale = 0 // 1 = полная гравитация, 0 = парит, <0 = отталкивается
node.physics.linearDamping = 0.5 // только запись
node.physics.enabled = false // только запись: приостановить это тело
node.physics.applyImpulse([x, y]): this // мгновенный импульс; будит тело
node.physics.applyForce([x, y]): this // непрерывная сила; будит тело
node.physics.moveTo(p, deg = 0): this // телепорт / привести кинематическое тело к мировой позе
node.physics.id // нативный id тела (0 = отсоединено / нет поддержки физики)Физика владеет трансформом
Для dynamic- или kinematic-тела нативный шаг физики пишет трансформ узла. Не задавай
node.position каждый кадр — управляй динамическими телами через velocity / applyImpulse,
кинематическими — через moveTo. Чтение node.position / worldPosition всегда корректно; узел
пересинхронизируется с нативным слоем при чтении. См. Node2D.
держи тела на корневых узлах. Телу под движущимся родителем шаг физики пишет трансформ в мировых терминах — движение родителя и физика будут бороться.
Trigger2D — сенсорные зоны
const goal = new Node2D()
.aspect(Shape2D, { box: [32, 64] })
.aspect(Trigger2D) // без опций
goal.addEventListener('enter', other => win(other))
goal.trigger.moveTo(p: Vec2Like): this // переместить зону в мировую точку
goal.trigger.id // нативный id тела (0 = нет поддержки физики)Триггер — это статическое сенсорное тело, построенное из Shape2D узла: перекрывающиеся тела
эмитят события enter/exit узла вместо столкновения — оно никогда не блокирует движение. Перемещай
его через trigger.moveTo (статическое тело не следует за поздними перемещениями узла).
События enter / exit
Начало/конец контакта (твёрдое против твёрдого) и начало/конец перекрытия сенсора (триггер) оба
приходят как события enter/exit узла, доставляемые обоим узлам с другим узлом как аргументом:
hero.addEventListener('enter', other => {}) // коснулся стены или вошёл в триггер
hero.addEventListener('exit', other => {})CharacterController2D — движение платформера
node.aspect(CharacterController2D, {
speed?: number // горизонтальная скорость движения, мировые единицы/с; по умолчанию 200
jumpSpeed?: number // скорость отрыва прыжка, мировые единицы/с; по умолчанию 500
groundProbe?: number // доп. дистанция луча под ногами для проверки «на земле»; по умолчанию 6
footOffset?: number // расстояние от начала узла до ног; по умолчанию = половина высоты спрайта
})
node.controller.move(dir: number) // горизонтальное намерение в [-1, 1]; ЛИПКОЕ — задай 0, чтобы остановиться
node.controller.jump() // ставится в очередь; потребляется в следующем кадре, если на земле
node.controller.grounded // true, пока стоит на чём-то (обновляется каждый кадр)Построен на управлении скоростью: каждый кадр он задаёт горизонтальную скорость тела равной
dir * speed и отдаёт вертикаль Box2D (гравитация, прыжок, приземление, отклик стен/пола).
grounded берётся из короткого луча вниз от ног.
Если у узла их нет, прикрепление контроллера авто-добавляет Shape2D (авто-бокс из спрайта) и
Physics2D с { motion: 'dynamic', fixedRotation: true }.
setLoop(() => {
hero.controller.move((Input.key('ArrowRight') ? 1 : 0) - (Input.key('ArrowLeft') ? 1 : 0))
if (Input.key('Space')) hero.controller.jump()
})Запросы (статические)
Physics2D.raycast(from: Vec2Like, to: Vec2Like): RayHit | null
// RayHit = { node: Node2D | null, point: Vec2, normal: Vec2, fraction: number /* 0..1 вдоль луча */ }
Physics2D.overlapPoint(p: Vec2Like): Node2D | nullraycast возвращает ближайшее тело, задетое отрезком; overlapPoint — самое верхнее тело —
твёрдое или сенсор — чья форма содержит мировую точку.
Пикинг указателем
Узел с Shape2D плюс Physics2D или Trigger2D кликабелен указателем: тапы хит-тестятся через
Physics2D.overlapPoint, поэтому узел получает события click / touchstart. См.
События указателя и жесты. Для пикинга без тел используй
scene.pick() (границы спрайта).
Подводные камни
// ✗ полные экстенты — box принимает ПОЛУ-экстенты
hero.aspect(Shape2D, { box: [24, 40] }) // это бокс 48×80
// ✓ половина размера спрайта
hero.aspect(Shape2D, { box: [12, 20] })
// ✗ конфигурация после создания тел — сбрасывает мир
ground.aspect(Shape2D, {}).aspect(Physics2D, { motion: 'static' })
Physics2D.configure({ gravity: [0, -500] })
// ✓ сначала конфигурируй, потом создавай тела
// ✗ двигать динамическое тело записью трансформа
setLoop(() => { hero.x += 2 })
// ✓ управляй телом
hero.physics.velocity = [120, hero.physics.velocity.y]Смотрите также
- Node2D — слушатели
enter/exit, владение трансформом. - События указателя и жесты —
click/touchstartна кликабельных узлах. - Scene2D —
pick()без тел, события указателя на уровне сцены. - Sprite — масштаб отрисовки против размера коллайдера.