LeCodesdocs

2D-физика

Физика на Box2D как аспекты на Node2D. Четыре части, зеркалящие 3D-семейство:

  • Shape2D — чистая геометрия (аксессор node.shape). Сама по себе ничего не делает; две ниже превращают её в фикстуру.
  • Physics2D — твёрдое тело (node.physics): static, kinematic или dynamic.
  • Trigger2D — статическая сенсорная зона (node.trigger): эмитит enter/exit, ничего не блокирует.
  • CharacterController2D — помощник для платформеров (node.controller) поверх динамического тела.

Обзор

TypeScript
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)

Гейтинг хоста

TypeScript
Physics2D.supported    // статик: поставляет ли эта сборка 2D-физику?

На сборке без поддержки физики всё семейство молча инертно: тела получают id 0, каждый вызов — no-op, события не срабатывают — и пикинг узлов указателем (ниже) никогда не попадает. Оберни зависящий от физики геймплей в Physics2D.supported.

Мир: Physics2D.configure

TypeScript
Physics2D.configure(config?: {
  gravity?: Vec2Like        // мировые единицы/с², Y вверх (вниз = отрицательный Y); по умолчанию [0, -980]
  pixelsPerMeter?: number   // подстраивает внутренние допуски Box2D; ты всё равно задаёшь в мировых единицах; по умолчанию 64
  subSteps?: number         // под-шаги солвера на фиксированный шаг; по умолчанию 4
})

Создаёт — или сбрасывает — мир физики. Вызывай один раз, до создания любых тел; повторный вызов сбрасывает мир, осиротив существующие тела.

Симуляция шагает с фиксированными 60 Гц. Отрисованные трансформы интерполируются между шагами:

TypeScript
Physics2D.interpolation = false   // статик, глобально; по умолчанию true

Выключи интерполяцию, чтобы сэкономить покадровые записи трансформа при множестве движущихся тел (тогда они движутся дискретными шагами по 60 Гц).

Shape2D — геометрия

TypeScript
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 или на динамических телах.
Note

формы никогда не читают scale отрисовки узла — все экстенты это сырые мировые единицы. Масштабирование спрайта 2× не масштабирует его коллайдер.

Physics2D — твёрдые тела

TypeScript
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 бросает).

TypeScript
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.

Note

держи тела на корневых узлах. Телу под движущимся родителем шаг физики пишет трансформ в мировых терминах — движение родителя и физика будут бороться.

Trigger2D — сенсорные зоны

TypeScript
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 узла, доставляемые обоим узлам с другим узлом как аргументом:

TypeScript
hero.addEventListener('enter', other => {})   // коснулся стены или вошёл в триггер
hero.addEventListener('exit',  other => {})

CharacterController2D — движение платформера

TypeScript
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 }.

TypeScript
setLoop(() => {
  hero.controller.move((Input.key('ArrowRight') ? 1 : 0) - (Input.key('ArrowLeft') ? 1 : 0))
  if (Input.key('Space')) hero.controller.jump()
})

Запросы (статические)

TypeScript
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 | null

raycast возвращает ближайшее тело, задетое отрезком; overlapPoint — самое верхнее тело — твёрдое или сенсор — чья форма содержит мировую точку.

Пикинг указателем

Узел с Shape2D плюс Physics2D или Trigger2D кликабелен указателем: тапы хит-тестятся через Physics2D.overlapPoint, поэтому узел получает события click / touchstart. См. События указателя и жесты. Для пикинга без тел используй scene.pick() (границы спрайта).

Подводные камни

TypeScript
// ✗ полные экстенты — 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 на кликабельных узлах.
  • Scene2Dpick() без тел, события указателя на уровне сцены.
  • Sprite — масштаб отрисовки против размера коллайдера.