Skip to content

Instantly share code, notes, and snippets.

@SupremeTechnopriest
Last active August 8, 2019 06:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SupremeTechnopriest/e868585fcbf389a128eb832a545e9c13 to your computer and use it in GitHub Desktop.
Save SupremeTechnopriest/e868585fcbf389a128eb832a545e9c13 to your computer and use it in GitHub Desktop.
Flyweighted SAT.js Collision System
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