/** * Options of the `rigidbody` component accepted by {@link RigidBodyComponentSystem} that differ * from the properties of {@link RigidBodyComponent}. Each replaces the same-named property of the * options that {@link Entity#addComponent} derives from the component class; see * {@link ComponentOptionsOverrides}. */ export type RigidBodyComponentOptionsOverrides = { /** * - Same as {@link RigidBodyComponent#angularFactor}, * also accepting an `[x, y, z]` array. */ angularFactor?: Vec3 | number[]; /** * - Same as {@link RigidBodyComponent#linearFactor}, * also accepting an `[x, y, z]` array. */ linearFactor?: Vec3 | number[]; }; /** * The RigidBodyComponentSystem manages the physics simulation for all rigid body components * in the application and is accessed as `app.systems.rigidbody`. It owns the physics world, * creates and destroys the bodies behind rigid body and collision components, steps the * simulation once per frame and writes the resulting transforms back to their entities. It also * holds global settings such as {@link RigidBodyComponentSystem#gravity}, performs raycasts * and reports collisions. * * The system is only functional once a physics backend is installed: either by supplying * {@link AppOptions#physicsWorld} when creating the application, or automatically when the * application has loaded the Ammo.js {@link WasmModule}. Use a recent Ammo.js build: mesh * colliders only follow entity scale with a build that exposes `btScaledBvhTriangleMeshShape`. * * Set {@link RigidBodyComponentSystem#timeScale} to slow the simulation down, speed it up or * pause it, for example while a pause menu is open, and call * {@link RigidBodyComponentSystem#step} to advance it manually. * * @category Physics */ export class RigidBodyComponentSystem extends ComponentSystem { /** * Fired when a contact occurs between two rigid bodies. The handler is passed a * {@link SingleContactResult} object containing details of the contact between the two bodies. * * @event * @example * app.systems.rigidbody.on('contact', (result) => { * console.log(`Contact between ${result.a.name} and ${result.b.name}`); * }); */ static EVENT_CONTACT: string; /** @ignore */ maxSubSteps: number; /** * @type {number} * @ignore */ fixedTimeStep: number; /** * Scales the time the simulation is advanced by each frame. Defaults to 1. Values below 1 * run physics in slow motion and values above 1 speed it up. 0 pauses the simulation: the * system stops advancing it, bodies freeze in place, entity transforms are no longer driven * by their bodies and no contact or trigger events fire. The rest of the application keeps * running, so this suits a pause menu or inventory screen that must stay interactive while * the game world stands still. Negative values are treated as 0. * * This scale is applied on top of {@link AppBase#timeScale}. The simulation can still be * advanced manually with {@link RigidBodyComponentSystem#step} while paused, for example to * drive it from a custom time source. * * How slow motion below one fixed substep per frame looks depends on the backend: the Ammo * backend interpolates body transforms between substeps so motion stays smooth, while other * backends may only move bodies on the frames in which a substep runs. Fast forward is * limited by the maximum number of substeps the simulation may take per frame, beyond which * it runs slower than requested. * * Forces applied with {@link RigidBodyComponent#applyForce} while paused accumulate on the * body and are applied together on the next step, because forces are only cleared when the * simulation steps. Impulses and velocity changes take effect immediately. * * @example * // Freeze the game world while the pause menu is open * app.systems.rigidbody.timeScale = 0; * @example * // Run physics at quarter speed for a slow motion effect * app.systems.rigidbody.timeScale = 0.25; */ timeScale: number; /** * The world space vector representing global gravity in the physics simulation. Defaults to * [0, -9.81, 0] which is an approximation of the gravitational force on Earth. * * The value is applied to the physics backend at the start of the next step, whether the * vector is modified in place or replaced with a new one. * * @example * // Set the gravity in the physics world to simulate a planet with low gravity * app.systems.rigidbody.gravity = new Vec3(0, -3.7, 0); */ gravity: Vec3; /** * The gravity most recently applied to the physics backend. Compared against gravity each * step so the backend is only updated when the value changes. * * @type {Vec3} * @private */ private _appliedGravity; /** * @type {PhysicsWorld|null} * @private */ private _world; /** * @type {RigidBodyComponent[]} * @private */ private _dynamic; /** * @type {RigidBodyComponent[]} * @private */ private _kinematic; /** * @type {Trigger[]} * @private */ private _triggers; /** * @type {CollisionComponent[]} * @private */ private _compounds; /** * The contact listener installed on the physics backend. It forwards each contact pass to * this system, which keeps the listener methods private. * * @type {PhysicsContactListener} * @private */ private _contactListener; /** * The frame stats that record the duration of each physics step. * * @private */ private _stats; /** * @type {ObjectPool|null} * @private */ private contactPointPool; /** * @type {ObjectPool|null} * @private */ private contactResultPool; /** * @type {ObjectPool|null} * @private */ private singleContactResultPool; /** * The entities touched by each entity with contact or trigger events as of the last contact * pass, keyed by the GUID of the entity. * * @type {Object} * @private */ private collisions; /** * The entities touched by each entity in the contact pass in progress, keyed like * collisions. * * @type {Object} * @private */ private frameCollisions; id: string; ComponentType: typeof RigidBodyComponent; /** * Called once application libraries have loaded. Creates the Ammo backend when the Ammo * global is present and no backend was injected via {@link AppOptions#physicsWorld}. * * @ignore */ onLibraryLoaded(): void; /** * Installs a physics backend, applies the current gravity to it and registers the system's * contact listener with it. Called by * {@link AppBase#init} when {@link AppOptions#physicsWorld} is supplied, and internally by * Ammo auto-detection. A backend can be installed at most once. * * @param {PhysicsWorld} world - The physics backend. * @ignore */ setPhysicsWorld(world: PhysicsWorld): void; /** * Gets the installed physics backend, or null when no backend is installed. Supply a * backend via {@link AppOptions#physicsWorld}, or load the Ammo.js library to have one * installed automatically. * * @type {PhysicsWorld|null} * @alpha */ get physicsWorld(): PhysicsWorld | null; /** * The physics backend's native world - a btDiscreteDynamicsWorld with the Ammo backend - or * null if no backend is installed or it has no native world. Same as * {@link PhysicsWorld#nativeWorld}. An unsupported escape hatch for native functionality the * engine does not expose: code that uses it only works with that physics backend. * * @type {*} * @ignore */ get dynamicsWorld(): any; /** * The Ammo backend's native btDefaultCollisionConfiguration, or null with any other backend * or none. An unsupported escape hatch: code that uses it only works with the Ammo backend. * * @type {*} * @ignore */ get collisionConfiguration(): any; /** * The Ammo backend's native btCollisionDispatcher, or null with any other backend or none. * An unsupported escape hatch: code that uses it only works with the Ammo backend. * * @type {*} * @ignore */ get dispatcher(): any; /** * The Ammo backend's native btDbvtBroadphase, or null with any other backend or none. An * unsupported escape hatch: code that uses it only works with the Ammo backend. * * @type {*} * @ignore */ get overlappingPairCache(): any; /** * The Ammo backend's native btSequentialImpulseConstraintSolver, or null with any other * backend or none. An unsupported escape hatch: code that uses it only works with the Ammo * backend. * * @type {*} * @ignore */ get solver(): any; initializeComponentData(component: any, data: any): void; cloneComponent(entity: any, clone: any): import("../component.js").Component; /** * Disables a component that is being removed and destroys its body. * * @param {Entity} entity - The entity the component is being removed from. * @param {RigidBodyComponent} component - The component being removed. * @private */ private onBeforeRemove; /** * Called once the component is gone from its entity. A collision component left behind * supplied the body's shape; without a body it is a trigger volume, or a child of an * enclosing compound, so it is rebuilt into that role. The pairs the body was touching are * forgotten first: they belong to the old role, and a trigger built over an overlap that is * still in progress has to report it as new. Nothing is rebuilt while the entity itself is * being destroyed, since the collision component is about to go as well. * * @param {Entity} entity - The entity the component was removed from. * @private */ private onRemove; /** * Adds a body to the simulation with the given collision group and mask. * * @param {PhysicsBody} body - The body to add. * @param {number} group - The collision group bits. * @param {number} mask - The collision mask bits. * @private */ private addBody; /** * Removes a body from the simulation. * * @param {PhysicsBody} body - The body to remove. * @private */ private removeBody; /** * Adds a component's body to the simulation and registers the component with the update * lists for its body type. Fires 'simulationenabled' on the component. No-op unless the * component has a body, an enabled collision component and is not already simulating. * * @param {RigidBodyComponent} component - The component to add to the simulation. * @ignore */ enableSimulation(component: RigidBodyComponent): void; /** * Removes a component's body from the simulation and unregisters the component from the * update lists. Fires 'simulationdisabled' on the component. No-op unless the component * has a body and is currently simulating. * * @param {RigidBodyComponent} component - The component to remove from the simulation. * @ignore */ disableSimulation(component: RigidBodyComponent): void; /** * Adds a trigger's body to the simulation and registers the trigger for per-frame * transform updates. No-op if the trigger is already registered. * * @param {Trigger} trigger - The trigger to add to the simulation. * @ignore */ addTrigger(trigger: Trigger): void; /** * Removes a trigger's body from the simulation and unregisters the trigger. No-op if the * trigger is not registered. * * @param {Trigger} trigger - The trigger to remove from the simulation. * @ignore */ removeTrigger(trigger: Trigger): void; /** * Raycast the world and return the first entity the ray hits. Fire a ray into the world from * start to end, if the ray hits an entity with a collision component, it returns a * {@link RaycastResult}, otherwise returns null. * * @param {Vec3} start - The world space point where the ray starts. * @param {Vec3} end - The world space point where the ray ends. * @param {object} [options] - The additional options for the raycasting. * @param {number} [options.filterCollisionGroup] - Collision group to apply to the raycast. * @param {number} [options.filterCollisionMask] - Collision mask to apply to the raycast. * @param {boolean} [options.hitBackFaces] - Whether the ray can hit the back faces of mesh * colliders, which face away from the ray: the far side of a closed mesh, or the first surface * met by a ray starting inside one. A back-face hit reports a normal flipped to face the start * of the ray. Other collision shapes never report back-face hits. Defaults to true. * @param {any[]} [options.filterTags] - Tags filters. Defined the same way as a {@link Tags#has} * query but within an array. * @param {Function} [options.filterCallback] - Custom function to use to filter entities. * Must return true to proceed with result. Takes one argument: the entity to evaluate. * * @returns {RaycastResult|null} The result of the raycasting, or null if there was no hit or * no physics backend is installed. */ raycastFirst(start: Vec3, end: Vec3, options?: { filterCollisionGroup?: number; filterCollisionMask?: number; hitBackFaces?: boolean; filterTags?: any[]; filterCallback?: Function; }): RaycastResult | null; /** * Raycast the world and return all entities the ray hits. It returns an array of * {@link RaycastResult}, one for each hit. If no hits are detected, the returned array will be * of length 0. Results are returned in no particular order unless `options.sort` is true, in * which case they are sorted by distance with the closest first. * * @param {Vec3} start - The world space point where the ray starts. * @param {Vec3} end - The world space point where the ray ends. * @param {object} [options] - The additional options for the raycasting. * @param {boolean} [options.sort] - Whether to sort raycast results based on distance with closest * first. Defaults to false. * @param {number} [options.filterCollisionGroup] - Collision group to apply to the raycast. * @param {number} [options.filterCollisionMask] - Collision mask to apply to the raycast. * @param {boolean} [options.hitBackFaces] - Whether the ray can hit the back faces of mesh * colliders, which face away from the ray: the far side of a closed mesh, or the first surface * met by a ray starting inside one. A back-face hit reports a normal flipped to face the start * of the ray. Other collision shapes never report back-face hits. Defaults to true. * @param {any[]} [options.filterTags] - Tags filters. Defined the same way as a {@link Tags#has} * query but within an array. * @param {Function} [options.filterCallback] - Custom function to use to filter entities. * Must return true to proceed with result. Takes the entity to evaluate as argument. * * @returns {RaycastResult[]} An array of raycast hit results (0 length if there were no hits * or no physics backend is installed). * * @example * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 * const hits = this.app.systems.rigidbody.raycastAll(new Vec3(0, 2, 2), new Vec3(0, -2, -2)); * @example * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 * // where hit entity is tagged with `bird` OR `mammal` * const hits = this.app.systems.rigidbody.raycastAll(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { * filterTags: [ "bird", "mammal" ] * }); * @example * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 * // where hit entity has a `camera` component * const hits = this.app.systems.rigidbody.raycastAll(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { * filterCallback: (entity) => entity && entity.camera * }); * @example * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2, skipping the back faces * // of mesh colliders so a ray through a closed mesh hits it only where it enters * const hits = this.app.systems.rigidbody.raycastAll(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { * hitBackFaces: false * }); * @example * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 * // where hit entity is tagged with (`carnivore` AND `mammal`) OR (`carnivore` AND `reptile`) * // and the entity has an `anim` component * const hits = this.app.systems.rigidbody.raycastAll(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { * filterTags: [ * [ "carnivore", "mammal" ], * [ "carnivore", "reptile" ] * ], * filterCallback: (entity) => entity && entity.anim * }); */ raycastAll(start: Vec3, end: Vec3, options?: { sort?: boolean; filterCollisionGroup?: number; filterCollisionMask?: number; hitBackFaces?: boolean; filterTags?: any[]; filterCallback?: Function; }): RaycastResult[]; /** * Stores a collision between the entity and other in the contacts map and returns true if it * is a new collision. * * @param {Entity} entity - The entity. * @param {Entity} other - The entity that collides with the first entity. * @returns {boolean} True if this is a new collision, false otherwise. * @private */ private _storeCollision; /** * Allocates a pooled contact point that is the given one seen from the other body's * perspective: the points swap sides and the normal flips, so that it points away from body * A's surface just as the forward normal points away from body B's. * * @param {ContactPoint} forward - The contact point from body A's perspective. * @returns {ContactPoint} The reversed contact point. * @private */ private _createReverseContactPoint; /** * Allocates a pooled result for the global contact event from a contact point. * * @param {Entity} a - The first entity involved in the contact. * @param {Entity} b - The second entity involved in the contact. * @param {ContactPoint} contactPoint - The contact point, from the first entity's perspective. * @returns {SingleContactResult} The result. * @private */ private _createSingleContactResult; /** * Allocates a pooled result for the contact events of one entity. * * @param {Entity} other - The other entity involved in the contact. * @param {ContactPoint[]} contacts - The contact points, from the entity's perspective. * @returns {ContactResult} The result. * @private */ private _createContactResult; /** * Removes collisions that no longer exist from the collisions list and fires collisionend * events to the related entities. * * @private */ private _cleanOldCollisions; /** * Removes any stored collision keyed to the given entity. Called when a collision component is * removed so the persistent collisions map does not retain a destroyed entity. A new entity * that later reuses the same GUID (for example after reloading the same scene) would otherwise * inherit the stale entry and never fire `triggerleave` / `collisionend`, because the cached * entity no longer has a trigger or body. * * @param {Entity} entity - The entity whose stored collision should be removed. * @ignore */ clearEntityCollisions(entity: Entity): void; /** * Returns true if the entity has a contact event attached and false otherwise. * * @param {Entity} entity - Entity to test. * @returns {boolean} True if the entity has a contact and false otherwise. * @private */ private _hasContactEvent; /** * Called through the contact listener when the physics backend begins a contact pass. * * @private */ private onContactsBegin; /** * Called through the contact listener for each contacting pair the physics backend reports. * Fires the trigger and collision events. * * @param {PhysicsContactPair} pair - The contacting pair. Only valid during the call. * @private */ private onContactPair; /** * Called through the contact listener when the physics backend ends a contact pass. Fires * collisionend/triggerleave events for lost contacts and frees the pooled results. * * @private */ private onContactsEnd; /** * Advances the physics simulation by dt seconds. Synchronizes triggers, compound shapes and * kinematic bodies from their entities, steps the backend in fixed-length substeps (up to a * maximum number per call), writes the resulting transforms of dynamic bodies back to their * entities and fires contact and trigger events. * * The system calls this once per frame with the frame delta time multiplied by * {@link RigidBodyComponentSystem#timeScale}, unless that is 0. Call it directly to step the * simulation manually: to advance it while paused, to fast forward it by stepping several * times in one frame, or to drive it from a custom time source. Automatic stepping continues * while timeScale is above 0, so calling this every frame as well advances the simulation * twice per frame. Set timeScale to 0 first when taking over stepping entirely. The delta is * used as given, without applying timeScale. Does nothing when no physics backend is * installed. * * @param {number} dt - The amount of time to advance the simulation by, in seconds. * @example * // Pause automatic stepping and advance the simulation by 1/60 s per key press * const physics = app.systems.rigidbody; * physics.timeScale = 0; * app.keyboard.on('keydown', (event) => { * if (event.key === KEY_SPACE) { * physics.step(1 / 60); * } * }); */ step(dt: number): void; /** * Steps the simulation by the frame delta time scaled by * {@link RigidBodyComponentSystem#timeScale}, or skips the frame entirely when the scale is * 0. Registered on the application's update event when a physics backend is installed. * * @param {number} dt - The frame delta time in seconds. * @private */ private onUpdate; /** * Sets the world space gravity. Accepts either a Vec3 or three numbers. * * @param {number|Vec3} x - A Vec3 holding the gravity, or the x-component of the gravity. * @param {number} [y] - The y-component of the gravity. * @param {number} [z] - The z-component of the gravity. * @ignore * @deprecated Use {@link RigidBodyComponentSystem#gravity} instead. */ setGravity(x: number | Vec3, y?: number, z?: number): void; } import { Vec3 } from '../../../core/math/vec3.js'; import { ComponentSystem } from '../system.js'; import { RigidBodyComponent } from './component.js'; import type { PhysicsWorld } from '../../physics/physics-world.js'; import type { Trigger } from '../collision/trigger.js'; import type { RaycastResult } from './raycast-result.js'; import type { Entity } from '../../entity.js';