Интеграция с TypeScript: типизация и объявления

Ammo.js — это порт популярного физического движка Bullet Physics на JavaScript с использованием Emscripten. Работа с ним напрямую в TypeScript требует корректной типизации, так как библиотека поставляется без встроенных деклараций типов. Типизация обеспечивает безопасность кода, автодополнение и предотвращает большинство ошибок на этапе компиляции.


Подключение и базовая конфигурация

Ammo.js обычно подключается как модуль через import или глобально через <script>. Для TypeScript рекомендуется использовать модульный подход:

import Ammo from "ammo.js";

Для работы с TypeScript создаются декларации типов (.d.ts), которые описывают основные классы, методы и свойства:

declare module "ammo.js" {
    export default function Ammo(): Promise<typeof AmmoNamespace>;

    export namespace AmmoNamespace {
        class btVector3 {
            constructor(x?: number, y?: number, z?: number);
            x(): number;
            y(): number;
            z(): number;
            setValue(x: number, y: number, z: number): void;
        }

        class btRigidBody {
            constructor(constructionInfo: btRigidBodyConstructionInfo);
            setMassProps(mass: number, inertia: btVector3): void;
            setLinearVelocity(velocity: btVector3): void;
        }

        class btRigidBodyConstructionInfo {
            constructor(mass: number, motionState: btMotionState, collisionShape: btCollisionShape, localInertia: btVector3);
        }

        class btCollisionShape {}
        class btBoxShape extends btCollisionShape {
            constructor(halfExtents: btVector3);
        }

        class btMotionState {}
    }
}

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


Типизация векторных операций

Класс btVector3 — основной для представления позиции, скорости, силы. В TypeScript важно определить типы аргументов и возвращаемых значений:

const position = new Ammo.btVector3(0, 10, 0);
const velocity = new Ammo.btVector3(1, 0, 0);

position.setValue(5, 15, 0);
const xCoord: number = position.x();

Типизация предотвращает передачу некорректных типов, например строк вместо чисел. Это особенно важно при расчетах физических взаимодействий.


Создание тел с типами

Создание rigid body требует корректной типизации всех параметров:

const boxShape = new Ammo.btBoxShape(new Ammo.btVector3(1, 1, 1));
const mass = 1;
const localInertia = new Ammo.btVector3(0, 0, 0);
boxShape.calculateLocalInertia?.(mass, localInertia);

const motionState = new Ammo.btMotionState();
const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, boxShape, localInertia);

const body = new Ammo.btRigidBody(rbInfo);
body.setLinearVelocity(new Ammo.btVector3(0, 10, 0));

Здесь каждый объект имеет чётко определённый тип. Использование optional chaining (?.) учитывает возможность отсутствия метода в некоторых версиях Ammo.js.


Объявления глобальных объектов

Если Ammo.js подключается через <script> и создаёт глобальный объект, TypeScript требует объявить его:

declare var Ammo: {
    btVector3: typeof AmmoNamespace.btVector3;
    btRigidBody: typeof AmmoNamespace.btRigidBody;
    btBoxShape: typeof AmmoNamespace.btBoxShape;
    btRigidBodyConstructionInfo: typeof AmmoNamespace.btRigidBodyConstructionInfo;
    btMotionState: typeof AmmoNamespace.btMotionState;
};

Такое объявление позволяет использовать библиотеку без ошибок компиляции, сохраняя автодополнение.


Типизация коллизий и столкновений

Ammo.js предоставляет множество классов для работы с коллизиями: btCollisionShape, btBoxShape, btSphereShape, btCollisionObject. Для TypeScript желательно создать обобщённый тип:

type CollisionShape = AmmoNamespace.btBoxShape | AmmoNamespace.btSphereShape | AmmoNamespace.btCollisionShape;

Это упрощает создание функций, которые могут принимать разные формы тел:

function createRigidBody(shape: CollisionShape, mass: number): AmmoNamespace.btRigidBody {
    const localInertia = new Ammo.btVector3(0, 0, 0);
    shape.calculateLocalInertia?.(mass, localInertia);
    const motionState = new Ammo.btMotionState();
    const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
    return new Ammo.btRigidBody(rbInfo);
}

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


Асинхронная инициализация

Ammo.js часто поставляется как асинхронный модуль:

let AmmoInstance: typeof AmmoNamespace;

Ammo().then((AmmoLib) => {
    AmmoInstance = AmmoLib;
    const vec = new AmmoInstance.btVector3(0, 1, 0);
});

В TypeScript важно определить переменную AmmoInstance с точным типом typeof AmmoNamespace. Это гарантирует, что все методы и классы будут корректно распознаны компилятором.


Расширение типов для проекта

Для крупных проектов рекомендуется создавать собственные декларации, расширяя стандартный набор Ammo.js:

declare module "ammo.js" {
    export namespace AmmoNamespace {
        class btSphereShape extends btCollisionShape {
            constructor(radius: number);
        }

        class btQuaternion {
            constructor(x: number, y: number, z: number, w: number);
            setValue(x: number, y: number, z: number, w: number): void;
        }
    }
}

Это позволяет покрыть все используемые классы и методы, сохраняя строгую типизацию TypeScript и минимизируя runtime-ошибки.


Работа с памятью и очистка объектов

Ammo.js использует WebAssembly, поэтому объекты необходимо очищать вручную, чтобы не было утечек памяти:

const vec = new Ammo.btVector3(1, 2, 3);
// использование
vec.delete();

Типизация помогает отслеживать, какие объекты нужно удалить, особенно при работе с массивами тел и коллизий:

const bodies: AmmoNamespace.btRigidBody[] = [];
bodies.forEach(body => body.delete());

Итоговые рекомендации по интеграции с TypeScript

  • Создавать .d.ts с описанием всех используемых классов и методов.
  • Использовать строго типизированные функции для создания тел и векторов.
  • Объявлять глобальный объект, если Ammo.js подключается через <script>.
  • Расширять декларации по мере использования новых возможностей библиотеки.
  • Не забывать о ручной очистке объектов, типизированных как Ammo классы.
  • Обеспечивать асинхронную инициализацию с корректной типизацией переменной модуля.

Эта интеграция позволяет использовать все преимущества TypeScript: безопасность типов, автодополнение, проверку ошибок на этапе компиляции, одновременно работая с высокопроизводительным физическим движком Ammo.js.