Skip to main content

📜 Rapier Physics Scripting API

Access the full power of Rapier Physics through the Rapier Physics plugin's scripting interface.

Getting Started

🚀 Listening to Rapier Physics Plugin Events in JavaScript

To integrate and respond to events from the Rapier Physics Construct plugin in your external JavaScript code, follow the pattern below. This allows you to hook into powerful physics events like world initialization, object creation/destruction, collision detection, and more.

Event Requirements

These physics events require:

Rapier Physics initialization via event sheets Object creation/destruction via event sheet actions

Objects created/destroyed directly in JavaScript code will not trigger onobjectadded/onobjectdestroy events - only event sheet actions will dispatch these notifications.

✅ Step 1: Add Your Script as the Main Script

Ensure your external JavaScript file is set as the Main Script in your Construct project settings.

✅ Step 2: Register Event Listeners in runOnStartup

runOnStartup(async runtime => {
// This runs during the loading screen (before any layout starts).
// It's a good place to prepare event hooks or pre-initialization code.
// Note layouts, objects etc. are not yet available.
runtime.addEventListener("beforeprojectstart", () => OnBeforeProjectStart(runtime));
});

✅ Step 3: Listen to Rapier Physics Events

async function OnBeforeProjectStart(runtime) {
// At this point, the first layout and its instances are loaded.

const rapierPhysics = runtime.objects.Rapier.getFirstInstance();// change this if you rename the plugin

if (!rapierPhysics) {
console.warn("Rapier Physics instance not found.");
return;
}

// 🌍 Physics world is initialized and ready
rapierPhysics.addEventListener("onrapierinit", (e) => {
console.log("✅ Rapier Physics Initialized:", e);

// You can access the Rapier context in two ways:

// Option 1: From global (if available and not in worker)
if (typeof window !== "undefined" && window.MakeIt3DContext) {
const rapierContext = window.MakeIt3DContext.rapierContext;
console.log("Global Rapier context:", rapierContext);
}

// Option 2: Directly from the event
const rapierContext = e.data;
const { rapier, world, objects, characterControllers, joints } = rapierContext;

console.log("Rapier Library:", rapier);
console.log("Physics World:", world);
console.log("Objects Registry:", objects);
});

// 📦 Fired when a physics object (body/collider) is added
rapierPhysics.addEventListener("onobjectadded", (e) => {
console.log("📦 Physics Object Added:", e);
console.log("Object ID:", e.data.objectId);
console.log("Object Tag:", e.data.tag);
});

// ❌ Fired when a physics object is removed from the world
rapierPhysics.addEventListener("onobjectdestroy", (e) => {
console.log("❌ Physics Object Destroyed:", e);
console.log("Object ID:", e.data.objectId);
});

// 🎯 Fired when a raycast hits objects
rapierPhysics.addEventListener("onraycasthit", (e) => {
console.log("🎯 Raycast Hit:", e);
console.log("First Hit:", e.data.firstHit);
console.log("All Hits:", e.data.allHits);
});

// 🎮 Per-frame update hook
runtime.addEventListener("tick", () => Tick(runtime));
}

📝 Per-Frame Update (Tick Function)

function Tick(runtime) {
// Code to run every frame (tick)
// You can update UI, sync physics data, or perform runtime checks here

// Example: Access physics world for custom updates
if (typeof window !== "undefined" && window.MakeIt3DContext) {
const { world, bodies } = window.MakeIt3DContext.rapierContext;

// Custom physics logic here
// world.step() is handled automatically by the plugin
}
}

The Rapier Physics context becomes available globally after initialization through:

const rapierContext = window.MakeIt3DContext.rapierContext;
const { rapier, world, objects, characterControllers, joints, eventQueue } = rapierContext;

Context API Reference

PropertyTypeDescription
rapierRAPIERThe main Rapier physics library instance
worldRAPIER.WorldThe main physics world instance
eventQueueRAPIER.EventQueuePhysics event queue for collisions
objectsMap<string, Object>Registry of mesh + rigidBody pairs
colliderTypesRAPIER.ColliderDescCollider descriptor types for easy access
characterControllersMap<string, RAPIER.KinematicCharacterController>Character controllers registry
jointsMap<string, RAPIER.Joint>Registry of all joints/constraints
jointSetRAPIER.ImpulseJointSetThe joint set for physics simulation
pluginUtilsObjectUtility functions (UpdateWorldStep, MoveCharacter)

Basic Physics Object Creation

Creating Dynamic Bodies

// Access the Rapier context
const { rapier, world, objects } = window.MakeIt3DContext.rapierContext;

