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:
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.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.