Add realistic physics simulation with rigidbodies, colliders, and forces
Learn how to add realistic physics simulation to your Needle Engine projects using the built-in Rapier physics engine.
:::tip When to Use Physics
Realistic object movement and interactions
Gravity and falling objects
Collision detection and response
Force-based movement (explosions, wind, throwing)
Trigger zones for gameplay mechanics :::
Quick Start
Add physics to an object:
import{ Behaviour, Rigidbody }from"@needle-tools/engine";exportclassPhysicsExampleextendsBehaviour{start(){ // Add a rigidbody component at runtimeconstrb=this.gameObject.addNewComponent(Rigidbody); // Configure physics propertiesrb.mass =10;rb.drag =0.5;rb.angularDrag =0.5;console.log("Physics enabled!");}}
:::tip Unity/Blender Setup In most cases, you'll add Rigidbody and Collider components directly in Unity or Blender, not in code. The exported glTF will include these physics components automatically. :::
Rigidbodies
Rigidbodies add physics simulation to objects. They respond to forces, gravity, and collisions.
Basic Rigidbody
Rigidbody Properties
Property
Type
Description
mass
number
Mass in kilograms (default: 1)
drag
number
Linear drag (air resistance)
angularDrag
number
Rotational drag
useGravity
boolean
Apply gravity force
isKinematic
boolean
Controlled by code, not physics
lockRotation
boolean
Prevent rotation from physics
Apply Forces
Move rigidbodies using physics forces:
Impulse Force (Instant)
Continuous Force
Torque (Rotation)
Set Velocity Directly
Control rigidbody velocity:
Kinematic Rigidbodies
Kinematic rigidbodies are controlled by code, not physics:
:::tip Kinematic Use Cases
Moving platforms
Elevators
Doors
Character controllers
Anything that moves predictably but should affect other physics objects :::
:::tip Layer Configuration Layer collision matrices are usually configured in Unity's Physics Settings or Blender's physics properties, not in code. :::
Physics Materials
Control friction and bounciness:
Character Controller Example
Complete physics-based character:
Performance Tips
Reduce Physics Updates
Use Sleep
Physics objects automatically sleep when stationary. Don't wake them unnecessarily:
Simplify Colliders
Use primitive colliders (box, sphere, capsule) instead of mesh colliders
Mesh colliders are much slower, use sparingly
For complex shapes, combine multiple primitive colliders
Common Issues
Object Falls Through Floor
Problem: Fast-moving objects pass through thin colliders
Solution: Enable Continuous Collision Detection (CCD) in Unity/Blender, or use thicker colliders
import { Behaviour, Rigidbody } from "@needle-tools/engine";
export class Ball extends Behaviour {
private rigidbody?: Rigidbody;
awake() {
// Get the rigidbody component
this.rigidbody = this.gameObject.getComponent(Rigidbody);
}
start() {
if (this.rigidbody) {
// Configure mass and drag
this.rigidbody.mass = 5;
this.rigidbody.drag = 0.1;
this.rigidbody.angularDrag = 0.05;
// Enable/disable gravity
this.rigidbody.useGravity = true;
}
}
}
import { Behaviour, Rigidbody, serializable } from "@needle-tools/engine";
import { Vector3 } from "three";
export class Launcher extends Behaviour {
@serializable()
launchForce: number = 10;
private rigidbody?: Rigidbody;
awake() {
this.rigidbody = this.gameObject.getComponent(Rigidbody);
}
launch() {
if (this.rigidbody) {
// Apply instant force upward
const force = new Vector3(0, this.launchForce, 0);
this.rigidbody.applyImpulse(force);
console.log("Object launched!");
}
}
}
import { Behaviour, Rigidbody } from "@needle-tools/engine";
import { Vector3 } from "three";
export class Thruster extends Behaviour {
private rigidbody?: Rigidbody;
private thrust = 5;
awake() {
this.rigidbody = this.gameObject.getComponent(Rigidbody);
}
update() {
if (this.rigidbody) {
// Apply continuous upward force
const force = new Vector3(0, this.thrust, 0);
this.rigidbody.applyForce(force);
}
}
}
import { Behaviour, Rigidbody } from "@needle-tools/engine";
import { Vector3 } from "three";
export class Spinner extends Behaviour {
private rigidbody?: Rigidbody;
awake() {
this.rigidbody = this.gameObject.getComponent(Rigidbody);
}
start() {
if (this.rigidbody) {
// Apply rotational impulse around Y axis
const torque = new Vector3(0, 10, 0);
this.rigidbody.applyTorque(torque);
}
}
}
import { Behaviour, Rigidbody } from "@needle-tools/engine";
import { Vector3 } from "three";
export class DirectMovement extends Behaviour {
private rigidbody?: Rigidbody;
awake() {
this.rigidbody = this.gameObject.getComponent(Rigidbody);
}
update() {
if (this.rigidbody) {
// Set velocity directly
const velocity = new Vector3(0, 0, 5); // Move forward at 5 m/s
this.rigidbody.setVelocity(velocity);
// Set angular velocity (rotation)
const angularVel = new Vector3(0, 1, 0); // Spin around Y axis
this.rigidbody.setAngularVelocity(angularVel);
}
}
getSpeed(): number {
// Get current speed
return this.rigidbody?.getVelocity().length() ?? 0;
}
}
import { Behaviour, Rigidbody } from "@needle-tools/engine";
import { Vector3 } from "three";
export class Platform extends Behaviour {
private rigidbody?: Rigidbody;
private startPos = new Vector3();
awake() {
this.rigidbody = this.gameObject.getComponent(Rigidbody);
this.startPos.copy(this.gameObject.position);
}
start() {
if (this.rigidbody) {
// Make kinematic - controlled by code
this.rigidbody.isKinematic = true;
}
}
update() {
// Move platform with code
const time = this.context.time.time;
this.gameObject.position.y = this.startPos.y + Math.sin(time) * 2;
// Kinematic rigidbodies still affect dynamic objects!
// Objects will ride on this platform
}
}
import { Behaviour, Collision, serializable } from "@needle-tools/engine";
export class Breakable extends Behaviour {
@serializable()
breakForce: number = 10;
onCollisionEnter(col: Collision) {
console.log("Hit by:", col.collider.gameObject.name);
// Calculate impact force (approximate)
const impact = this.estimateImpact(col);
if (impact > this.breakForce) {
console.log("Object broke from impact!");
this.destroy();
}
}
private estimateImpact(col: Collision): number {
// Get relative velocity (simplified)
return 5; // In real code, calculate from velocities
}
}
import { Behaviour, Collision, serializable } from "@needle-tools/engine";
import { Object3D } from "three";
export class Checkpoint extends Behaviour {
@serializable()
nextCheckpoint?: Object3D;
private activated = false;
onTriggerEnter(col: Collision) {
if (this.activated) return;
// Check if player entered
const player = col.collider.gameObject.getComponent(PlayerController);
if (player) {
this.activated = true;
console.log("Checkpoint reached!");
this.onCheckpointReached(player);
}
}
private onCheckpointReached(player: PlayerController) {
// Handle checkpoint logic
player.setSpawnPoint(this.gameObject.position);
}
}
import { Behaviour, RaycastOptions } from "@needle-tools/engine";
import { Vector3 } from "three";
export class Scanner extends Behaviour {
update() {
const origin = this.gameObject.position;
const direction = this.gameObject.forward;
// Raycast using physics engine (tests against colliders)
const options: RaycastOptions = {
maxDistance: 10,
precise: true, // Use collider shapes
};
const hits = this.context.physics.engine.raycast(origin, direction, options);
if (hits.length > 0) {
console.log("Found object:", hits[0].object.name);
console.log("Distance:", hits[0].distance);
}
}
}
import { Behaviour, Rigidbody } from "@needle-tools/engine";
export class LayeredObject extends Behaviour {
awake() {
// Set object layer
this.gameObject.layers.set(1); // Layer 1
}
start() {
const rb = this.gameObject.getComponent(Rigidbody);
if (rb) {
// Configure which layers this rigidbody collides with
// (This is typically done in Unity/Blender physics settings)
}
}
}
import { Behaviour, PhysicsMaterial } from "@needle-tools/engine";
export class BouncyBall extends Behaviour {
start() {
// Physics materials are typically set in Unity/Blender
// But you can modify them at runtime:
const collider = this.gameObject.getComponent(BoxCollider);
if (collider?.physicsMaterial) {
collider.physicsMaterial.bounciness = 0.9; // Very bouncy
collider.physicsMaterial.friction = 0.1; // Low friction
}
}
}
import { Behaviour, Rigidbody, serializable } from "@needle-tools/engine";
import { Vector3 } from "three";
export class PhysicsCharacter extends Behaviour {
@serializable()
moveSpeed: number = 5;
@serializable()
jumpForce: number = 8;
private rigidbody?: Rigidbody;
private isGrounded = false;
awake() {
this.rigidbody = this.gameObject.getComponent(Rigidbody);
}
start() {
if (this.rigidbody) {
// Configure for character
this.rigidbody.lockRotation = true; // Don't fall over
this.rigidbody.drag = 2; // Stop quickly
}
}
update() {
if (!this.rigidbody) return;
const input = this.context.input;
const velocity = this.rigidbody.getVelocity();
// Horizontal movement
const moveX = (input.getKey("d") ? 1 : 0) - (input.getKey("a") ? 1 : 0);
const moveZ = (input.getKey("s") ? 1 : 0) - (input.getKey("w") ? 1 : 0);
velocity.x = moveX * this.moveSpeed;
velocity.z = moveZ * this.moveSpeed;
// Jump
if (input.getKeyDown("space") && this.isGrounded) {
velocity.y = this.jumpForce;
}
this.rigidbody.setVelocity(velocity);
// Check if grounded (simplified)
this.checkGrounded();
}
private checkGrounded() {
// Use raycast to check ground
const origin = this.gameObject.position.clone();
origin.y += 0.1;
const direction = new Vector3(0, -1, 0);
const hits = this.context.physics.raycast(origin, direction, {
maxDistance: 0.2
});
this.isGrounded = hits.length > 0;
}
}
// Don't do this every frame:
❌ this.gameObject.getComponent(Rigidbody);
// Cache it once:
✅ awake() {
this.rigidbody = this.gameObject.getComponent(Rigidbody);
}
// This wakes the rigidbody:
rigidbody.setVelocity(new Vector3(0, 0, 0));
// This keeps it asleep:
if (rigidbody.getVelocity().length() > 0.1) {
rigidbody.setVelocity(new Vector3(0, 0, 0));
}