planck
Version:
2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development
1,195 lines (1,018 loc) • 35.1 kB
text/typescript
/*
* Planck.js
*
* Copyright (c) Erin Catto, Ali Shakiba
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { options } from "../util/options";
import { Vec2, Vec2Value } from "../common/Vec2";
import { BroadPhase } from "../collision/BroadPhase";
import { Solver, ContactImpulse, TimeStep } from "./Solver";
import { Body, BodyDef } from "./Body";
import { Joint } from "./Joint";
import { Contact } from "./Contact";
import { AABBValue, RayCastInput, RayCastOutput } from "../collision/AABB";
import { Fixture, FixtureProxy } from "./Fixture";
import { Manifold } from "../collision/Manifold";
/** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT;
/** @internal */ const _CONSTRUCTOR_FACTORY = typeof CONSTRUCTOR_FACTORY === "undefined" ? false : CONSTRUCTOR_FACTORY;
export interface WorldDef {
/** [default: { x : 0, y : 0}] */
gravity?: Vec2Value;
/** [default: true] */
allowSleep?: boolean;
/** [default: true] */
warmStarting?: boolean;
/** [default: true] */
continuousPhysics?: boolean;
/** [default: false] */
subStepping?: boolean;
/** [default: true] */
blockSolve?: boolean;
/** @internal [8] For the velocity constraint solver. */
velocityIterations?: number;
/** @internal [3] For the position constraint solver. */
positionIterations?: number;
}
/** @internal */ const DEFAULTS: WorldDef = {
gravity: Vec2.zero(),
allowSleep: true,
warmStarting: true,
continuousPhysics: true,
subStepping: false,
blockSolve: true,
velocityIterations: 8,
positionIterations: 3,
};
/**
* Callback function for ray casts, see {@link World.rayCast}.
*
* Called for each fixture found in the query.
* The returned value replaces the ray-cast input maxFraction.
* You control how the ray cast proceeds by returning a numeric/float value.
*
* - `0` to terminate the ray cast
* - `fraction` to clip the ray cast at current point
* - `1` don't clip the ray and continue
* - `-1` (or anything else) to continue
*
* @param fixture The fixture hit by the ray
* @param point The point of initial intersection
* @param normal The normal vector at the point of intersection
* @param fraction The fraction along the ray at the point of intersection
*
* @returns A number to update the maxFraction
*/
export type WorldRayCastCallback = (fixture: Fixture, point: Vec2, normal: Vec2, fraction: number) => number;
/**
* Called for each fixture found in the query AABB. It may return `false` to terminate the query.
*/
export type WorldAABBQueryCallback = (fixture: Fixture) => boolean;
declare module "./World" {
/** @hidden @deprecated Use new keyword. */
// @ts-expect-error
function World(deg: WorldDef): World;
/** @hidden @deprecated Use new keyword. */
// @ts-expect-error
function World(gravity: Vec2): World;
/** @hidden @deprecated Use new keyword. */
// @ts-expect-error
function World(): World;
}
/**
* The `World` class contains the bodies and joints. It manages all aspects
* of the simulation and allows for asynchronous queries (like AABB queries
* and ray-casts). Much of your interactions with Planck.js will be with a
* World object.
*/
// @ts-expect-error
export class World {
/** @internal */ m_solver: Solver;
/** @internal */ m_broadPhase: BroadPhase;
/** @internal */ m_contactList: Contact | null;
/** @internal */ m_contactCount: number;
/** @internal */ m_bodyList: Body | null;
/** @internal */ m_bodyCount: number;
/** @internal */ m_jointList: Joint | null;
/** @internal */ m_jointCount: number;
/** @internal */ m_stepComplete: boolean;
/** @internal */ m_allowSleep: boolean;
/** @internal */ m_gravity: Vec2;
/** @internal */ m_clearForces: boolean;
/** @internal */ m_newFixture: boolean;
/** @internal */ m_locked: boolean;
/** @internal */ m_warmStarting: boolean;
/** @internal */ m_continuousPhysics: boolean;
/** @internal */ m_subStepping: boolean;
/** @internal */ m_blockSolve: boolean;
/** @internal */ m_velocityIterations: number;
/** @internal */ m_positionIterations: number;
/** @internal */ m_t: number;
/** @internal */ m_step_callback: ((world: World) => unknown)[];
// TODO
/** @internal */ _listeners: {
[key: string]: any[];
};
/**
* @param def World definition or gravity vector.
*/
constructor(def?: WorldDef | Vec2Value) {
if (_CONSTRUCTOR_FACTORY && !(this instanceof World)) {
return new World(def);
}
this.s_step = new TimeStep();
if (!def) {
def = {};
} else if (Vec2.isValid(def)) {
def = { gravity: def as Vec2 };
}
def = options(def, DEFAULTS) as WorldDef;
this.m_solver = new Solver(this);
this.m_broadPhase = new BroadPhase();
this.m_contactList = null;
this.m_contactCount = 0;
this.m_bodyList = null;
this.m_bodyCount = 0;
this.m_jointList = null;
this.m_jointCount = 0;
this.m_stepComplete = true;
this.m_allowSleep = def.allowSleep;
this.m_gravity = Vec2.clone(def.gravity);
this.m_clearForces = true;
this.m_newFixture = false;
this.m_locked = false;
// These are for debugging the solver.
this.m_warmStarting = def.warmStarting;
this.m_continuousPhysics = def.continuousPhysics;
this.m_subStepping = def.subStepping;
this.m_blockSolve = def.blockSolve;
this.m_velocityIterations = def.velocityIterations;
this.m_positionIterations = def.positionIterations;
this.m_t = 0;
this.m_step_callback = [];
}
/** @hidden */
_serialize(): object {
const bodies = [];
const joints = [];
for (let b = this.getBodyList(); b; b = b.getNext()) {
bodies.push(b);
}
for (let j = this.getJointList(); j; j = j.getNext()) {
// @ts-ignore
if (typeof j._serialize === "function") {
joints.push(j);
}
}
return {
gravity: this.m_gravity,
bodies,
joints,
};
}
/** @hidden */
static _deserialize(data: any, context: any, restore: any): World {
if (!data) {
return new World();
}
const world = new World(data.gravity);
if (data.bodies) {
for (let i = data.bodies.length - 1; i >= 0; i -= 1) {
world._addBody(restore(Body, data.bodies[i], world));
}
}
if (data.joints) {
for (let i = data.joints.length - 1; i >= 0; i--) {
world.createJoint(restore(Joint, data.joints[i], world));
}
}
return world;
}
/**
* Get the world body list. With the returned body, use Body.getNext to get the
* next body in the world list. A null body indicates the end of the list.
*
* @return the head of the world body list.
*/
getBodyList(): Body | null {
return this.m_bodyList;
}
/**
* Get the world joint list. With the returned joint, use Joint.getNext to get
* the next joint in the world list. A null joint indicates the end of the list.
*
* @return the head of the world joint list.
*/
getJointList(): Joint | null {
return this.m_jointList;
}
/**
* Get the world contact list. With the returned contact, use Contact.getNext to
* get the next contact in the world list. A null contact indicates the end of
* the list.
*
* Warning: contacts are created and destroyed in the middle of a time step.
* Use ContactListener to avoid missing contacts.
*
* @return the head of the world contact list.
*/
getContactList(): Contact | null {
return this.m_contactList;
}
getBodyCount(): number {
return this.m_bodyCount;
}
getJointCount(): number {
return this.m_jointCount;
}
/**
* Get the number of contacts (each may have 0 or more contact points).
*/
getContactCount(): number {
return this.m_contactCount;
}
/**
* Change the global gravity vector.
*/
setGravity(gravity: Vec2Value): void {
this.m_gravity.set(gravity);
}
/**
* Get the global gravity vector.
*/
getGravity(): Vec2 {
return this.m_gravity;
}
/**
* Is the world locked (in the middle of a time step).
*/
isLocked(): boolean {
return this.m_locked;
}
/**
* Enable/disable sleep.
*/
setAllowSleeping(flag: boolean): void {
if (flag == this.m_allowSleep) {
return;
}
this.m_allowSleep = flag;
if (this.m_allowSleep == false) {
for (let b = this.m_bodyList; b; b = b.m_next) {
b.setAwake(true);
}
}
}
getAllowSleeping(): boolean {
return this.m_allowSleep;
}
/**
* Enable/disable warm starting. For testing.
*/
setWarmStarting(flag: boolean): void {
this.m_warmStarting = flag;
}
getWarmStarting(): boolean {
return this.m_warmStarting;
}
/**
* Enable/disable continuous physics. For testing.
*/
setContinuousPhysics(flag: boolean): void {
this.m_continuousPhysics = flag;
}
getContinuousPhysics(): boolean {
return this.m_continuousPhysics;
}
/**
* Enable/disable single stepped continuous physics. For testing.
*/
setSubStepping(flag: boolean): void {
this.m_subStepping = flag;
}
getSubStepping(): boolean {
return this.m_subStepping;
}
/**
* Set flag to control automatic clearing of forces after each time step.
*/
setAutoClearForces(flag: boolean): void {
this.m_clearForces = flag;
}
/**
* Get the flag that controls automatic clearing of forces after each time step.
*/
getAutoClearForces(): boolean {
return this.m_clearForces;
}
/**
* Manually clear the force buffer on all bodies. By default, forces are cleared
* automatically after each call to step. The default behavior is modified by
* calling setAutoClearForces. The purpose of this function is to support
* sub-stepping. Sub-stepping is often used to maintain a fixed sized time step
* under a variable frame-rate. When you perform sub-stepping you will disable
* auto clearing of forces and instead call clearForces after all sub-steps are
* complete in one pass of your game loop.
*
* See {@link World.setAutoClearForces}
*/
clearForces(): void {
for (let body = this.m_bodyList; body; body = body.getNext()) {
body.m_force.setZero();
body.m_torque = 0.0;
}
}
/**
* Query the world for all fixtures that potentially overlap the provided AABB.
*
* @param aabb The query box.
* @param callback Called for each fixture found in the query AABB. It may return `false` to terminate the query.
*/
queryAABB(aabb: AABBValue, callback: WorldAABBQueryCallback): void {
if (_ASSERT) console.assert(typeof callback === "function");
const broadPhase = this.m_broadPhase;
this.m_broadPhase.query(aabb, function (proxyId: number): boolean {
// TODO GC
const proxy = broadPhase.getUserData(proxyId);
return callback(proxy.fixture);
});
}
/**
* Ray-cast the world for all fixtures in the path of the ray. Your callback
* controls whether you get the closest point, any point, or n-points. The
* ray-cast ignores shapes that contain the starting point.
*
* @param point1 The ray starting point
* @param point2 The ray ending point
* @param callback A function that is called for each fixture that is hit by the ray. You control how the ray cast proceeds by returning a numeric/float value.
*/
rayCast(point1: Vec2Value, point2: Vec2Value, callback: WorldRayCastCallback): void {
if (_ASSERT) console.assert(typeof callback === "function");
const broadPhase = this.m_broadPhase;
this.m_broadPhase.rayCast(
{
maxFraction: 1.0,
p1: point1,
p2: point2,
},
function (input: RayCastInput, proxyId: number): number {
// TODO GC
const proxy = broadPhase.getUserData(proxyId);
const fixture = proxy.fixture;
const index = proxy.childIndex;
// @ts-ignore
const output: RayCastOutput = {}; // TODO GC
const hit = fixture.rayCast(output, input, index);
if (hit) {
const fraction = output.fraction;
const point = Vec2.add(Vec2.mulNumVec2(1.0 - fraction, input.p1), Vec2.mulNumVec2(fraction, input.p2));
return callback(fixture, point, output.normal, fraction);
}
return input.maxFraction;
},
);
}
/**
* Get the number of broad-phase proxies.
*/
getProxyCount(): number {
return this.m_broadPhase.getProxyCount();
}
/**
* Get the height of broad-phase dynamic tree.
*/
getTreeHeight(): number {
return this.m_broadPhase.getTreeHeight();
}
/**
* Get the balance of broad-phase dynamic tree.
*/
getTreeBalance(): number {
return this.m_broadPhase.getTreeBalance();
}
/**
* Get the quality metric of broad-phase dynamic tree. The smaller the better.
* The minimum is 1.
*/
getTreeQuality(): number {
return this.m_broadPhase.getTreeQuality();
}
/**
* Shift the world origin. Useful for large worlds. The body shift formula is:
* position -= newOrigin
*
* @param newOrigin The new origin with respect to the old origin
*
* Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
*/
shiftOrigin(newOrigin: Vec2Value): void {
if (_ASSERT) console.assert(this.isLocked() == false);
if (this.isLocked()) {
return;
}
for (let b = this.m_bodyList; b; b = b.m_next) {
b.m_xf.p.sub(newOrigin);
b.m_sweep.c0.sub(newOrigin);
b.m_sweep.c.sub(newOrigin);
}
for (let j = this.m_jointList; j; j = j.m_next) {
j.shiftOrigin(newOrigin);
}
this.m_broadPhase.shiftOrigin(newOrigin);
}
/** @internal Used for deserialize. */
_addBody(body: Body): void {
if (_ASSERT) console.assert(this.isLocked() === false);
if (this.isLocked()) {
return;
}
// Add to world doubly linked list.
body.m_prev = null;
body.m_next = this.m_bodyList;
if (this.m_bodyList) {
this.m_bodyList.m_prev = body;
}
this.m_bodyList = body;
++this.m_bodyCount;
this.publish("add-body", body);
}
/**
* Create a rigid body given a definition. No reference to the definition is
* retained.
*
* Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
*/
createBody(def?: BodyDef): Body;
createBody(position: Vec2Value, angle?: number): Body;
// tslint:disable-next-line:typedef
createBody(arg1?, arg2?) {
if (_ASSERT) console.assert(this.isLocked() == false);
if (this.isLocked()) {
return null;
}
let def: BodyDef = {};
if (!arg1) {
} else if (Vec2.isValid(arg1)) {
def = { position: arg1, angle: arg2 };
} else if (typeof arg1 === "object") {
def = arg1;
}
const body = new Body(this, def);
this._addBody(body);
return body;
}
createDynamicBody(def?: BodyDef): Body;
createDynamicBody(position: Vec2Value, angle?: number): Body;
// tslint:disable-next-line:typedef
createDynamicBody(arg1?, arg2?) {
let def: BodyDef = {};
if (!arg1) {
} else if (Vec2.isValid(arg1)) {
def = { position: arg1, angle: arg2 };
} else if (typeof arg1 === "object") {
def = arg1;
}
def.type = "dynamic";
return this.createBody(def);
}
createKinematicBody(def?: BodyDef): Body;
createKinematicBody(position: Vec2Value, angle?: number): Body;
// tslint:disable-next-line:typedef
createKinematicBody(arg1?, arg2?) {
let def: BodyDef = {};
if (!arg1) {
} else if (Vec2.isValid(arg1)) {
def = { position: arg1, angle: arg2 };
} else if (typeof arg1 === "object") {
def = arg1;
}
def.type = "kinematic";
return this.createBody(def);
}
/**
* Destroy a body from the world.
*
* Warning: This automatically deletes all associated shapes and joints.
*
* Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
*/
destroyBody(b: Body): boolean {
if (_ASSERT) console.assert(this.m_bodyCount > 0);
if (_ASSERT) console.assert(this.isLocked() == false);
if (this.isLocked()) {
return;
}
if (b.m_destroyed) {
return false;
}
// Delete the attached joints.
let je = b.m_jointList;
while (je) {
const je0 = je;
je = je.next;
this.publish("remove-joint", je0.joint);
this.destroyJoint(je0.joint);
b.m_jointList = je;
}
b.m_jointList = null;
// Delete the attached contacts.
let ce = b.m_contactList;
while (ce) {
const ce0 = ce;
ce = ce.next;
this.destroyContact(ce0.contact);
b.m_contactList = ce;
}
b.m_contactList = null;
// Delete the attached fixtures. This destroys broad-phase proxies.
let f = b.m_fixtureList;
while (f) {
const f0 = f;
f = f.m_next;
this.publish("remove-fixture", f0);
f0.destroyProxies(this.m_broadPhase);
b.m_fixtureList = f;
}
b.m_fixtureList = null;
// Remove world body list.
if (b.m_prev) {
b.m_prev.m_next = b.m_next;
}
if (b.m_next) {
b.m_next.m_prev = b.m_prev;
}
if (b == this.m_bodyList) {
this.m_bodyList = b.m_next;
}
b.m_destroyed = true;
--this.m_bodyCount;
this.publish("remove-body", b);
return true;
}
/**
* Create a joint to constrain bodies together. No reference to the definition
* is retained. This may cause the connected bodies to cease colliding.
*
* Note: creating a joint doesn't wake the bodies.
*
* Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
*/
createJoint<T extends Joint>(joint: T): T | null {
if (_ASSERT) console.assert(!!joint.m_bodyA);
if (_ASSERT) console.assert(!!joint.m_bodyB);
if (_ASSERT) console.assert(this.isLocked() == false);
if (this.isLocked()) {
return null;
}
// Connect to the world list.
joint.m_prev = null;
joint.m_next = this.m_jointList;
if (this.m_jointList) {
this.m_jointList.m_prev = joint;
}
this.m_jointList = joint;
++this.m_jointCount;
// Connect to the bodies' doubly linked lists.
joint.m_edgeA.joint = joint;
joint.m_edgeA.other = joint.m_bodyB;
joint.m_edgeA.prev = null;
joint.m_edgeA.next = joint.m_bodyA.m_jointList;
if (joint.m_bodyA.m_jointList) joint.m_bodyA.m_jointList.prev = joint.m_edgeA;
joint.m_bodyA.m_jointList = joint.m_edgeA;
joint.m_edgeB.joint = joint;
joint.m_edgeB.other = joint.m_bodyA;
joint.m_edgeB.prev = null;
joint.m_edgeB.next = joint.m_bodyB.m_jointList;
if (joint.m_bodyB.m_jointList) joint.m_bodyB.m_jointList.prev = joint.m_edgeB;
joint.m_bodyB.m_jointList = joint.m_edgeB;
// If the joint prevents collisions, then flag any contacts for filtering.
if (joint.m_collideConnected == false) {
for (let edge = joint.m_bodyB.getContactList(); edge; edge = edge.next) {
if (edge.other == joint.m_bodyA) {
// Flag the contact for filtering at the next time step (where either
// body is awake).
edge.contact.flagForFiltering();
}
}
}
this.publish("add-joint", joint);
return joint;
}
/**
* Destroy a joint.
*
* Warning: This may cause the connected bodies to begin colliding.
*
* Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
*/
destroyJoint(joint: Joint): void {
if (_ASSERT) console.assert(this.isLocked() == false);
if (this.isLocked()) {
return;
}
// Remove from the doubly linked list.
if (joint.m_prev) {
joint.m_prev.m_next = joint.m_next;
}
if (joint.m_next) {
joint.m_next.m_prev = joint.m_prev;
}
if (joint == this.m_jointList) {
this.m_jointList = joint.m_next;
}
// Disconnect from bodies.
const bodyA = joint.m_bodyA;
const bodyB = joint.m_bodyB;
// Wake up connected bodies.
bodyA.setAwake(true);
bodyB.setAwake(true);
// Remove from body 1.
if (joint.m_edgeA.prev) {
joint.m_edgeA.prev.next = joint.m_edgeA.next;
}
if (joint.m_edgeA.next) {
joint.m_edgeA.next.prev = joint.m_edgeA.prev;
}
if (joint.m_edgeA == bodyA.m_jointList) {
bodyA.m_jointList = joint.m_edgeA.next;
}
joint.m_edgeA.prev = null;
joint.m_edgeA.next = null;
// Remove from body 2
if (joint.m_edgeB.prev) {
joint.m_edgeB.prev.next = joint.m_edgeB.next;
}
if (joint.m_edgeB.next) {
joint.m_edgeB.next.prev = joint.m_edgeB.prev;
}
if (joint.m_edgeB == bodyB.m_jointList) {
bodyB.m_jointList = joint.m_edgeB.next;
}
joint.m_edgeB.prev = null;
joint.m_edgeB.next = null;
if (_ASSERT) console.assert(this.m_jointCount > 0);
--this.m_jointCount;
// If the joint prevents collisions, then flag any contacts for filtering.
if (joint.m_collideConnected == false) {
let edge = bodyB.getContactList();
while (edge) {
if (edge.other == bodyA) {
// Flag the contact for filtering at the next time step (where either
// body is awake).
edge.contact.flagForFiltering();
}
edge = edge.next;
}
}
this.publish("remove-joint", joint);
}
/** @internal */
s_step: TimeStep; // reuse
/**
* Take a time step. This performs collision detection, integration, and
* constraint solution.
*
* Broad-phase, narrow-phase, solve and solve time of impacts.
*
* @param timeStep Time step, this should not vary.
*/
step(timeStep: number, velocityIterations?: number, positionIterations?: number): void {
this.publish("pre-step", timeStep);
if ((velocityIterations | 0) !== velocityIterations) {
// TODO: remove this in future
velocityIterations = 0;
}
velocityIterations = velocityIterations || this.m_velocityIterations;
positionIterations = positionIterations || this.m_positionIterations;
// If new fixtures were added, we need to find the new contacts.
if (this.m_newFixture) {
this.findNewContacts();
this.m_newFixture = false;
}
this.m_locked = true;
this.s_step.reset(timeStep);
this.s_step.velocityIterations = velocityIterations;
this.s_step.positionIterations = positionIterations;
this.s_step.warmStarting = this.m_warmStarting;
this.s_step.blockSolve = this.m_blockSolve;
// Update contacts. This is where some contacts are destroyed.
this.updateContacts();
// Integrate velocities, solve velocity constraints, and integrate positions.
if (this.m_stepComplete && timeStep > 0.0) {
this.m_solver.solveWorld(this.s_step);
// Synchronize fixtures, check for out of range bodies.
for (let b = this.m_bodyList; b; b = b.getNext()) {
// If a body was not in an island then it did not move.
if (b.m_islandFlag == false) {
continue;
}
if (b.isStatic()) {
continue;
}
// Update fixtures (for broad-phase).
b.synchronizeFixtures();
}
// Look for new contacts.
this.findNewContacts();
}
// Handle TOI events.
if (this.m_continuousPhysics && timeStep > 0.0) {
this.m_solver.solveWorldTOI(this.s_step);
}
if (this.m_clearForces) {
this.clearForces();
}
this.m_locked = false;
let callback: (world: World) => unknown;
while ((callback = this.m_step_callback.shift())) {
callback(this);
}
this.publish("post-step", timeStep);
}
/**
* Queue a function to be called after ongoing simulation step. If no simulation is in progress call it immediately.
*/
queueUpdate(callback: (world: World) => unknown): void {
if (!this.isLocked()) {
callback(this);
} else {
this.m_step_callback.push(callback);
}
}
/**
* @internal
* Call this method to find new contacts.
*/
findNewContacts(): void {
this.m_broadPhase.updatePairs((proxyA: FixtureProxy, proxyB: FixtureProxy) => this.createContact(proxyA, proxyB));
}
/**
* @internal
* Callback for broad-phase.
*/
createContact(proxyA: FixtureProxy, proxyB: FixtureProxy): void {
const fixtureA = proxyA.fixture;
const fixtureB = proxyB.fixture;
const indexA = proxyA.childIndex;
const indexB = proxyB.childIndex;
const bodyA = fixtureA.getBody();
const bodyB = fixtureB.getBody();
// Are the fixtures on the same body?
if (bodyA == bodyB) {
return;
}
// TODO_ERIN use a hash table to remove a potential bottleneck when both
// bodies have a lot of contacts.
// Does a contact already exist?
let edge = bodyB.getContactList(); // ContactEdge
while (edge) {
if (edge.other == bodyA) {
const fA = edge.contact.getFixtureA();
const fB = edge.contact.getFixtureB();
const iA = edge.contact.getChildIndexA();
const iB = edge.contact.getChildIndexB();
if (fA == fixtureA && fB == fixtureB && iA == indexA && iB == indexB) {
// A contact already exists.
return;
}
if (fA == fixtureB && fB == fixtureA && iA == indexB && iB == indexA) {
// A contact already exists.
return;
}
}
edge = edge.next;
}
if (bodyB.shouldCollide(bodyA) == false) {
return;
}
if (fixtureB.shouldCollide(fixtureA) == false) {
return;
}
// Call the factory.
const contact = Contact.create(fixtureA, indexA, fixtureB, indexB);
if (contact == null) {
return;
}
// Insert into the world.
contact.m_prev = null;
if (this.m_contactList != null) {
contact.m_next = this.m_contactList;
this.m_contactList.m_prev = contact;
}
this.m_contactList = contact;
++this.m_contactCount;
}
/**
* @internal
* Removes old non-overlapping contacts, applies filters and updates contacts.
*/
updateContacts(): void {
// Update awake contacts.
let c: Contact;
let next_c = this.m_contactList;
while ((c = next_c)) {
next_c = c.getNext();
const fixtureA = c.getFixtureA();
const fixtureB = c.getFixtureB();
const indexA = c.getChildIndexA();
const indexB = c.getChildIndexB();
const bodyA = fixtureA.getBody();
const bodyB = fixtureB.getBody();
// Is this contact flagged for filtering?
if (c.m_filterFlag) {
if (bodyB.shouldCollide(bodyA) == false) {
this.destroyContact(c);
continue;
}
if (fixtureB.shouldCollide(fixtureA) == false) {
this.destroyContact(c);
continue;
}
// Clear the filtering flag.
c.m_filterFlag = false;
}
const activeA = bodyA.isAwake() && !bodyA.isStatic();
const activeB = bodyB.isAwake() && !bodyB.isStatic();
// At least one body must be awake and it must be dynamic or kinematic.
if (activeA == false && activeB == false) {
continue;
}
const proxyIdA = fixtureA.m_proxies[indexA].proxyId;
const proxyIdB = fixtureB.m_proxies[indexB].proxyId;
const overlap = this.m_broadPhase.testOverlap(proxyIdA, proxyIdB);
// Here we destroy contacts that cease to overlap in the broad-phase.
if (overlap == false) {
this.destroyContact(c);
continue;
}
// The contact persists.
c.update(this);
}
}
/** @internal */
destroyContact(contact: Contact): void {
// Remove from the world.
if (contact.m_prev) {
contact.m_prev.m_next = contact.m_next;
}
if (contact.m_next) {
contact.m_next.m_prev = contact.m_prev;
}
if (contact == this.m_contactList) {
this.m_contactList = contact.m_next;
}
Contact.destroy(contact, this);
--this.m_contactCount;
}
/**
* Called when two fixtures begin to touch.
*
* Implement contact callbacks to get contact information. You can use these
* results for things like sounds and game logic. You can also get contact
* results by traversing the contact lists after the time step. However, you
* might miss some contacts because continuous physics leads to sub-stepping.
* Additionally you may receive multiple callbacks for the same contact in a
* single time step. You should strive to make your callbacks efficient because
* there may be many callbacks per time step.
*
* Warning: You cannot create/destroy world entities inside these callbacks.
*/
on(name: "begin-contact", listener: (contact: Contact) => void): World;
/**
* Called when two fixtures cease to touch.
*
* Implement contact callbacks to get contact information. You can use these
* results for things like sounds and game logic. You can also get contact
* results by traversing the contact lists after the time step. However, you
* might miss some contacts because continuous physics leads to sub-stepping.
* Additionally you may receive multiple callbacks for the same contact in a
* single time step. You should strive to make your callbacks efficient because
* there may be many callbacks per time step.
*
* Warning: You cannot create/destroy world entities inside these callbacks.
*/
on(name: "end-contact", listener: (contact: Contact) => void): World;
/**
* This is called after a contact is updated. This allows you to inspect a
* contact before it goes to the solver. If you are careful, you can modify the
* contact manifold (e.g. disable contact). A copy of the old manifold is
* provided so that you can detect changes. Note: this is called only for awake
* bodies. Note: this is called even when the number of contact points is zero.
* Note: this is not called for sensors. Note: if you set the number of contact
* points to zero, you will not get an end-contact callback. However, you may get
* a begin-contact callback the next step.
*
* Warning: You cannot create/destroy world entities inside these callbacks.
*/
on(name: "pre-solve", listener: (contact: Contact, oldManifold: Manifold) => void): World;
/**
* This lets you inspect a contact after the solver is finished. This is useful
* for inspecting impulses. Note: the contact manifold does not include time of
* impact impulses, which can be arbitrarily large if the sub-step is small.
* Hence the impulse is provided explicitly in a separate data structure. Note:
* this is only called for contacts that are touching, solid, and awake.
*
* Warning: You cannot create/destroy world entities inside these callbacks.
*/
on(name: "post-solve", listener: (contact: Contact, impulse: ContactImpulse) => void): World;
/** Listener is called when a body is removed. */
on(name: "remove-body", listener: (body: Body) => void): World;
/** Listener is called when a joint is removed implicitly or explicitly. */
on(name: "remove-joint", listener: (joint: Joint) => void): World;
/** Listener is called when a fixture is removed implicitly or explicitly. */
on(name: "remove-fixture", listener: (fixture: Fixture) => void): World;
/** Listener is called when a body is added. */
on(name: "add-body", listener: (body: Body) => void): World;
/** Listener is called when a joint is added. */
on(name: "add-joint", listener: (joint: Joint) => void): World;
/** Listener is called when a fixture is added. */
on(name: "add-fixture", listener: (fixture: Fixture) => void): World;
/**
* Register an event listener.
*/
// tslint:disable-next-line:typedef
on(name, listener) {
if (typeof name !== "string" || typeof listener !== "function") {
return this;
}
if (!this._listeners) {
this._listeners = {};
}
if (!this._listeners[name]) {
this._listeners[name] = [];
}
this._listeners[name].push(listener);
return this;
}
off(name: "begin-contact", listener: (contact: Contact) => void): World;
off(name: "end-contact", listener: (contact: Contact) => void): World;
off(name: "pre-solve", listener: (contact: Contact, oldManifold: Manifold) => void): World;
off(name: "post-solve", listener: (contact: Contact, impulse: ContactImpulse) => void): World;
off(name: "remove-body", listener: (body: Body) => void): World;
off(name: "remove-joint", listener: (joint: Joint) => void): World;
off(name: "remove-fixture", listener: (fixture: Fixture) => void): World;
off(name: "add-body", listener: (body: Body) => void): World;
off(name: "add-joint", listener: (joint: Joint) => void): World;
off(name: "add-fixture", listener: (fixture: Fixture) => void): World;
/**
* Remove an event listener.
*/
// tslint:disable-next-line:typedef
off(name, listener) {
if (typeof name !== "string" || typeof listener !== "function") {
return this;
}
const listeners = this._listeners && this._listeners[name];
if (!listeners || !listeners.length) {
return this;
}
const index = listeners.indexOf(listener);
if (index >= 0) {
listeners.splice(index, 1);
}
return this;
}
publish(name: string, arg1?: any, arg2?: any, arg3?: any): number {
const listeners = this._listeners && this._listeners[name];
if (!listeners || !listeners.length) {
return 0;
}
for (let l = 0; l < listeners.length; l++) {
listeners[l].call(this, arg1, arg2, arg3);
}
return listeners.length;
}
/** @internal */
beginContact(contact: Contact): void {
this.publish("begin-contact", contact);
}
/** @internal */
endContact(contact: Contact): void {
this.publish("end-contact", contact);
}
/** @internal */
preSolve(contact: Contact, oldManifold: Manifold): void {
this.publish("pre-solve", contact, oldManifold);
}
/** @internal */
postSolve(contact: Contact, impulse: ContactImpulse): void {
this.publish("post-solve", contact, impulse);
}
/**
* Joints and fixtures are destroyed when their associated body is destroyed.
* Register a destruction listener so that you may nullify references to these
* joints and shapes.
*
* `function(object)` is called when any joint or fixture is about to
* be destroyed due to the destruction of one of its attached or parent bodies.
*/
/**
* Register a contact filter to provide specific control over collision.
* Otherwise the default filter is used (defaultFilter). The listener is owned
* by you and must remain in scope.
*
* Moved to Fixture.
*/
}