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