Sync Ammo.js Physics with Svelte Lifecycle
This article explains how to efficiently integrate the Ammo.js physics engine with the Svelte component lifecycle. You will learn how to initialize the WebAssembly-based physics world, run a performant simulation loop using Svelte’s native lifecycle hooks, synchronize rigid body data with your visual rendering library, and properly clean up memory to prevent leaks.
1. Initialize Ammo.js inside onMount
Because Ammo.js relies on WebAssembly (WASM), it must be loaded
asynchronously in the browser. Svelte’s onMount hook is the
ideal place to initialize the library, as it ensures the code only runs
on the client-side after the DOM is ready.
<script>
import { onMount, onDestroy } from 'svelte';
let physicsWorld;
let animationFrameId;
let lastTime = performance.now();
onMount(async () => {
// Wait for Ammo WebAssembly to load
const AmmoLib = await Ammo();
// Set up collision configuration and dispatchers
const collisionConfiguration = new AmmoLib.btDefaultCollisionConfiguration();
const dispatcher = new AmmoLib.btCollisionDispatcher(collisionConfiguration);
const overlappingPairCache = new AmmoLib.btDbvtBroadphase();
const solver = new AmmoLib.btSequentialImpulseConstraintSolver();
// Create the physics world
physicsWorld = new AmmoLib.btDiscreteDynamicsWorld(
dispatcher,
overlappingPairCache,
solver,
collisionConfiguration
);
physicsWorld.setGravity(new AmmoLib.btVector3(0, -9.81, 0));
// Start the render/physics loop
startLoop();
});
</script>2. Implement the Simulation Loop
To keep the physics simulation running smoothly, step the physics
world inside a requestAnimationFrame loop. This loop
calculates the time delta between frames and updates the Ammo.js
world.
function startLoop() {
const tick = () => {
const now = performance.now();
const deltaTime = (now - lastTime) / 1000; // Convert to seconds
lastTime = now;
if (physicsWorld) {
// Step simulation (deltaTime, maxSubSteps, fixedTimeStep)
physicsWorld.stepSimulation(deltaTime, 10);
// Update visual meshes here
updateVisuals();
}
animationFrameId = requestAnimationFrame(tick);
};
animationFrameId = requestAnimationFrame(tick);
}3. Synchronize Physics Bodies with Visual Meshes
Ammo.js calculates physics in its own memory space. To reflect these updates in a WebGL renderer (like Three.js) integrated with Svelte, you must extract the transform from the rigid body’s motion state and apply it to your visual objects during every tick.
let rigidBodies = []; // Array of { mesh, body } pairings
function updateVisuals() {
const transform = new Ammo.btTransform();
for (let i = 0; i < rigidBodies.length; i++) {
const { mesh, body } = rigidBodies[i];
const motionState = body.getMotionState();
if (motionState) {
motionState.getWorldTransform(transform);
const origin = transform.getOrigin();
const rotation = transform.getRotation();
// Update the visual mesh position and rotation
mesh.position.set(origin.x(), origin.y(), origin.z());
mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
}
}
}4. Prevent Memory Leaks using onDestroy
Ammo.js is a C++ library ported to WebAssembly, meaning it does not
benefit from automatic JavaScript garbage collection. You must manually
destroy instantiated objects. Use Svelte’s onDestroy
lifecycle hook to cancel the animation frame and free up WebAssembly
memory.
<script>
// ... previous setup code ...
onDestroy(() => {
// Stop the loop
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
// Clean up rigid bodies and physics world
if (physicsWorld) {
// Iterate backwards to safely remove bodies
for (let i = rigidBodies.length - 1; i >= 0; i--) {
const { body } = rigidBodies[i];
physicsWorld.removeRigidBody(body);
Ammo.destroy(body);
}
// Destroy core world components
Ammo.destroy(physicsWorld);
// Destroy solvers, dispatchers, and configurations initially created
}
});
</script>