API Examples
Below are several examples that show how Bounce is typically used.
Iterating over all bodies in a world
const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createDynamicBody({ shape: boxShape, position: [0, 5, 0] });
const body3 = world.createDynamicBody({ shape: boxShape, position: [0, 20, 0] });
for (const body of world.dynamicBodies) { // can also use world.kinematicBodies or world.staticBodies
console.log(body.position); // logs three times: [0, 0, 0], [0, 5, 0], [0, 20, 0]
}
Creating a convex hull shape
// convex hulls can be easily created from a flat array of vertices, commonly known as a "point cloud".
const shape = world.createConvexHull({
vertices: vertexPositionArray,
});
const body = world.createDynamicBody({ shape: shape, position: [0, 0, 5] });
Creating a compound shape
const subShape1 = world.createSphere({ radius: 1.0 });
const subShape2 = world.createBox({ width: 2.0, height: 3.0, depth: 4.0 });
// compound shapes are collections of shapes that act as a single shape.
// these can be used to efficiently model more complex shapes, such as non-convex shapes.
const shape = world.createCompoundShape([
{ shape: subShape1, transform: { position: [0, -2, 0] } },
{ shape: subShape2, transform: { position: [0, +2, 0], rotation: [0, Math.PI / 4, 0] } },
]);
const body = world.createDynamicBody({ shape: shape, position: [0, 0, 5] });
Finding all bodies that intersect a sphere
// create a bounce world
const world = new World();
// add a box-shaped body for the ground
const ground = world.createStaticBody({
position: { x: 0, y: 0, z: 0 },
orientation: { x: 0, y: 0, z: 0, w: 1 },
shape: world.createBox({ width: 100, height: 5, depth: 100 }),
});
// add a bunch of capsule-shaped bodies
const capsuleShape = world.createCapsule({ radius: 1.0, height: 2.0 });
for (let i = 0; i < 100; i++) {
const x = (Math.random() - 0.5) * 100;
const y = 15 + (Math.random() - 0.5) * 10;
const z = (Math.random() - 0.5) * 100;
const body = world.createDynamicBody({
position: [x, y, z], // alternate syntax
orientation: [0, 0, 0, 1], // alternate syntax
friction: 0.4,
restitution: 0.3,
shape: capsuleShape,
mass: 1.0,
});
}
// find all bodies intersecting a sphere of radius 5 centered at position (20, 7, -30)
const intersectionShape = Sphere.create({ radius: 5 });
// in this example, we'll put all the intersecting bodies in this array
const intersectingBodies = [];
// this callback gets called on every body that intersects the sphere
function onHit(result: CollideShapeResult) {
intersectingBodies.push(result.body);
// we can return true to stop the search early
return false;
}
world.intersectShape(
onHit,
intersectionShape,
{ position: { x: 20, y: 7, z: -30 } },
);
// log overlapping body positions
for (const body of intersectingBodies) {
console.log(body.position);
}
Reusing shapes
// instead of creating a new shape per body like this:
for (let i = 0; i < 100; i++) {
const shape = world.createBox({ width: 1, height: 1, depth: 1 })
const body = world.createDynamicBody({ shape: shape })
}
// you can create many bodies sharing a single shape
const shape = world.createBox({ width: 1, height: 1, depth: 1 })
for (let i = 0; i < 100; i++) {
const body = world.createDynamicBody({ shape: shape })
}
Computed shape properties can be updated, which also ensures that all associated body properties are updated as well
const shape = world.createBox({ width: 1, height: 1, depth: 1 });
console.log(shape.computedVolume); // volume = 1.0 m^3
const body = world.createDynamicBody({ shape: shape, density: 1000.0 });
console.log(body.mass); // mass = 1000.0 kg
shape.width = 2;
shape.height = 2;
shape.depth = 2;
console.log(shape.computedVolume); // volume = 1.0 m^3 <-- not updated yet!
// the body's mass will still be the old value, since the world is not aware that the shape has been updated
console.log(body.mass); // mass = 1000.0 kg
// inform the world of the change
shape.commitChanges()
console.log(shape.computedVolume); // volume = 8.0 m^3 <-- updated!
// now the body's mass is correct, reflecting the shape's increased volume
console.log(body.mass); // mass = 8000.0 kg
Destroying shapes and bodies
const shape = world.createSphere({ radius: 1.5 });
const body1 = world.createStaticBody({ shape: shape });
const body2 = world.createStaticBody({ shape: shape });
const body3 = world.createStaticBody({ shape: shape });
console.log(world.hasShape(shape)); // true
console.log(world.hasBody(body1)); // true
console.log(world.hasBody(body2)); // true
console.log(world.hasBody(body3)); // true
world.destroyBody(body1);
console.log(world.hasBody(body1)); // false
console.log(world.hasShape(shape)); // true < destroying a body does not destroy its shape
// destroying a shape does destroy all bodies that use it
world.destroyShape(shape);
console.log(world.hasShape(shape)); // false
console.log(world.hasBody(body2)); // false
console.log(world.hasBody(body3)); // false
Creating bodies
const sphere = world.createSphere({ radius: 1.5 })
const dynamicBody = world.createDynamicBody({ shape: sphere });
const staticBody = world.createStaticBody({ shape: sphere });
const kinematicBody = world.createKinematicBody({ shape: sphere });
// alternative syntax
const dynamicBody2 = world.createBody({
shape: sphere,
type: BodyType.dynamic, // or BodyType.kinematic or BodyType.static
});
Destroying bodies
const shape = world.createSphere({ radius: 1.5 })
const body = world.createDynamicBody({ shape: shape });
console.log(world.hasBody(body)); // true
console.log(world.hasShape(shape)); // true
world.destroyBody(body);
console.log(world.hasBody(body)); // false
// note, destroying bodies does not destroy associated shapes, as they may be used later by other bodies
console.log(world.hasShape(shape)); // true
Updating body properties
function onHit(result) {
console.log("hit");
}
const queryShape = Sphere.create({ radius: 5 });
const world = new World();
const shape = world.createSphere({ radius: 2 });
const body = world.createKinematicBody({ shape: shape, position: [0, 0, 0] });
console.log(body.position); // [0, 0, 0]
world.intersectShape(
onHit,
queryShape,
{ position: { x: 0, y: 10, z: 0 } },
); // correct result: does not log "hit"
body.position.set([0, 5, 0]);
console.log(body.position); // [0, 5, 0] <-- seeems correct, but...
world.intersectShape(
onHit,
queryShape,
{ position: { x: 0, y: 10, z: 0 } },
); // incorrect result: does not log "hit"
// must inform the world of the change first
body.commitChanges();
world.intersectShape(
onHit,
queryShape,
{ position: { x: 0, y: 10, z: 0 } },
); // correct result: finds the hit, logs "hit"
Applying forces and impulses
const world = new World();
const shape = world.createCapsule({ radius: 5.0, length: 2.0 });
const body = world.createDynamicBody({ shape, position: [0, 5, 0] });
// impulses can be used to instantly affect the velocity of a body
// apply a linear impulse on the center-of-mass of the body. only affects linear velocity
body.applyLinearImpulse({ x: 0, y: 1000, z: 0 });
// apply an angular impulse about an axis in the local space of the body. only affects angular velocity
body.applyAngularImpulse({ x: 0, y: 0, z: 1000 });
// apply an impulse on an arbitrary point on the body (in world space). may affect both linear and angular velocity
body.applyImpulse({ x: 0, y: 1000, z: 0 }, { x: 0, y: 7, z: 0 });
// apply an impulse on an arbitrary point on the body (in local space). may affect both linear and angular velocity.
// the third optional argument is useLocalFrame, false by default. when set to true, it uses the local frame of the body to compute the change in velocity
body.applyImpulse({ x: 0, y: 1000, z: 0 }, { x: 0, y: 3, z: 0 }, false);
// forces can be used to gradually affect the acceleration of a body over time
// create a linear force that applies to the center-of-mass of the body. only affects linear acceleration
body.applyLinearForce({ x: 0, y: 1000, z: 0 });
// create an angular force about an axis in the local space of the body. only affects angular acceleration
body.applyAngularForce({ x: 0, y: 0, z: 1000 });
// add a persistent force calculated from an arbitrary point on the body (world space). may affect both linear and angular acceleration
body.applyForce({ x: 0, y: 1000, z: 0 }, { x: 0, y: 7, z: 0 });
// add a persistent force calculated from an arbitrary point on the body (local space). may affect both linear and angular acceleration.
// the third optional argument is useLocalFrame, false by default. when set to true, it uses the local frame of the body to compute the change in velocity
body.applyForce({ x: 0, y: 1000, z: 0 }, { x: 0, y: 7, z: 0 }, false);
// since forces persist and accumulate over time, they can be cleared if needed.
body.clearForces();
Collision filters can be used to control which bodies collide with each other
const flags = createBitFlags([
'Player',
'Monster',
'Ghost',
] as const);
const shape = world.createSphere({ radius: 2.0 });
const player = world.createKinematicBody({
shape: shape,
position: [0, 0, 0],
belongsToGroups: flags.Player,
collidesWithGroups: flags.Monster,
});
const ghost = world.createKinematicBody({
shape: shape,
position: [-1.5, 0, 0],
belongsToGroups: flags.Ghost,
collidesWithGroups: flags.None,
});
const monster1 = world.createKinematicBody({
shape: shape,
position: [+1.5, 0, 0],
belongsToGroups: flags.Monster,
collidesWithGroups: flags.Player | flags.Monster,
});
const monster2 = world.createKinematicBody({
shape: shape,
position: [+2.5, 0, 0],
belongsToGroups: flags.Monster,
collidesWithGroups: flags.Player | flags.Monster,
});
// player and ghost will not collide with each other
// player and monster1 will collide with each other
// monster1 and monster2 will collide with each other
Serializing and deserializing a world, to copy a world
const world1 = new World();
const shape = world.createSphere({ radius: 1.5 });
const body = world.createDynamicBody({
shape,
position: [0, 5, 0],
});
const world2 = new World();
for (const body of world2.dynamicBodies) {
console.log(body.position.y); // doesn't log anything, we never get here
}
const array = new Float32Array(10000);
world1.toArray(array);
world2.fromArray(array);
for (const body of world2.dynamicBodies) {
console.log(body.position.y); // logs once: 5
}
Serializing and deserializing a single body
const world = new World({ gravity: [0, 0, 0] }); // no gravity
const body = world.createDynamicBody({
linearVelocity: [5, 0, 0],
position: [0, 0, 0],
shape: world.createSphere({ radius: 1.5 }),
});
const bodyArray = [];
body.toArray(bodyArray);
// simulate 1 second of time passing
world.takeOneStep(1);
console.log(body.position); // [5, 0, 0]
// another second passing
world.takeOneStep(1);
console.log(body.position); // [10, 0, 0]
body.fromArray(bodyArray);
console.log(body.position); // [0, 0, 0]
Rolling back all dynamic bodies in a world
const world = new World({ gravity: [0, 0, 0] });
const shape = world.createSphere({ radius: 1.5 })
const body1 = world.createDynamicBody({ shape, position: [0, 0, 10], linearVelocity: [1, 0, 0] });
const body2 = world.createDynamicBody({ shape, position: [0, 0, 20], linearVelocity: [2, 0, 0] });
const body3 = world.createDynamicBody({ shape, position: [0, 0, 30], linearVelocity: [4, 0, 0] });
let bufferIndex = 0;
const rollbackBuffers = [
new Float32Array(1000),
new Float32Array(1000),
new Float32Array(1000),
new Float32Array(1000),
new Float32Array(1000),
]
for (const body of world.dynamicBodies) {
console.log(body.position); // three logs: [0, 0, 10], [0, 0, 20], [0, 0, 30]
}
for (let i = 0; i < rollbackBuffers.length; i++) {
// serialize state and store for rollback
world.dynamicBodies.toArray(rollbackBuffers[bufferIndex]);
bufferIndex++
world.takeOneStep(1); // simulating one second per step, to keep the math simple for this example
}
for (const body of world.dynamicBodies) {
console.log(body.position); // three logs: [10, 0, 10], [20, 0, 20], [40, 0, 30]
}
// roll back to third step
world.dynamicBodies.fromArray(rollbackBuffers[2])
for (const body of world.dynamicBodies) {
console.log(body.position); // three logs: [2, 0, 10], [4, 0, 20], [8, 0, 30]
}
Scene query precision
// by default, or if preciseWithContacts is specified as the desired precision, contact points are computed and available in the result
world.intersectShape(
(result) => console.log(result.pointA), // contact point(s) available
queryShape,
{ position: { x: 0, y: 0, z: 0 } },
{ precision: QueryPrecision.preciseWithContacts }
);
// precise query but without contact point(s)
world.intersectShape(
(result) => console.log('precise hit'),
queryShape,
{ position: { x: 0, y: 0, z: 0 } },
{ precision: QueryPrecision.precise }
);
// most performant, uses bounding boxes only
world.intersectShape(
(result) => console.log('approximate hit'),
queryShape,
{ position: { x: 0, y: 0, z: 0 } },
{ precision: QueryPrecision.approximate }
);
Raycast queries
const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createDynamicBody({ shape: boxShape, position: [0, 5, 0] });
const body3 = world.createDynamicBody({ shape: boxShape, position: [0, 20, 0] });
const ray = Ray.create({ origin: [0, -10, 0], direction: [0, 1, 0], length: 100 });
world.castRay(
(result) => console.log(result.body.position, result.pointA),
ray,
{
returnClosestOnly: false,
precision: QueryPrecision.preciseWithContacts,
}
); // logs three positions: [0, 0, 0], [0, 5, 0], [0, 20, 0] // note: may not be sorted
world.castRay(
(result) => console.log(result.body.position, result.pointA),
ray,
{
returnClosestOnly: true,
precision: QueryPrecision.preciseWithContacts,
}
); // logs one position: [0, 0, 0]
// more performant, uses bounding boxes only
world.castRay(
(result) => console.log(result.body.position),
ray,
{
returnClosestOnly: false,
precision: QueryPrecision.approximate,
}
); // logs three positions: [0, 0, 0], [0, 5, 0], [0, 20, 0] // note: may not be sorted
Shapecasting / sweeping tests
const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createDynamicBody({ shape: boxShape, position: [0, 5, 0] });
const body3 = world.createDynamicBody({ shape: boxShape, position: [0, 20, 0] });
// we'll make a capsule that will sweep across the first two bodies
const capsule = Capsule.create({ radius: 1.0, length: 2.0 });
world.castShape(
(result) => result.body.position,
capsule,
{ position: { x: 0, y: -10, z: 0 } }, // start transform
{ x: 0, y: 15, z: 0 }, // this vector is treated as the displacement
{ treatAsDisplacement: true }
); // logs twice: [0, 0, 0], [0, 5, 0]
world.castShape(
(result) => result.body.position,
capsule,
{ position: { x: 0, y: -10, z: 0 } }, // start transform
{ x: 0, y: 5, z: 0 }, // this vector is treated as the ending position
{ treatAsDisplacement: false }
); // logs twice: [0, 0, 0], [0, 5, 0]
Estimating collision response
// can use a body that is created inside the world
const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
const boxBody = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0], mass: 5.0 });
// can also use a body that is created outside of the world
const sphereShape = Sphere.create({ radius: 1.5 });
const sphereBody = DynamicBody.create({ shape: sphereShape, position: [2, 0, 0], mass: 10.0, linearVelocity: [5, 0, 0] });
// this type of query estimates the change in linear and angular velocity due to a collision between bodies.
// it does not actually apply any changes in velocity itself, it is just a query.
const result = EstimateCollisionResponseResult.create();
world.estimateCollisionResponse(result, sphereBody, boxBody);
console.log(result.deltaLinearVelocityA, result.deltaAngularVelocityA);
console.log(result.deltaLinearVelocityB, result.deltaAngularVelocityB);
Check if two bodies are in contact
const boxShape = world.createBox({ width: 1, height: 2, depth: 1 });
const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createDynamicBody({ shape: boxShape, position: [0, 0.5, 0] });
const body3 = world.createDynamicBody({ shape: boxShape, position: [0, 20, 0] });
world.takeOneStep(1 / 60);
console.log(world.didBodiesCollide(body1, body2)); // true
console.log(world.didBodiesCollide(body1, body3)); // false
Iterate over all contact manifolds involving a body
const boxShape = world.createBox({ width: 1, height: 2, depth: 1 });
const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createDynamicBody({ shape: boxShape, position: [0, 0.5, 0] });
const body3 = world.createDynamicBody({ shape: boxShape, position: [0, -1.5, 0] });
const body4 = world.createDynamicBody({ shape: boxShape, position: [0, 20, 0] });
const body5 = world.createDynamicBody({ shape: boxShape, position: [0, 20.5, 0] });
world.takeOneStep(1 / 60);
// iterate over all contact manifolds involving a specific body
for (const manifold of world.iterateContactManifolds(body1)) {
console.log([manifold.bodyA, manifold.bodyB]); // logs for [body1, body2] and [body1, body3] (order may not be the same)
}
// or iterate over contact manifolds involving a specific pair of bodies
for (const manifold of world.iterateContactManifolds(body1, body2)) {
console.log([manifold.bodyA, manifold.bodyB]); // logs for [body1, body2] (order may not be the same)
}
// or iterate over all contact manifolds in the world
for (const manifold of world.iterateContactManifolds()) {
console.log([manifold.bodyA, manifold.bodyB]); // logs for [body1, body2], [body1, body3] and [body4, body5] (order may not be the same)
}
Translating shapes in local space
// by default, bounce internally centers all shapes about the origin in their local space.
const sphere1 = world.createSphere({ radius: 1.0 });
const body1 = world.createDynamicBody({ shape: sphere1 });
// a shape can be translated relative to the shape's local space origin
const sphere2 = world.createSphere({ radius: 1.0, position: [3, 0, 0] });
const body2 = world.createDynamicBody({ shape: sphere2 });
// for a typical character controller capsule, you may want the bottom of the capsule to be the local space origin
const radius = 1.0;
const height = 2.0;
const halfCapsuleHeight = radius + height / 2;
const shape = world.createCapsule({ radius, height, position: [0, -halfCapsuleHeight, 0] });
const body = world.createDynamicBody({ shape: shape });
More examples of existing functionality are coming soon: setting up joints and constraints, creating trimeshes, creating heightmaps, and more.