Last active
August 8, 2019 06:28
-
-
Save SupremeTechnopriest/e868585fcbf389a128eb832a545e9c13 to your computer and use it in GitHub Desktop.
Flyweighted SAT.js Collision System
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SAT from 'sat' | |
// Size of each memory pool | |
const POOL_SIZE = 100 | |
// Setting this to true will draw a client side | |
// representation of the collider shapes | |
const DRAW_COLLIDERS = false | |
// Generators | |
const createPool = (size, content) => { | |
return new Array(size).fill().map(() => content()) | |
} | |
// Pools | |
const polygonPool = createPool(POOL_SIZE, () => new SAT.Polygon()) | |
const circlePool = createPool(POOL_SIZE, () => new SAT.Circle()) | |
const vectorPool = createPool(POOL_SIZE * 5, () => new SAT.Vector()) | |
const responsePool = createPool(POOL_SIZE, () => new SAT.Response()) | |
const categoryCheck = (c1, c2) => c1.collidesWith.some(category => c2.categories.includes(category)) | |
function checkOverlap (c1, s1, c2, s2, response) { | |
response.clear() | |
if (c1.shape === 'circle') { | |
if (c2.shape === 'circle') { | |
return SAT.testCircleCircle(s1, s2, response) | |
} else if (c2.shape === 'box') { | |
return SAT.testCirclePolygon(s1, s2, response) | |
} else if (c2.shape === 'polygon') { | |
return SAT.testCirclePolygon(s1, s2, response) | |
} | |
} else if (c1.shape === 'box') { | |
if (c2.shape === 'circle') { | |
return SAT.testPolygonCircle(s1, s2, response) | |
} else if (c2.shape === 'box') { | |
return SAT.testPolygonPolygon(s1, s2, response) | |
} else if (c2.shape === 'polygon') { | |
return SAT.testPolygonPolygon(s1, s2, response) | |
} | |
} else if (c1.shape === 'polygon') { | |
if (c2.shape === 'circle') { | |
return SAT.testPolygonCircle(s1, s2, response) | |
} else if (c2.shape === 'box') { | |
return SAT.testPolygonPolygon(s1, s2, response) | |
} else if (c2.shape === 'polygon') { | |
return SAT.testPolygonPolygon(s1, s2, response) | |
} | |
} | |
} | |
function setBox (polygon, position, rotation, pOffset, rOffset, width, height, offsetV, pointsV, edgesV, normalsV, calcV) { | |
// NOTE: Don't be tempted to use an SAT.Box.toPolygon here. It | |
// thrashes the garbage collcetor really badly. | |
pointsV[0].x = 0 | |
pointsV[0].y = 0 | |
pointsV[1].x = width | |
pointsV[1].y = 0 | |
pointsV[2].x = width | |
pointsV[2].y = height | |
pointsV[3].x = 0 | |
pointsV[3].y = height | |
polygon.calcPoints = calcV | |
polygon.edges = edgesV | |
polygon.normals = normalsV | |
polygon.points = pointsV | |
polygon._recalc() | |
polygon.pos.x = position.x - (pOffset ? pOffset.x : 0) | |
polygon.pos.y = position.y - (pOffset ? pOffset.y : 0) | |
offsetV.x = rOffset ? rOffset.x : 0 | |
offsetV.y = rOffset ? rOffset.y : 0 | |
polygon.setOffset(offsetV) | |
polygon.setAngle(rotation.radians) | |
} | |
function setPolygon (polygon, position, rotation, offset, points, offsetV, pointsV, edgesV, normalsV, calcV) { | |
// NOTE: Don't use setPoints here. It creates new vectors each time | |
// and thrashes the garbage collector setOffset and setAngle are safe. | |
polygon.pos.x = position.x | |
polygon.pos.y = position.y | |
points.forEach(({ x, y }, i) => { | |
pointsV[i].x = x | |
pointsV[i].y = y | |
}) | |
polygon.calcPoints = calcV | |
polygon.edges = edgesV | |
polygon.normals = normalsV | |
polygon.points = pointsV | |
polygon._recalc() | |
offsetV.x = offset ? offset.x : 0 | |
offsetV.y = offset ? offset.y : 0 | |
polygon.setOffset(offsetV) | |
polygon.setAngle(rotation.radians) | |
} | |
let debugPoly | |
let debugCircle | |
let debugPoly2 | |
let debugCircle2 | |
export default function (modngn) { | |
const { engine, services: { spatial } } = modngn | |
let nearby | |
if (DRAW_COLLIDERS) { | |
debugPoly = modngn.entities.PolygonCollider() | |
debugCircle = modngn.entities.CircleCollider() | |
debugPoly2 = modngn.entities.PolygonCollider() | |
debugCircle2 = modngn.entities.CircleCollider() | |
} | |
return (position, rotation, collider, id) => { | |
nearby = spatial.broadphase(position) | |
let lastPolygon = 0 | |
let lastCircle = 0 | |
let lastVector = 0 | |
let lastResponse = 0 | |
const vectorCluster = len => new Array(len).fill().map(() => vectorPool[lastVector++]) | |
if (collider.active) { | |
let shape = null | |
switch (collider.shape) { | |
case 'circle': { | |
shape = circlePool[lastCircle++] | |
shape.pos.x = position.x | |
shape.pos.y = position.y | |
shape.r = collider.radius | |
if (DRAW_COLLIDERS) { | |
engine.updateComponent(debugCircle, 'POSITION', shape.pos) | |
engine.updateComponent(debugCircle, 'RADIUS', { radius: shape.r }) | |
} | |
break | |
} | |
case 'box': { | |
shape = polygonPool[lastPolygon++] | |
setBox( | |
shape, | |
position, | |
rotation, | |
collider.positionOffset, | |
collider.rotationOffset, | |
collider.width, | |
collider.height, | |
vectorCluster(1), | |
vectorCluster(4), | |
vectorCluster(4), | |
vectorCluster(4), | |
vectorCluster(4) | |
) | |
if (DRAW_COLLIDERS) { | |
engine.updateComponent(debugPoly, 'POSITION', shape.pos) | |
engine.updateComponent(debugPoly, 'ROTATION', { radians: shape.angle }) | |
engine.updateComponent(debugPoly, 'POINTS', { | |
points: JSON.stringify(shape.calcPoints) | |
}) | |
} | |
break | |
} | |
case 'polygon': { | |
shape = polygonPool[lastPolygon++] | |
setPolygon( | |
shape, | |
position, | |
rotation, | |
collider.offset, | |
collider.points, | |
vectorCluster(1), | |
vectorCluster(collider.points.length), | |
vectorCluster(collider.points.length), | |
vectorCluster(collider.points.length), | |
vectorCluster(collider.points.length) | |
) | |
if (DRAW_COLLIDERS) { | |
engine.updateComponent(debugPoly, 'POSITION', shape.pos) | |
engine.updateComponent(debugPoly, 'ROTATION', { radians: shape.angle }) | |
engine.updateComponent(debugPoly, 'POINTS', { | |
points: JSON.stringify(shape.calcPoints) | |
}) | |
} | |
break | |
} | |
default: | |
} | |
nearby.forEach((id2) => { | |
// Test each collider type | |
const colliders = modngn.getAddress('components.engine.collider.composure.components', []) | |
colliders.forEach(colliderName => { | |
const collider2 = engine.getComponent(id2, colliderName) | |
if (!collider2) return | |
if (id === id2) return | |
if (collider2.active) { | |
const position2 = engine.getComponent(id2, 'POSITION') | |
const rotation2 = engine.getComponent(id2, 'ROTATION') | |
let shape2 = null | |
switch (collider2.shape) { | |
case 'circle': | |
shape2 = circlePool[lastCircle++] | |
shape2.pos.x = position2.x, | |
shape2.pos.y = position2.y | |
shape2.r = collider2.radius | |
if (DRAW_COLLIDERS) { | |
engine.updateComponent(debugCircle2, 'POSITION', shape2.pos) | |
engine.updateComponent(debugCircle2, 'RADIUS', { radius: shape2.r }) | |
} | |
break | |
case 'box': { | |
shape2 = polygonPool[lastPolygon++] | |
setBox( | |
shape2, | |
position2, | |
rotation2, | |
collider2.positionOffset, | |
collider2.rotationOffset, | |
collider2.width, | |
collider2.height, | |
vectorCluster(1), | |
vectorCluster(4), | |
vectorCluster(4), | |
vectorCluster(4), | |
vectorCluster(4) | |
) | |
if (DRAW_COLLIDERS) { | |
engine.updateComponent(debugPoly2, 'POSITION', shape2.pos) | |
engine.updateComponent(debugPoly2, 'ROTATION', { radians: shape2.angle }) | |
engine.updateComponent(debugPoly2, 'POINTS', { | |
points: JSON.stringify(shape2.calcPoints) | |
}) | |
} | |
break | |
} | |
case 'polygon': { | |
shape2 = polygonPool[lastPolygon++] | |
setPolygon( | |
shape2, | |
position2, | |
rotation2, | |
collider2.offset, | |
collider2.points, | |
vectorCluster(1), | |
vectorCluster(collider2.points.length), | |
vectorCluster(collider2.points.length), | |
vectorCluster(collider2.points.length), | |
vectorCluster(collider2.points.length) | |
) | |
if (DRAW_COLLIDERS) { | |
engine.updateComponent(debugPoly2, 'POSITION', shape2.pos) | |
engine.updateComponent(debugPoly2, 'ROTATION', { radians: shape2.angle }) | |
engine.updateComponent(debugPoly2, 'POINTS', { | |
points: JSON.stringify(shape2.calcPoints) | |
}) | |
} | |
break | |
} | |
default: | |
} | |
if (categoryCheck(collider, collider2)) { | |
const response = responsePool[lastResponse++] | |
if (checkOverlap(collider, shape, collider2, shape2, response)) { | |
engine.emit('overlap', id, id2, collider.name, collider2.name, response) | |
const overlap = engine.getComponent(id, 'OVERLAP') | |
if (overlap) { | |
overlap.with.push(id2) | |
overlap.overlapX.push(response.overlapV.x) | |
overlap.overlapY.push(response.overlapV.y) | |
overlap.collider.push(collider2.name) | |
} else { | |
engine.addComponent(id, 'OVERLAP', { | |
with: [id2], | |
overlapX: [response.overlapV.x], | |
overlapY: [response.overlapV.y], | |
collider: [collider2.name] | |
}) | |
} | |
} | |
} | |
} | |
}) | |
}) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment