Skip to content

Instantly share code, notes, and snippets.

@mtdowling
Created February 8, 2017 06:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mtdowling/2bd0c31ed01d1c83dd1024f758a14460 to your computer and use it in GitHub Desktop.
Save mtdowling/2bd0c31ed01d1c83dd1024f758a14460 to your computer and use it in GitHub Desktop.
A slice of some of my collision rounding code
local mod = {}
--- Returns true if the rects overlap.
-- @param a Table rect containing x1, y1, x2, y2
-- @param b Table rect containing x1, y1, x2, y2
-- @return bool Returns true if rects overlap.
mod.doRectsOverlap = function(a, b)
return a.x1 <= b.x2 and a.x2 >= b.x1 and a.y1 <= b.y2 and a.y2 >= b.y1
end
--- Creates a quadrant of a rect.
-- @param item Item that contains x, y, w, and h.
-- @param addHalfW Set to true to use the right half.
-- @param addHalfH Set to true to use the bottom half.
-- @return table Returns the rect table (x1, y1, x2, y2)
local function createQuadrant(item, addHalfW, addHalfH)
local x1, y1 = item.x, item.y
if addHalfW then x1 = x1 + item.w * 0.5 end
if addHalfH then y1 = y1 + item.h * 0.5 end
return {
x1 = x1,
y1 = y1,
x2 = x1 + item.w * 0.5,
y2 = y1 + item.h * 0.5,
}
end
local function isInQuadrant(cols, addHalfW, addHalfH)
assert(#cols > 0)
local itemQuad = createQuadrant(cols[1].itemRect, addHalfW, addHalfH)
for _, col in ipairs(cols) do
if mod.doRectsOverlap(itemQuad, col._rect) and col.type ~= "cross" then
return true
end
end
return false
end
local function inUpperLeftQuad(cols)
return isInQuadrant(cols, false, false)
end
local function inUpperRightQuad(cols)
return isInQuadrant(cols, true, false)
end
local function inLowerRightQuad(cols)
return isInQuadrant(cols, true, true)
end
local function inLowerLeftQuad(cols)
return isInQuadrant(cols, false, true)
end
--- Returns a rounded collision for a movement attempt.
-- @param dx Attempted x movement.
-- @param dy Attempted y movement.
-- @param x Current X.
-- @param y Current Y.
-- @param cols Detected bump collisions.
-- @return Returns the updated x and y
mod.roundCollisions = function(dx, dy, x, y, cols)
local movement = max(abs(dx), abs(dy))
-- Add and cache rects on each col.
for _, col in ipairs(cols) do
local item = col.otherRect
col._rect = {
x1 = item.x,
y1 = item.y,
x2 = item.x + item.w,
y2 = item.y + item.h,
}
end
if dx > 0 then
if not inUpperRightQuad(cols) then
if inLowerRightQuad(cols) then
y = y - movement
end
elseif not inLowerRightQuad(cols) then
y = y + movement
end
elseif dx < 0 then
if not inUpperLeftQuad(cols) then
if inLowerLeftQuad(cols) then
y = y - movement
end
elseif not inLowerLeftQuad(cols) then
y = y + movement
end
elseif dy > 0 then
if not inLowerRightQuad(cols) then
if inLowerLeftQuad(cols) then
x = x + movement
end
elseif not inLowerLeftQuad(cols) then
x = x - movement
end
elseif dy < 0 then
if not inUpperLeftQuad(cols) then
if inUpperRightQuad(cols) then
x = x - movement
end
elseif not inUpperRightQuad(cols) then
x = x + movement
end
end
return x, y
end
return mod
local ecs = require "ecs"
local collision = require "collision"
local components = require "components"
local utils = require "utils"
local clamp = lume.clamp
--- Updates entity physics.
local MovementSystem = ecs.createSystem("Movement", "pos", "motion")
--- Default filter used for collisions.
MovementSystem.collisionFilter = collision.filters.default()
--- Invoke each collision event.
function MovementSystem.processCollisions(collisions)
for _, c in ipairs(collisions) do
local item, other = c.item, c.other
local aemitter, bemitter = item.emitter, other.emitter
if aemitter then aemitter:emit("collision", item, other, c) end
-- Only emit the collision event to `other` if other's filter accepts it.
if bemitter then
local otherFilter = other.collision and other.collision.filter
if not otherFilter or otherFilter(other, item) then
bemitter:emit("collision", other, item, c)
end
end
end
end
--- Checks the collisions for a given entity with the destination coordinates.
function MovementSystem.checkCollisions(e, ex, ey)
local pos = assert(e.pos, "No pos component")
local coll = assert(e.collision, "No collision component")
local box = coll.box or {0, 0}
if not game.play.bump.rects[e] then
return ex, ey, {}, 0
end
-- Resolve bump collisions and get the collided coordinates.
local x, y, colls, len = game.play.bump:check(
e, ex + box[1], ey + box[2], coll.filter)
-- Adjust back to un-do the bbox adjustment.
x, y = x - box[1], y - box[2]
-- Account for crazy ass teleportation type collisions with
-- arrows, spikes, etc.
local actualMovement = math.max(math.abs(pos.x - x), math.abs(pos.y - y))
local movement = math.max(math.abs(pos.x - ex), math.abs(pos.y - ey))
if actualMovement > movement then
return pos.x, pos.y, colls, len
end
return x, y, colls, len
end
--- Move the object to the given position and invoke the given collision fns
function MovementSystem.move(e, x, y, collisions)
local collisionComponent = e.collision
if collisionComponent then
local box, pos = collisionComponent.box, e.pos
pos.x, pos.y = x, y
if game.play:tryMove(e, pos.x + box[1], pos.y + box[2]) then
if collisions then
MovementSystem.processCollisions(collisions)
end
end
end
end
function MovementSystem:updateEach(dt, e)
local coll, pos, motion = e.collision, e.pos, e.motion
local currentDx, currentDy = motion.dx, motion.dy
local dx, dy = motion.dx or 0, motion.dy or 0
-- Not moving, then don't update.
if dx == 0 and dy == 0 then return end
-- Emit the move event so other systems can know that the entity moved.
e:send("move", e, dx, dy)
if coll then
-- Move the entity based on bump collision resolutions.
self:_moveTowards(e, pos, coll, dx, dy)
else
-- If this is not in bump, then just move the entity.
pos.x, pos.y = pos.x + dx, pos.y + dy
end
-- Remove the motion attributes.
if motion.dx == currentDx then motion.dx = 0 end
if motion.dy == currentDy then motion.dy = 0 end
end
function MovementSystem:_moveTowards(e, pos, coll, dx, dy)
local expectX, expectY = pos.x + dx, pos.y + dy
local x, y, colls = MovementSystem.checkCollisions(e, expectX, expectY)
local hasDx = math.abs(dx) > 1 ^ -8
local hasDy = math.abs(dy) > 1 ^ -8
-- Only round when not moving diagonally, when collisions are detected, and
-- the expected move is not what was resolved in bump.
if coll.roundedCorners ~= false
and #colls > 0
and (x ~= expectX or y ~= expectY)
and ((not hasDx and hasDy) or (hasDx and not hasDy)) then
if hasDx then
x, y = collision.roundCollisions(dx, 0, x, y, colls)
else
x, y = collision.roundCollisions(0, dy, x, y, colls)
end
end
return MovementSystem.move(e, x, y, colls)
end
return MovementSystem
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment