Serialize Ammo.js Physics World for Save Games
To serialize the state of an ammo.js physics world for game saves, you cannot directly stringify the physics world object because ammo.js is a WebAssembly/Emscripten port of the C++ Bullet Physics engine, meaning its data lives in native memory heap space. Instead, you must manually extract key state properties—such as position, rotation, linear velocity, angular velocity, and activation states—from every rigid body in the scene. This data can then be saved as a standard JSON object and injected back into the ammo.js physics bodies upon loading the game.
Step 1: Identify and Map Rigid Bodies
To successfully restore physics states, each rigid body in your
physics world must map to a unique identifier (ID) that persists across
save files. You can assign a unique ID to each body using the
setUserPointer or userPointer property when
creating the body:
// Assign a unique ID to the ammo.js rigid body
rigidBody.userPointer = uniqueObjectId; Step 2: Extracting the Physics State (Serialization)
To save the game, iterate through all collision objects in the dynamic world. Filter for dynamic rigid bodies, extract their spatial coordinates and velocities, and write them to a JSON-serializable array.
function serializePhysicsWorld(dynamicsWorld) {
const savedStates = [];
const numCollisionObjects = dynamicsWorld.getNumCollisionObjects();
for (let i = 0; i < numCollisionObjects; i++) {
const obj = dynamicsWorld.getCollisionObjectArray().at(i);
const body = Ammo.castObject(obj, Ammo.btRigidBody);
// Only serialize dynamic, non-static objects
if (body && !body.isStaticOrKinematicObject()) {
const transform = new Ammo.btTransform();
if (body.getMotionState()) {
body.getMotionState().getWorldTransform(transform);
} else {
transform = body.getWorldTransform();
}
const origin = transform.getOrigin();
const rotation = transform.getRotation();
const linearVelocity = body.getLinearVelocity();
const angularVelocity = body.getAngularVelocity();
savedStates.push({
id: body.userPointer,
position: { x: origin.x(), y: origin.y(), z: origin.z() },
rotation: { x: rotation.x(), y: rotation.y(), z: rotation.z(), w: rotation.w() },
linearVelocity: { x: linearVelocity.x(), y: linearVelocity.y(), z: linearVelocity.z() },
angularVelocity: { x: angularVelocity.x(), y: angularVelocity.y(), z: angularVelocity.z() },
activationState: body.getActivationState()
});
Ammo.destroy(transform);
}
}
return JSON.stringify(savedStates);
}Step 3: Restoring the Physics State (Deserialization)
When loading a saved game, parse the JSON array. Match each record back to its corresponding rigid body in the active physics world using the unique ID, apply the saved transforms, reset the velocities, and force the physics engine to reactivate the bodies.
function deserializePhysicsWorld(dynamicsWorld, jsonStateString, bodyMap) {
const savedStates = JSON.parse(jsonStateString);
savedStates.forEach(state => {
const body = bodyMap[state.id]; // Look up the rigid body from your game's registry
if (body) {
// 1. Create and apply new transform
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));
body.setWorldTransform(transform);
if (body.getMotionState()) {
body.getMotionState().setWorldTransform(transform);
}
// 2. Restore Velocities
const linVel = new Ammo.btVector3(state.linearVelocity.x, state.linearVelocity.y, state.linearVelocity.z);
const angVel = new Ammo.btVector3(state.angularVelocity.x, state.angularVelocity.y, state.angularVelocity.z);
body.setLinearVelocity(linVel);
body.setAngularVelocity(angVel);
// 3. Restore Activation State and Clear Forces
body.clearForces();
body.setActivationState(state.activationState);
body.activate(); // Wake the body up so physics calculations resume immediately
// Clean up temporary WASM memory allocations
Ammo.destroy(transform);
Ammo.destroy(linVel);
Ammo.destroy(angVel);
}
});
}Critical Implementation Details
- Memory Management: Because ammo.js uses WebAssembly
heap memory, you must explicitly call
Ammo.destroy()on any newly created instances ofbtTransform,btVector3, orbtQuaternioninside your save/load loops to prevent severe memory leaks. - Sleeping States: Saving the
activationStateis crucial. If a body was “sleeping” (inactive due to resting on a surface) when the game was saved, restoring its activation state prevents unnecessary CPU overhead when the save is loaded. - Interpolation Erasure: To prevent visual stuttering directly after loading, clear the interpolation history of your visual meshes (e.g., in Three.js or Babylon.js) so they instantly snap to the newly loaded physics transform instead of trying to animate towards it.