// Create a dynamic rigid body
const bodyDesc = rapier.RigidBodyDesc.dynamic()
.setTranslation(0.0, 5.0, 0.0)
.setLinvel(0.0, 0.0, 0.0);

const body = world.createRigidBody(bodyDesc);

// Create a box collider
const colliderDesc = rapier.ColliderDesc.cuboid(0.5, 0.5, 0.5)
.setDensity(1.0)
.setFriction(0.5)
.setRestitution(0.3);

const collider = world.createCollider(colliderDesc, body);

// Store the object pair in the objects map
const objectId = "dynamicBox_" + Date.now();
objects.set(objectId, {
rigidBody: body,
collider: collider,
mesh: null, // Will be populated if you have a corresponding 3D mesh
type: "dynamic",
tag: "box"
});

console.log("Created dynamic box with ID:", objectId);

Creating Static Bodies (Ground, Walls)

const { rapier, world, objects } = window.MakeIt3DContext.rapierContext;

// Create static ground
const groundBodyDesc = rapier.RigidBodyDesc.fixed()
.setTranslation(0.0, -1.0, 0.0);

const groundBody = world.createRigidBody(groundBodyDesc);

const groundColliderDesc = rapier.ColliderDesc.cuboid(10.0, 0.1, 10.0)
.setFriction(0.7);

const groundCollider = world.createCollider(groundColliderDesc, groundBody);

// Store in objects map
objects.set("ground", {
rigidBody: groundBody,
collider: groundCollider,
mesh: null,
type: "static",
tag: "ground"
});

Creating Kinematic Bodies

const { rapier, world, objects } = window.MakeIt3DContext.rapierContext;

// Create kinematic body (can be moved but not affected by forces)
const kinematicBodyDesc = rapier.RigidBodyDesc.kinematicVelocityBased()
.setTranslation(2.0, 0.0, 0.0);

const kinematicBody = world.createRigidBody(kinematicBodyDesc);

const kinematicColliderDesc = rapier.ColliderDesc.ball(0.5);
const kinematicCollider = world.createCollider(kinematicColliderDesc, kinematicBody);

// Store in objects map
objects.set("movingPlatform", {
rigidBody: kinematicBody,
collider: kinematicCollider,
mesh: null,
type: "kinematic",
tag: "platform"
});

// Move kinematic body
kinematicBody.setLinvel({ x: 1.0, y: 0.0, z: 0.0 });

Working with Forces and Impulses

Applying Forces

// Get an object from registry
const objectData = objects.get("dynamicBox_123");

if (objectData && objectData.rigidBody) {
const body = objectData.rigidBody;

// Apply force at center of mass
body.addForce({ x: 0.0, y: 100.0, z: 0.0 }, true);

// Apply force at specific point
const forcePoint = { x: 0.0, y: 0.0, z: 0.0 };
const force = { x: 50.0, y: 0.0, z: 0.0 };
body.addForceAtPoint(force, forcePoint, true);

// Apply impulse (instant velocity change)
body.applyImpulse({ x: 0.0, y: 10.0, z: 0.0 }, true);

// Apply torque impulse (rotation)
body.applyTorqueImpulse({ x: 0.0, y: 5.0, z: 0.0 }, true);
}

Setting Velocities

const objectData = objects.get("player");

if (objectData && objectData.rigidBody) {
const body = objectData.rigidBody;

// Set linear velocity
body.setLinvel({ x: 5.0, y: 0.0, z: 0.0 });

// Set angular velocity
body.setAngvel({ x: 0.0, y: 2.0, z: 0.0 });

// Get current velocities
const linvel = body.linvel();
const angvel = body.angvel();

console.log("Linear velocity:", linvel);
console.log("Angular velocity:", angvel);
}

Raycasting and Spatial Queries

Basic Raycasting

const { world } = window.MakeIt3DContext.rapierContext;

// Define ray
const rayOrigin = { x: 0.0, y: 5.0, z: 0.0 };
const rayDirection = { x: 0.0, y: -1.0, z: 0.0 };
const maxDistance = 10.0;
const solid = true;

// Cast ray and get first hit
const hit = world.castRay(
rayOrigin,
rayDirection,
maxDistance,
solid
);

if (hit) {
console.log("Ray hit at distance:", hit.toi);
console.log("Hit point:", {
x: rayOrigin.x + rayDirection.x * hit.toi,
y: rayOrigin.y + rayDirection.y * hit.toi,
z: rayOrigin.z + rayDirection.z * hit.toi
});
console.log("Hit normal:", hit.normal);
}

Advanced Raycasting with Filters

const { world } = window.MakeIt3DContext.rapierContext;

