Ammo.js является портом библиотеки Bullet Physics на JavaScript, обеспечивая физическую симуляцию с поддержкой ригидного тела, коллизий, мягких тел и ограничений. Одним из ключевых аспектов сложных симуляций является детерминированность, то есть способность получать одинаковый результат при повторении симуляции с одинаковыми начальными условиями.
Детерминированная симуляция особенно важна для многопользовательских игр, сетевых симуляций и воспроизводимых экспериментов, где требуется синхронизация состояния между различными клиентами или сохранение точного состояния для повторного воспроизведения.
Фиксированный шаг времени (Fixed Time Step) Для
детерминированности необходимо использовать фиксированный шаг симуляции,
обычно через метод
stepSimulation(deltaTime, maxSubSteps, fixedTimeStep).
const fixedTimeStep = 1 / 60;
physicsWorld.stepSimulation(deltaTime, 10, fixedTimeStep);
Здесь deltaTime — время между кадрами,
maxSubSteps — максимальное количество промежуточных шагов,
а fixedTimeStep — фиксированный шаг интеграции. Любое
отклонение от фиксированного шага ведёт к накоплению ошибок и
недетерминированности.
Инициализация генератора случайных чисел Любые стохастические процессы (например, генерация случайных начальных скоростей) должны использовать фиксированный seed, чтобы повторяемость была гарантирована.
Порядок добавления объектов и ограничений Ammo.js чувствителен к порядку создания и добавления объектов в мир. Любые различия в порядке могут привести к различиям в результатах из-за параллельных вычислений и порядка обработки коллизий.
Сериализация позволяет сохранить полное состояние физического мира и восстановить его позже без потери точности. Ammo.js не предоставляет встроенного API для полного snapshot’а, но можно реализовать это через прямое считывание состояния каждого тела.
Для каждого объекта в мире сохраняются следующие параметры:
btVector3): x, y, zbtQuaternion): x, y, z, wbtVector3)btVector3)Пример функции сериализации одного тела:
function serializeRigidBody(body) {
const transform = body.getWorldTransform();
const origin = transform.getOrigin();
const rotation = transform.getRotation();
return {
position: { x: origin.x(), y: origin.y(), z: origin.z() },
rotation: { x: rotation.x(), y: rotation.y(), z: rotation.z(), w: rotation.w() },
linearVelocity: body.getLinearVelocity().toArray(),
angularVelocity: body.getAngularVelocity().toArray(),
mass: body.getMass(),
type: body.isStaticObject() ? "static" : body.isKinematicObject() ? "kinematic" : "dynamic"
};
}
Для полного мира создаётся массив сериализованных объектов, который затем можно сохранять в JSON:
const worldState = [];
for (let i = 0; i < physicsWorld.getNumCollisionObjects(); i++) {
const body = physicsWorld.getCollisionObjectArray().at(i);
worldState.push(serializeRigidBody(body));
}
const serialized = JSON.stringify(worldState);
Восстановление предполагает создание объектов с сохранёнными параметрами и точное восстановление трансформаций и скоростей:
function restoreRigidBody(state, collisionShape) {
const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(state.position.x, state.position.y, state.position.z));
transform.setRotation(new Ammo.btQuaternion(state.rotation.x, state.rotation.y, state.rotation.z, state.rotation.w));
const motionState = new Ammo.btDefaultMotionState(transform);
const localInertia = new Ammo.btVector3(0, 0, 0);
collisionShape.calculateLocalInertia(state.mass, localInertia);
const rbInfo = new Ammo.btRigidBodyConstructionInfo(state.mass, motionState, collisionShape, localInertia);
const body = new Ammo.btRigidBody(rbInfo);
body.setLinearVelocity(new Ammo.btVector3(...state.linearVelocity));
body.setAngularVelocity(new Ammo.btVector3(...state.angularVelocity));
physicsWorld.addRigidBody(body);
return body;
}
Такой подход гарантирует, что восстановленный мир будет идентичен исходному при условии фиксированного шага симуляции.
Использовать одинаковые коллизионные формы Разные типы коллизионных примитивов (Box, Sphere, ConvexHull) могут давать различные результаты столкновений при одинаковых условиях.
Избегать динамических аллокций во время симуляции Создание и удаление объектов в середине кадра может нарушить порядок обработки столкновений и внести недетерминированность.
Фиксировать порядок обработки коллизий Ammo.js
позволяет обходить все объекты через массив
getCollisionObjectArray(). Для воспроизводимости важно
обходить их в одном и том же порядке.
Сохранять состояние генераторов случайных чисел Любой внешний шум или случайные силы должны контролироваться через фиксированные seed’ы.
Проверка повторяемости После восстановления мира рекомендуется выполнить несколько шагов симуляции и проверить, что все объекты находятся в тех же позициях и с теми же скоростями.
В многопользовательских играх можно использовать сериализацию состояния мира для снапшотов, которые рассылаются клиентам. Каждый клиент при получении состояния полностью синхронизирует свои объекты и продолжает симуляцию с фиксированным шагом. Это позволяет:
Ammo.js с детерминированной симуляцией и корректной сериализацией состояния обеспечивает высокий уровень контроля над динамикой объектов, делая возможным точное воспроизведение экспериментов и реализацию сложных игровых механик.