Guide to btActionInterface in Ammo.js Physics

This article provides a comprehensive overview of the btActionInterface in ammo.js (the JavaScript port of the Bullet Physics engine) and demonstrates how to use it to build custom physics controllers. You will learn the core concepts behind this interface, how it integrates into the physics simulation loop, and how to implement your own custom action controllers for specialized object behaviors.

Understanding btActionInterface

In physics engines, some behaviors cannot be easily achieved using standard rigid body forces or constraints alone. Examples include character controllers, vehicle suspension systems, and custom wind or gravity fields.

The btActionInterface is an abstract base class provided by Bullet Physics and ammo.js designed specifically for these scenarios. It allows you to create custom utility classes that hook directly into the internal physics simulation loop. By overriding its core methods, you can execute custom code during every simulation substep, ensuring that your controller updates in perfect synchronization with the physics world.

Key Methods of the Interface

To create a custom controller using btActionInterface, you must implement two primary methods:

  1. updateAction(collisionWorld, timeStep): This is the heart of the controller. The physics world calls this method at every simulation step. It receives the collision world instance and the time increment (timeStep) as arguments. Inside this method, you write the logic to inspect rigid bodies, calculate forces, modify velocities, or manually adjust positions.
  2. debugDraw(debugDrawer): This method is used for rendering debug graphics. If you have visual debug drawing enabled in your ammo.js setup, you can use this method to draw lines, spheres, or coordinate systems representing the internal state of your controller.

How to Implement a Custom Controller

In ammo.js, implementing C++ interfaces directly in JavaScript requires leveraging the Emscripten binding layer. The standard way to build and use a custom action controller involves three main steps.

1. Define the Custom Action Class

You need to create a class that implements the updateAction and debugDraw methods.

class CustomGravityController {
    constructor(rigidBody, gravityVector) {
        this.body = rigidBody;
        this.gravity = gravityVector; // An Ammo.btVector3
    }

    // Called automatically by the physics world every step
    updateAction(collisionWorld, timeStep) {
        if (!this.body.isActive()) {
            return;
        }
        
        // Example: Apply a custom continuous force to the body
        const force = new Ammo.btVector3(
            this.gravity.x() * timeStep,
            this.gravity.y() * timeStep,
            this.gravity.z() * timeStep
        );
        
        this.body.applyCentralForce(force);
        Ammo.destroy(force);
    }

    // Called for debug rendering
    debugDraw(debugDrawer) {
        // Optional: Implement debug drawing helper logic here
    }
}

2. Registering the Action with the Dynamics World

For ammo.js to execute your custom controller, you must register it with your instance of btDiscreteDynamicsWorld. This is done using the addAction method.

// Set up your physics world and rigid body
const dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(...);
const myBody = new Ammo.btRigidBody(...);

// Instantiate your custom action
const myController = new CustomGravityController(myBody, new Ammo.btVector3(0, -9.8, 0));

// Add the controller to the simulation loop
dynamicsWorld.addAction(myController);

Once added, the dynamics world will call updateAction on your controller during every call to dynamicsWorld.stepSimulation().

3. Cleanup and Removal

To prevent memory leaks or errors when destroying objects in your scene, you must cleanly remove the controller from the dynamics world when it is no longer needed.

// Remove the action from the simulation loop
dynamicsWorld.removeAction(myController);

// Clean up any allocated Ammo objects manually to prevent memory leaks
Ammo.destroy(myController.gravity);

Alternatives: Physics Tick Callbacks

In some versions of ammo.js, directly extending btActionInterface through JavaScript can be restrictive due to WebIDL binding limitations. If you encounter issues extending the interface directly, an excellent alternative is to use the world’s pre-tick or post-tick callbacks.

You can set a callback that executes custom controller logic on every tick like this:

dynamicsWorld.setInternalTickCallback((world, timeStep) => {
    // Your custom controller update logic runs here
    // This achieves the exact same result as updateAction
}, isPreTick);

Using either btActionInterface or internal tick callbacks ensures your custom movement, character controls, or environmental forces remain deterministic and fully synchronized with the underlying physics simulation.