// Cast ray and get all intersections
const hits = [];
world.intersectionsWithRay(
rayOrigin,
rayDirection,
maxDistance,
solid,
(hit) => {
hits.push(hit);
return true; // Continue searching for more hits
}
);

console.log("Found", hits.length, "intersections");
hits.forEach((hit, index) => {
console.log(`Hit ${index}:`, hit);
});

Shape Casting

const { rapier, world } = window.MakeIt3DContext.rapierContext;

// Create a shape to cast (sphere in this case)
const shape = new rapier.Ball(0.5);
const shapePos = { x: 0.0, y: 5.0, z: 0.0 };
const shapeRot = { w: 1.0, x: 0.0, y: 0.0, z: 0.0 };
const shapeVel = { x: 0.0, y: -1.0, z: 0.0 };

// Cast shape
const hit = world.castShape(
shapePos,
shapeRot,
shapeVel,
shape,
maxDistance,
solid
);

if (hit) {
console.log("Shape hit at time:", hit.toi);
}

Collision Detection and Events

Setting Up Collision Events

const { world, eventQueue, objects } = window.MakeIt3DContext.rapierContext;

// Process collision events each frame
function processCollisionEvents() {
eventQueue.drainCollisionEvents((handle1, handle2, started) => {
// Get colliders involved in collision
const collider1 = world.getCollider(handle1);
const collider2 = world.getCollider(handle2);

if (started) {
console.log("Collision started between:", handle1, "and", handle2);

// Find object IDs from our registry
let objectId1 = null, objectId2 = null;

for (const [id, objectData] of objects.entries()) {
if (objectData.collider && objectData.collider.handle === handle1) objectId1 = id;
if (objectData.collider && objectData.collider.handle === handle2) objectId2 = id;
}

console.log("Objects involved:", objectId1, objectId2);
} else {
console.log("Collision ended between:", handle1, "and", handle2);
}
});
}

// Call this in your tick function
function onTick(runtime) {
processCollisionEvents();
}

Custom Collision Response

function handleCollision(objectId1, objectId2) {
const objectData1 = objects.get(objectId1);
const objectData2 = objects.get(objectId2);

if (objectData1 && objectData2) {
const body1 = objectData1.rigidBody;
const body2 = objectData2.rigidBody;

// Example: Explosion effect on collision
if (objectData1.tag === "explosive" || objectData2.tag === "explosive") {
const explosionForce = 1000.0;
const pos1 = body1.translation();
const pos2 = body2.translation();

// Calculate direction from explosion center
const direction = {
x: pos2.x - pos1.x,
y: pos2.y - pos1.y,
z: pos2.z - pos1.z
};

// Normalize and apply force
const distance = Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z);
if (distance > 0) {
const force = {
x: (direction.x / distance) * explosionForce,
y: (direction.y / distance) * explosionForce,
z: (direction.z / distance) * explosionForce
};

body2.applyImpulse(force, true);
}
}
}
}

// Usage
const playerController = new AdvancedCharacterController("player");

// In your tick function
function onTick(runtime) {
const deltaTime = runtime.dt;
playerController.update(deltaTime);

// Handle input
if (runtime.keyboard.isKeyDown("ArrowRight")) {
playerController.move({ x: 1, y: 0, z: 0 });
}
if (runtime.keyboard.isKeyDown("Space")) {
playerController.jump();
}
}

Character Controller

Basic Character Movement

const { world, characterControllers, pluginUtils } = window.MakeIt3DContext.rapierContext;

// Use the plugin's built-in character movement function
function moveCharacter(controllerId, inputX, inputZ, speed) {
// This uses the plugin's utility function
pluginUtils.MoveCharacter(controllerId, inputX, inputZ, speed);
}

// Alternative: Manual character controller usage
function manualMoveCharacter(controllerId, movement) {
const controller = characterControllers.get(controllerId);
const objectData = objects.get(controllerId);

if (controller && objectData && objectData.collider) {
controller.computeColliderMovement(
objectData.collider,
movement
);

const correctedMovement = controller.computedMovement();
const currentPos = objectData.collider.translation();

// Apply movement to the collider's parent body
const body = objectData.rigidBody;
if (body) {
body.setTranslation({
x: currentPos.x + correctedMovement.x,
y: currentPos.y + correctedMovement.y,
z: currentPos.z + correctedMovement.z
}, true);
}

// Check if character is grounded
const isGrounded = controller.computedGrounded();
console.log("Character grounded:", isGrounded);
}
}

// Usage examples
moveCharacter("player", 1.0, 0.0, 5.0); // Move right at speed 5
const movement = { x: 0.1, y: 0.0, z: 0.0 }; // Move right manually
manualMoveCharacter("player", movement);

