planck
Version:
2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development
288 lines (234 loc) • 6.84 kB
text/typescript
/*
* Copyright (c) Erin Catto
* Licensed under the MIT license
*/
import { World, Vec2, DynamicTree, AABB, Math, Testbed, RayCastInput, RayCastOutput } from "planck";
class Actor {
aabb = new AABB();
fraction: number;
overlap: boolean;
proxyId: number | null = null;
}
const testbed = Testbed.mount();
const world = new World();
testbed.start(world);
const ACTOR_COUNT = 128;
const WORLD_EXTENT = 15.0;
const PROXY_EXTENT = 0.5;
let automated = false;
const tree = new DynamicTree<Actor>();
const actors: Actor[] = [];
for (let i = 0; i < ACTOR_COUNT; ++i) {
const actor = new Actor();
actors[i] = actor;
getRandomAABB(actor.aabb);
actor.proxyId = tree.createProxy(actor.aabb, actor);
}
const queryAABB = new AABB({ x: -3.0, y: -4.0 + WORLD_EXTENT }, { x: 5.0, y: 6.0 + WORLD_EXTENT });
function queryCallback(proxyId: number) {
const actor = tree.getUserData(proxyId);
actor.overlap = AABB.testOverlap(queryAABB, actor.aabb);
return true;
}
function runQuery(tree: DynamicTree<Actor>) {
tree.query(queryAABB, queryCallback);
for (let i = 0; i < actors.length; ++i) {
if (actors[i].proxyId == null) {
continue;
}
const overlap = AABB.testOverlap(queryAABB, actors[i].aabb);
// console.assert(overlap == actors[i].overlap);
}
}
const rayCastInput: RayCastInput = {
// p1: { x: 0.0, y: 2.0 + worldExtent },
// p2: { x: 0.0, y: -2.0 + worldExtent },
p1: { x: -5.0, y: 5.0 + WORLD_EXTENT },
p2: { x: 7.0, y: -4.0 + WORLD_EXTENT },
maxFraction: 1.0,
};
const rayCastResult = {} as {
actor: Actor | null;
output: RayCastOutput | null;
input: RayCastInput;
};
function rayCastCallback(input: RayCastInput, proxyId: number) {
const actor = tree.getUserData(proxyId);
const output = {} as RayCastOutput;
const hit = actor.aabb.rayCast(output, input);
if (hit) {
rayCastResult.output = output;
rayCastResult.actor = actor;
rayCastResult.actor.fraction = output.fraction;
return output.fraction;
}
return input.maxFraction;
}
function runRayCast() {
rayCastResult.actor = null;
rayCastResult.output = null;
rayCastResult.input = {
p1: Vec2.clone(rayCastInput.p1),
p2: Vec2.clone(rayCastInput.p2),
maxFraction: rayCastInput.maxFraction,
};
// Ray cast against the dynamic tree.
tree.rayCast(rayCastResult.input, rayCastCallback);
// Brute force ray cast.
let bruteActor: Actor | null = null;
let bruteOutput: RayCastOutput | null = null;
for (let i = 0; i < actors.length; ++i) {
const actor = actors[i];
if (actor.proxyId == null) {
continue;
}
const output = {} as RayCastOutput;
const hit = actor.aabb.rayCast(output, rayCastResult.input);
if (hit) {
bruteActor = actor;
bruteOutput = output;
rayCastResult.input.maxFraction = output.fraction;
}
}
if (bruteActor != null) {
// console.assert(bruteOutput?.fraction == rayCastOutput.fraction);
}
return rayCastResult;
}
testbed.step = function () {
for (let i = 0; i < actors.length; ++i) {
actors[i].fraction = 1.0;
actors[i].overlap = false;
}
if (automated == true) {
const actionCount = Math.max(1, actors.length >> 2);
for (let i = 0; i < actionCount; ++i) {
runAction();
}
}
runQuery(tree);
const rayCastResult = runRayCast();
for (let i = 0; i < actors.length; ++i) {
const actor = actors[i];
if (actor.proxyId == null) continue;
let c = testbed.color(0.9, 0.9, 0.9);
if (actor == rayCastResult.actor && actor.overlap) {
c = testbed.color(0.9, 0.6, 0.6);
} else if (actor == rayCastResult.actor) {
c = testbed.color(0.6, 0.9, 0.6);
} else if (actor.overlap) {
c = testbed.color(0.6, 0.6, 0.9);
}
testbed.drawAABB(actor.aabb, c);
}
testbed.drawAABB(queryAABB, testbed.color(0.7, 0.7, 0.7));
testbed.drawSegment(rayCastResult.input.p1, rayCastResult.input.p2, testbed.color(0.9, 0.9, 0.9));
testbed.drawPoint(rayCastResult.input.p1, 6.0, testbed.color(0.2, 0.9, 0.2));
testbed.drawPoint(rayCastResult.input.p2, 6.0, testbed.color(0.9, 0.2, 0.2));
if (rayCastResult.actor) {
const p = Vec2.combine(
1 - rayCastResult.actor.fraction,
rayCastResult.input.p1,
rayCastResult.actor.fraction,
rayCastResult.input.p2,
);
testbed.drawPoint(p, 6.0, testbed.color(0.2, 0.2, 0.9));
}
const height = tree.getHeight();
testbed.status("dynamic tree height", height);
};
testbed.keydown = function (code, char) {
switch (char) {
case "Z":
automated = !automated;
break;
case "C":
createProxy();
break;
case "X":
destroyProxy();
break;
case "M":
moveProxy();
break;
}
};
function getRandomAABB(aabb: AABB) {
// aabb.lowerBound.x = -proxyExtent;
// aabb.lowerBound.y = -proxyExtent + worldExtent;
aabb.lowerBound.x = Math.random(-WORLD_EXTENT, WORLD_EXTENT);
aabb.lowerBound.y = Math.random(0.0, 2.0 * WORLD_EXTENT);
aabb.upperBound.x = aabb.lowerBound.x + 2.0 * PROXY_EXTENT;
aabb.upperBound.y = aabb.lowerBound.y + 2.0 * PROXY_EXTENT;
}
function moveAABB(aabb: AABB) {
const d = {
x: Math.random(-0.5, 0.5),
y: Math.random(-0.5, 0.5),
};
// d.x = 2.0;
// d.y = 0.0;
aabb.lowerBound.add(d);
aabb.upperBound.add(d);
const c0 = Vec2.mid(aabb.lowerBound, aabb.upperBound);
const min = {
x: -WORLD_EXTENT,
y: 0.0,
};
const max = {
x: WORLD_EXTENT,
y: 2.0 * WORLD_EXTENT,
};
const c = Vec2.clampVec2(c0, min, max);
aabb.lowerBound.add(c).sub(c0);
aabb.upperBound.add(c).sub(c0);
}
function createProxy() {
for (let i = 0; i < actors.length; ++i) {
const j = (Math.random() * actors.length) | 0;
const actor = actors[j];
if (actor.proxyId == null) {
getRandomAABB(actor.aabb);
actor.proxyId = tree.createProxy(actor.aabb, actor);
return;
}
}
}
function destroyProxy() {
for (let i = 0; i < actors.length; ++i) {
const j = (Math.random() * actors.length) | 0;
const actor = actors[j];
if (actor.proxyId != null) {
tree.destroyProxy(actor.proxyId);
actor.proxyId = null;
return;
}
}
}
function moveProxy() {
for (let i = 0; i < actors.length; ++i) {
const j = (Math.random() * actors.length) | 0;
const actor = actors[j];
if (actor.proxyId == null) {
continue;
}
const aabb0 = actor.aabb;
moveAABB(actor.aabb);
const displacement = Vec2.sub(actor.aabb.getCenter(), aabb0.getCenter());
tree.moveProxy(actor.proxyId, actor.aabb, displacement);
return;
}
}
function runAction() {
const choice = (Math.random() * 20) | 0;
switch (choice) {
case 0:
createProxy();
break;
case 1:
destroyProxy();
break;
default:
moveProxy();
}
}