Установка через npm и сборщики модулей

Библиотека Ammo.js распространяется в виде npm-пакета и может использоваться в современных проектах на базе модульных сборщиков. Пакет представляет собой порт физического движка Bullet Physics, скомпилированный в WebAssembly или asm.js с помощью Emscripten.

Установка пакета

Установка выполняется стандартным способом:

npm install ammo.js

или при использовании Yarn:

yarn add ammo.js

После установки пакет появляется в директории node_modules и может быть импортирован в код.


Структура пакета и особенности загрузки

Ammo.js не является обычным ES-модулем. Библиотека экспортирует фабричную функцию, возвращающую промис, который разрешается в объект модуля. Это связано с асинхронной инициализацией WebAssembly.

Типичная структура использования:

import Ammo from "ammo.js";

Ammo().then((AmmoLib) => {
    const collisionConfig = new AmmoLib.btDefaultCollisionConfiguration();
    const dispatcher = new AmmoLib.btCollisionDispatcher(collisionConfig);
});

Почему используется промис

В отличие от большинства npm-пакетов, Ammo.js:

  • Загружает .wasm файл;
  • Инициализирует виртуальную память;
  • Компилирует WebAssembly-модуль в рантайме.

Поэтому доступ к API возможен только после завершения инициализации.


Использование в среде Webpack

Webpack корректно работает с Ammo.js без дополнительной настройки в большинстве случаев. Однако важно учитывать несколько аспектов:

1. Поддержка WebAssembly

Webpack 5 поддерживает WebAssembly из коробки. Если используется Webpack 4, требуется включение соответствующих опций:

module.exports = {
    experiments: {
        asyncWebAssembly: true
    }
};

2. Правильная обработка wasm-файла

Иногда требуется явно указать тип ресурса:

module.exports = {
    module: {
        rules: [
            {
                test: /\.wasm$/,
                type: "webassembly/async"
            }
        ]
    }
};

В современных конфигурациях это обычно не требуется.


Использование в Vite

Vite работает с WebAssembly значительно проще благодаря нативной поддержке ES-модулей.

Установка стандартная:

npm install ammo.js

Импорт аналогичен:

import Ammo from "ammo.js";

const AmmoLib = await Ammo();

Особенность dev-сервера

Во время разработки Vite корректно обрабатывает .wasm как статический ресурс. Дополнительной конфигурации обычно не требуется.

Если возникают ошибки MIME-типа, необходимо убедиться, что сервер возвращает:

Content-Type: application/wasm

Использование с Rollup

Rollup требует подключения плагина для работы с WebAssembly:

npm install @rollup/plugin-wasm --save-dev

Конфигурация:

import wasm from "@rollup/plugin-wasm";

export default {
    plugins: [
        wasm()
    ]
};

После этого Ammo.js будет корректно подключаться как асинхронный модуль.


Разница между asm.js и WebAssembly сборками

Пакет может содержать несколько вариантов сборки:

  • asm.js (медленнее, но совместимее)
  • WebAssembly (быстрее и предпочтительнее)

Современные браузеры используют WebAssembly по умолчанию. Проверка поддержки:

if (typeof WebAssembly === "object") {
    // используется wasm
}

WebAssembly-версия обеспечивает:

  • Более высокую производительность;
  • Меньшее потребление памяти;
  • Более быструю инициализацию.

Динамическая загрузка (Lazy Loading)

В крупных проектах целесообразно загружать Ammo.js только при необходимости.

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

async function loadPhysics() {
    const AmmoModule = await import("ammo.js");
    const AmmoLib = await AmmoModule.default();
    return AmmoLib;
}

Такой подход:

  • Уменьшает размер основного бандла;
  • Ускоряет первоначальную загрузку страницы;
  • Позволяет подключать физику только в нужных сценах.

Настройка пути к wasm-файлу

Иногда сборщик размещает .wasm в отдельной директории. В таком случае требуется указать путь вручную:

import Ammo from "ammo.js";