Advanced Character Controller

class AdvancedCharacterController {
constructor(collider) {
this.collider = collider;
this.body = collider.parent();
this.controller = window.MakeIt3DContext.rapierContext.characterController;

// Movement properties
this.speed = 5.0;
this.jumpForce = 10.0;
this.isGrounded = false;
}

update(deltaTime) {
const currentPos = this.collider.translation();
let movement = { x: 0, y: 0, z: 0 };

// Apply gravity if not grounded
if (!this.isGrounded) {
movement.y = -9.81 * deltaTime;
}

// Compute collision-aware movement
this.controller.computeColliderMovement(this.collider, movement);
const correctedMovement = this.controller.computedMovement();

// Update position
this.body.setTranslation({
x: currentPos.x + correctedMovement.x,
y: currentPos.y + correctedMovement.y,
z: currentPos.z + correctedMovement.z
}, true);

// Update grounded state
this.isGrounded = this.controller.computedGrounded();
}

move(direction) {
const currentVel = this.body.linvel();
this.body.setLinvel({
x: direction.x * this.speed,
y: currentVel.y,
z: direction.z * this.speed
});
}

jump() {
if (this.isGrounded) {
this.body.applyImpulse({ x: 0, y: this.jumpForce, z: 0 }, true);
}
}
}

// Usage
const playerCollider = colliders.get("player");
const playerController = new AdvancedCharacterController(playerCollider);

// In your tick function
function onTick(runtime) {
const deltaTime = runtime.dt;
playerController.update(deltaTime);

// Handle input
if (runtime.keyboard.isKeyDown("ArrowRight")) {
playerController.move({ x: 1, y: 0, z: 0 });
}
if (runtime.keyboard.isKeyDown("Space")) {
playerController.jump();
}
}

Joints and Constraints

Fixed Joint

const { rapier, world, objects, joints } = window.MakeIt3DContext.rapierContext;

const objectData1 = objects.get("object1");
const objectData2 = objects.get("object2");

if (objectData1 && objectData2) {
const body1 = objectData1.rigidBody;
const body2 = objectData2.rigidBody;

const jointDesc = rapier.JointDesc.fixed(
{ x: 0.0, y: 0.0, z: 0.0 }, // anchor point on body1
{ w: 1.0, x: 0.0, y: 0.0, z: 0.0 }, // rotation on body1
{ x: 1.0, y: 0.0, z: 0.0 }, // anchor point on body2
{ w: 1.0, x: 0.0, y: 0.0, z: 0.0 } // rotation on body2
);

const joint = world.createImpulseJoint(jointDesc, body1, body2, true);
joints.set("fixedJoint1", joint);
}

Revolute Joint (Hinge)

const { rapier, world, objects, joints } = window.MakeIt3DContext.rapierContext;

const doorData = objects.get("door");
const frameData = objects.get("doorFrame");

if (doorData && frameData) {
const body1 = doorData.rigidBody;
const body2 = frameData.rigidBody;

const jointDesc = rapier.JointDesc.revolute(
{ x: 0.0, y: 0.0, z: 0.0 }, // anchor on body1
{ x: -0.5, y: 0.0, z: 0.0 }, // anchor on body2
{ x: 0.0, y: 1.0, z: 0.0 } // rotation axis
);

// Set joint limits
jointDesc.setLimits(-1.57, 1.57); // -90 to +90 degrees

const joint = world.createImpulseJoint(jointDesc, body1, body2, true);
joints.set("doorHinge", joint);
}

Prismatic Joint (Slider)

const { rapier, world, objects, joints } = window.MakeIt3DContext.rapierContext;

const pistonData = objects.get("piston");
const cylinderData = objects.get("cylinder");

if (pistonData && cylinderData) {
const body1 = pistonData.rigidBody;
const body2 = cylinderData.rigidBody;

const jointDesc = rapier.JointDesc.prismatic(
{ x: 0.0, y: 0.0, z: 0.0 }, // anchor on body1
{ x: 0.0, y: 0.0, z: 0.0 }, // anchor on body2
{ x: 0.0, y: 1.0, z: 0.0 } // slide axis
);

// Set slide limits
jointDesc.setLimits(0.0, 2.0); // Can slide 2 units up

const joint = world.createImpulseJoint(jointDesc, body1, body2, true);
joints.set("pistonSlider", joint);
}

Best Practices

✅ Object Registration

Always register physics objects for easy management:

