Паттерн связывания физических тел с игровыми объектами

Ammo.js — это JavaScript-порт известной физической библиотеки Bullet Physics, позволяющий создавать реалистичную физику в браузере. Основное назначение библиотеки — симуляция динамики твёрдых тел, столкновений, трения, гравитации и ограничений между объектами. В контексте игрового движка или визуализации ключевой задачей является связывание физических тел с игровыми объектами для корректного отображения и управления.

В Ammo.js физические объекты представлены через btRigidBody, а визуальные объекты — через классы движка рендеринга (например, Three.js Mesh). Связь между ними обеспечивает паттерн синхронизации, при котором изменения состояния физического тела отражаются на игровом объекте.


Создание физических тел

Физическое тело создаётся по следующей схеме:

  1. Определение формы (btCollisionShape) Примеры форм:

    • btBoxShape — кубическая форма, принимает вектор половинных размеров.
    • btSphereShape — сферическая форма, принимает радиус.
    • btCapsuleShape — капсула, принимает радиус и высоту.
  2. Масса и инерция Масса определяет, является объект динамическим или статическим. Для динамических тел необходимо рассчитать локальную инерцию:

    const mass = 1;
    const localInertia = new Ammo.btVector3(0, 0, 0);
    shape.calculateLocalInertia(mass, localInertia);
  3. Создание движения (btDefaultMotionState) Этот объект хранит позицию и ориентацию тела и служит связующим звеном между физикой и визуализацией:

    const transform = new Ammo.btTransform();
    transform.setIdentity();
    transform.setOrigin(new Ammo.btVector3(x, y, z));
    const motionState = new Ammo.btDefaultMotionState(transform);
  4. Создание btRigidBody С телом связываются форма, масса и инерция:

    const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
    const body = new Ammo.btRigidBody(rbInfo);
    dynamicsWorld.addRigidBody(body);

Связывание с игровым объектом

После добавления тела в мир необходимо синхронизировать визуальный объект с физическим:

function syncVisualWithPhysics(mesh, body) {
    const transform = new Ammo.btTransform();
    body.getMotionState().getWorldTransform(transform);
    const origin = transform.getOrigin();
    const rotation = transform.getRotation();

    mesh.position.set(origin.x(), origin.y(), origin.z());
    mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
}

Ключевые моменты:

  • getWorldTransform() возвращает текущую позицию и ориентацию физического тела.
  • Позиция и вращение обновляются каждую итерацию физического мира.
  • Важно избегать прямого изменения mesh.position вне синхронизации с физикой для динамических объектов.

Паттерн “Motion State Proxy”

Для упрощения синхронизации часто используют Motion State Proxy, оборачивающий btDefaultMotionState и визуальный объект:

class MotionStateProxy {
    constructor(mesh) {
        this.mesh = mesh;
        this.transform = new Ammo.btTransform();
        this.motionState = new Ammo.btDefaultMotionState();
    }

    updateFromPhysics(body) {
        body.getMotionState().getWorldTransform(this.transform);
        const origin = this.transform.getOrigin();
        const rotation = this.transform.getRotation();
        this.mesh.position.set(origin.x(), origin.y(), origin.z());
        this.mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
    }
}

Использование такого паттерна повышает читаемость кода и централизует управление синхронизацией.


Управление кинематическими телами

Кинематические тела не подчиняются физическим силам, но могут влиять на динамические объекты. Их создают с нулевой массой и флагом CF_KINEMATIC_OBJECT:

body.setCollisionFlags(body.getCollisionFlags() | 2); // 2 — CF_KINEMATIC_OBJECT
body.setActivationState(4); // 4 — DISABLE_DEACTIVATION

Позицию таких тел обновляют напрямую через btTransform, синхронизируя с визуальным объектом.


Связывание сложных объектов

Для игровых объектов с несколькими частями (например, робот с соединёнными сегментами) используется паттерн Compound Shape:

const compoundShape = new Ammo.btCompoundShape();
const childTransform = new Ammo.btTransform();
childTransform.setIdentity();
childTransform.setOrigin(new Ammo.btVector3(offsetX, offsetY, offsetZ));
compoundShape.addChildShape(childTransform, childShape);

После добавления всех частей создаётся одно btRigidBody, что позволяет управлять сложной структурой как единым целым, при этом каждое визуальное представление синхронизируется через соответствующий Motion State.


Оптимизация синхронизации

  1. Минимизировать аллокации Частое создание новых btTransform и btVector3 замедляет симуляцию. Лучше переиспользовать объекты.
  2. Обновлять только динамические объекты Статические или кинематические объекты не требуют постоянной синхронизации.
  3. Группировка обновлений При большом количестве тел полезно обновлять позиции и вращения в одном цикле после шага симуляции.

Интеграция с игровым циклом

Симуляция Ammo.js обычно интегрируется в игровой цикл следующим образом:

function update(deltaTime) {
    dynamicsWorld.stepSimulation(deltaTime, 10);
    for (const obj of dynamicObjects) {
        obj.motionStateProxy.updateFromPhysics(obj.body);
    }
    renderer.render(scene, camera);
}
  • stepSimulation() выполняет шаг физического мира.
  • Синхронизация с визуальными объектами выполняется после шага.
  • Можно задавать максимум итераций для повышения стабильности (10 в примере).

Работа с ограничениями

Ammo.js поддерживает различные виды соединений (constraints), связывающих два тела:

  • btPoint2PointConstraint — шарнирное соединение.
  • btHingeConstraint — вращение вокруг одной оси.
  • btSliderConstraint — линейное движение по оси.

После добавления ограничения синхронизация визуальных объектов осуществляется по той же схеме через Motion State Proxy, при этом физика автоматически рассчитывает взаимное влияние тел.


Практические рекомендации

  • Всегда использовать единый источник истины для позиции и ориентации — физическое тело.
  • Для сложных игровых объектов стоит применять паттерн MotionStateProxy и Compound Shapes.
  • Избегать прямого вмешательства в физические тела между шагами симуляции.
  • Визуальная синхронизация должна выполняться после stepSimulation() для корректного отображения столкновений и взаимодействий.

Такой подход обеспечивает надёжную интеграцию Ammo.js в игровую архитектуру, позволяя поддерживать физически достоверные сцены с минимальными ошибками и высокой производительностью.