Ammo({
    locateFile: (path) => {
        if (path.endsWith(".wasm")) {
            return "/assets/wasm/" + path;
        }
        return path;
    }
}).then((AmmoLib) => {
    // работа с библиотекой
});

Параметр locateFile используется рантаймом Emscripten для поиска бинарных ресурсов.


Использование в TypeScript

Ammo.js не всегда поставляется с полноценными типами. Возможны два варианта:

1. Установка готовых типов

npm install --save-dev @types/ammo.js

2. Объявление модуля вручную

Создание файла ammo.d.ts:

declare module "ammo.js" {
    export default function Ammo(config?: any): Promise<any>;
}

Для строгой типизации рекомендуется описывать используемые классы вручную, например:

type AmmoType = Awaited<ReturnType<typeof Ammo>>;

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

Ammo.js часто используется совместно с Three.js для реализации физики в браузерных 3D-приложениях.

Типичная схема инициализации:

import * as THREE from "three";
import Ammo from "ammo.js";

async function init() {
    const AmmoLib = await Ammo();

    const physicsWorld = new AmmoLib.btDiscreteDynamicsWorld(
        new AmmoLib.btCollisionDispatcher(
            new AmmoLib.btDefaultCollisionConfiguration()
        ),
        new AmmoLib.btDbvtBroadphase(),
        new AmmoLib.btSequentialImpulseConstraintSolver(),
        new AmmoLib.btDefaultCollisionConfiguration()
    );
}

Разделение графики и физики позволяет:

  • Выполнять симуляцию независимо от рендеринга;
  • Обновлять физику с фиксированным шагом времени;
  • Повышать стабильность симуляции.

Работа в Node.js

Ammo.js может использоваться в среде Node.js, если требуется серверная физическая симуляция.

Пример:

const Ammo = require("ammo.js");

Ammo().then((AmmoLib) => {
    const world = new AmmoLib.btDiscreteDynamicsWorld(...);
});

Следует учитывать:

  • Отсутствие DOM;
  • Ограничения производительности;
  • Необходимость ручного управления памятью.

Управление памятью в контексте сборщиков

Ammo.js использует модель ручного управления памятью, унаследованную от C++.

Важно:

const vec = new AmmoLib.btVector3(0, 10, 0);

// использование

AmmoLib.destroy(vec);

Сборщики модулей не влияют на жизненный цикл объектов внутри WebAssembly. Утечки памяти возможны при отсутствии вызова destroy.


Оптимизация размера бандла

Ammo.js — тяжёлый модуль (несколько мегабайт). Для оптимизации:

  • Использовать динамическую загрузку;
  • Подключать только в нужных маршрутах;
  • Включать code splitting;
  • Использовать production-режим сборщика.

Пример для Webpack:

webpack --mode production

В production-режиме происходит:

  • Минификация JS-кода;
  • Оптимизация WebAssembly;
  • Удаление неиспользуемого кода.

Частые проблемы при установке

Ошибка: WebAssembly module not found

Причины:

  • Неверный путь к .wasm;
  • Неправильная конфигурация сборщика;
  • Отсутствие поддержки wasm в среде.

Ошибка: MIME type application/octet-stream

Решение — настройка сервера для выдачи:

application/wasm

Ошибка двойной инициализации

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

let ammoInstance;

async function getAmmo() {
    if (!ammoInstance) {
        ammoInstance = await Ammo();
    }
    return ammoInstance;
}

Архитектурные рекомендации

При использовании Ammo.js в проекте со сборщиком модулей рекомендуется:

  • Создать отдельный модуль physics.js;
  • Инициализировать библиотеку один раз;
  • Экспортировать готовый AmmoLib;
  • Изолировать физическую логику от слоя рендеринга;
  • Использовать фиксированный timestep (например, 1/60).

Такая структура облегчает масштабирование проекта и упрощает тестирование.


Итоговая схема подключения

  1. Установка через npm.
  2. Асинхронная инициализация через Ammo().
  3. Настройка сборщика при необходимости.
  4. Управление путём к .wasm.
  5. Контроль памяти вручную.
  6. Использование динамической загрузки для оптимизации.

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