objects.set("uniqueId", {
rigidBody: body,
collider: collider,
mesh: threeMesh, // if you have a corresponding 3D mesh
type: "dynamic", // or "static", "kinematic"
tag: "player" // custom tag for identification
});

✅ Use Descriptive IDs

Use unique, descriptive IDs for easy management:

objects.set("player_character", {
rigidBody: playerBody,
collider: playerCollider,
type: "dynamic",
tag: "player"
});

objects.set("enemy_01", {
rigidBody: enemyBody,
collider: enemyCollider,
type: "dynamic",
tag: "enemy"
});

✅ Memory Management

Clean up physics objects when removing them:

const objectData = objects.get("objectId");
if (objectData) {
// Remove from world first
if (objectData.rigidBody) {
world.removeRigidBody(objectData.rigidBody);
}
if (objectData.collider) {
world.removeCollider(objectData.collider);
}

// Remove from registry
objects.delete("objectId");
}

✅ Efficient Collision Handling

Process collision events efficiently:

function processCollisions() {
eventQueue.drainCollisionEvents((handle1, handle2, started) => {
// Only process collision starts to avoid duplicates
if (started) {
handleCollision(handle1, handle2);
}
});
}

Important Notes

⚠️ Context Availability

The Rapier context is only available after physics initialization. Always check:

if (window.MakeIt3DContext && window.MakeIt3DContext.rapierContext) {
const { world, objects } = window.MakeIt3DContext.rapierContext;
// Your code here
}

⚠️ World Stepping

The physics world step is handled automatically by the plugin using pluginUtils.UpdateWorldStep(). Avoid calling world.step() manually unless you need custom timing.

⚠️ Coordinate System

Rapier uses a right-handed coordinate system where:

  • X: Right
  • Y: Up
  • Z: Forward (towards viewer)

⚠️ Units and Scale

Rapier works best with objects sized around 0.1 to 10 units. Avoid very large or very small objects for stability.

Advanced Examples

Custom Physics Material

const { rapier, world } = window.MakeIt3DContext.rapierContext;

// Create collider with custom physics properties
const colliderDesc = rapier.ColliderDesc.cuboid(1.0, 1.0, 1.0)
.setDensity(2.0) // Heavy object
.setFriction(0.9) // High friction
.setRestitution(0.8) // Bouncy
.setFrictionCombineRule(rapier.CoefficientCombineRule.Max)
.setRestitutionCombineRule(rapier.CoefficientCombineRule.Max);

const collider = world.createCollider(colliderDesc, body);

Sensor/Trigger Areas

const { rapier, world, objects } = window.MakeIt3DContext.rapierContext;

// Create a sensor that detects but doesn't collide
const sensorDesc = rapier.ColliderDesc.cuboid(2.0, 2.0, 2.0)
.setSensor(true); // This makes it a sensor

const sensor = world.createCollider(sensorDesc, sensorBody);

// Register the sensor
objects.set("triggerZone", {
rigidBody: sensorBody,
collider: sensor,
type: "sensor",
tag: "trigger"
});

// Handle sensor events
eventQueue.drainCollisionEvents((handle1, handle2, started) => {
const collider1 = world.getCollider(handle1);
const collider2 = world.getCollider(handle2);

if (collider1.isSensor() || collider2.isSensor()) {
if (started) {
console.log("Object entered sensor zone");
} else {
console.log("Object left sensor zone");
}
}
});

Physics-Based Vehicles

class SimpleVehicle {
constructor(chassisBody, wheelPositions) {
this.chassis = chassisBody;
this.wheels = [];

// Create wheels and attach them
wheelPositions.forEach((pos, index) => {
const wheelBody = this.createWheel(pos);
const joint = this.attachWheel(wheelBody, pos);

this.wheels.push({ body: wheelBody, joint: joint });
});
}

createWheel(position) {
const wheelBodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(position.x, position.y, position.z);
const wheelBody = world.createRigidBody(wheelBodyDesc);

const wheelColliderDesc = RAPIER.ColliderDesc.ball(0.3)
.setFriction(0.8);
world.createCollider(wheelColliderDesc, wheelBody);

return wheelBody;
}

attachWheel(wheelBody, position) {
const jointDesc = RAPIER.JointDesc.revolute(
position, // anchor on chassis
{ x: 0.0, y: 0.0, z: 0.0 }, // anchor on wheel
{ x: 0.0, y: 0.0, z: 1.0 } // rotation axis
);

return world.createImpulseJoint(jointDesc, this.chassis, wheelBody, true);
}

applyMotorTorque(torque) {
this.wheels.forEach(wheel => {
wheel.body.applyTorqueImpulse({ x: 0.0, y: 0.0, z: torque }, true);
});
}
}