diff --git a/init.lua b/init.lua index 12bc10d..3605eb1 100644 --- a/init.lua +++ b/init.lua @@ -41,271 +41,102 @@ local newPolygonShape = Shapes.newPolygonShape local newCircleShape = Shapes.newCircleShape local newPointShape = Shapes.newPointShape -local function __NULL__() end - local HC = {} -function HC:init(cell_size, callback_collide, callback_stop) - self._active_shapes = {} - self._passive_shapes = {} - self._ghost_shapes = {} - self.groups = {} - self._colliding_only_last_frame = {} - - self.on_collide = callback_collide or __NULL__ - self.on_stop = callback_stop or __NULL__ - self._hash = common_local.instance(Spatialhash, cell_size) +function HC:init(cell_size) + self.hash = common_local.instance(Spatialhash, cell_size or 100) end -function HC:clear() - self._active_shapes = {} - self._passive_shapes = {} - self._ghost_shapes = {} - self.groups = {} - self._colliding_only_last_frame = {} - self._hash = common_local.instance(Spatialhash, self._hash.cell_size) +-- spatial hash management +function HC:resetHash(cell_size) + local hash = self.hash + self.hash = common_local.instance(Spatialhash, cell_size or 100) + for shape in pairs(hash:shapes()) do + self.hash:register(shape, shape:bbox()) + end return self end -function HC:setCallbacks(collide, stop) - if type(collide) == "table" and not (getmetatable(collide) or {}).__call then - stop = collide.stop - collide = collide.collide - end +function HC:register(shape) + self.hash:register(shape, shape:bbox()) - if collide then - assert(type(collide) == "function" or (getmetatable(collide) or {}).__call, - "collision callback must be a function or callable table") - self.on_collide = collide - end - - if stop then - assert(type(stop) == "function" or (getmetatable(stop) or {}).__call, - "stop callback must be a function or callable table") - self.on_stop = stop - end - - return self -end - -function HC:addShape(shape) - assert(shape.bbox and shape.collidesWith, - "Cannot add custom shape: Incompatible shape.") - - self._active_shapes[shape] = shape - self._hash:insert(shape, shape:bbox()) - shape._groups = {} - - local hash = self._hash - local move, rotate,scale = shape.move, shape.rotate, shape.scale - for _, func in ipairs{'move', 'rotate', 'scale'} do - local old_func = shape[func] - shape[func] = function(self, ...) - local x1,y1,x2,y2 = self:bbox() - old_func(self, ...) - local x3,y3,x4,y4 = self:bbox() - hash:update(self, x1,y1, x2,y2, x3,y3, x4,y4) + -- keep track of where/how big the shape is + for _, f in ipairs({'move', 'rotate', 'scale'}) do + local old_function = shape[f] + shape[f] = function(this, ...) + local x1,y1,x2,y2 = this:bbox() + old_function(this, ...) + self.hash:update(this, x1,y1,x2,y2, this:bbox()) + return this end end - function shape:_removeFromHash() - shape.move, shape.rotate, shape.scale = move, rotate, scale - return hash:remove(shape, self:bbox()) - end - - function shape:neighbors() - local neighbors = hash:inRange(self:bbox()) - rawset(neighbors, self, nil) - return neighbors - end - - function shape:inGroup(group) - return self._groups[group] - end - return shape end -function HC:activeShapes() - return pairs(self._active_shapes) -end - -function HC:shapesInRange(x1,y1, x2,y2) - return self._hash:inRange(x1,y1, x2,y2) -end - -function HC:addPolygon(...) - return self:addShape(newPolygonShape(...)) -end - -function HC:addRectangle(x,y,w,h) - return self:addPolygon(x,y, x+w,y, x+w,y+h, x,y+h) -end - -function HC:addCircle(cx, cy, radius) - return self:addShape(newCircleShape(cx,cy, radius)) -end - -function HC:addPoint(x,y) - return self:addShape(newPointShape(x,y)) -end - -function HC:share_group(shape, other) - for name,group in pairs(shape._groups) do - if group[other] then return true end - end - return false -end - --- check for collisions -function HC:update(dt) - -- cache for tested/colliding shapes - local tested, colliding = {}, {} - local function may_skip_test(shape, other) - return (shape == other) - or (tested[other] and tested[other][shape]) - or self._ghost_shapes[other] - or self:share_group(shape, other) - end - - -- collect active shapes. necessary, because a callback might add shapes to - -- _active_shapes, which will lead to undefined behavior (=random crashes) in - -- next() - local active = {} - for shape in self:activeShapes() do - active[shape] = shape - end - - local only_last_frame = self._colliding_only_last_frame - for shape in pairs(active) do - tested[shape] = {} - for other in self._hash:rangeIter(shape:bbox()) do - if not self._active_shapes[shape] then - -- break out of this loop is shape was removed in a callback - break - end - - if not may_skip_test(shape, other) then - local collide, sx,sy = shape:collidesWith(other) - if collide then - if not colliding[shape] then colliding[shape] = {} end - colliding[shape][other] = {sx, sy} - - -- flag shape colliding this frame and call collision callback - if only_last_frame[shape] then - only_last_frame[shape][other] = nil - end - self.on_collide(dt, shape, other, sx, sy) - end - tested[shape][other] = true - end +function HC:remove(shape) + self.hash:remove(shape, shape:bbox()) + for _, f in ipairs({'move', 'rotate', 'scale'}) do + shape[f] = function() + error(f.."() called on a removed shape") end end + return self +end - -- call stop callback on shapes that do not collide anymore - for a,reg in pairs(only_last_frame) do - for b, info in pairs(reg) do - self.on_stop(dt, a, b, info[1], info[2]) +-- shape constructors +function HC:polygon(...) + return self:register(newPolygonShape(...)) +end + +function HC:rectangle(x,y,w,h) + return self:polygon(x,y, x+w,y, x+w,y+h, x,y+h) +end + +function HC:circle(x,y,r) + return self:register(newCircleShape(x,y,r)) +end + +function HC:point(x,y) + return self:register(newPointShape(x,y)) +end + +-- collision detection +function HC:neighbors(shape) + local neighbors = self.hash:inSameCells(shape:bbox()) + rawset(neighbors, shape, nil) + return neighbors +end + +function HC:collisions(shape) + local candidates = self:neighbors(shape) + for other in pairs(candidates) do + local collides, dx, dy = shape:collidesWith(other) + if collides then + rawset(candidates, other, {dx,dy, x=dx, y=dy}) + else + rawset(candidates, other, nil) end end - - self._colliding_only_last_frame = colliding + return candidates end --- get list of shapes at point (x,y) -function HC:shapesAt(x, y) - local shapes = {} - for s in pairs(self._hash:cellAt(x,y)) do - if s:contains(x,y) then - shapes[#shapes+1] = s - end - end - return shapes -end - --- remove shape from internal tables and the hash -function HC:remove(shape, ...) - if not shape then return end - self._active_shapes[shape] = nil - self._passive_shapes[shape] = nil - self._ghost_shapes[shape] = nil - for name, group in pairs(shape._groups) do - group[shape] = nil - end - shape:_removeFromHash() - - return self:remove(...) -end - --- group support -function HC:addToGroup(group, shape, ...) - if not shape then return end - assert(self._active_shapes[shape] or self._passive_shapes[shape], - "Shape is not registered with HC") - if not self.groups[group] then self.groups[group] = {} end - self.groups[group][shape] = true - shape._groups[group] = self.groups[group] - return self:addToGroup(group, ...) -end - -function HC:removeFromGroup(group, shape, ...) - if not shape or not self.groups[group] then return end - assert(self._active_shapes[shape] or self._passive_shapes[shape], - "Shape is not registered with HC") - self.groups[group][shape] = nil - shape._groups[group] = nil - return self:removeFromGroup(group, ...) -end - -function HC:setPassive(shape, ...) - if not shape then return end - if not self._ghost_shapes[shape] then - assert(self._active_shapes[shape], "Shape is not active") - self._active_shapes[shape] = nil - self._passive_shapes[shape] = shape - end - return self:setPassive(...) -end - -function HC:setActive(shape, ...) - if not shape then return end - if not self._ghost_shapes[shape] then - assert(self._passive_shapes[shape], "Shape is not passive") - self._active_shapes[shape] = shape - self._passive_shapes[shape] = nil - end - - return self:setActive(...) -end - -function HC:setGhost(shape, ...) - if not shape then return end - assert(self._active_shapes[shape] or self._passive_shapes[shape], - "Shape is not registered with HC") - - self._active_shapes[shape] = nil - -- dont remove from passive shapes, see below - self._ghost_shapes[shape] = shape - return self:setGhost(...) -end - -function HC:setSolid(shape, ...) - if not shape then return end - assert(self._ghost_shapes[shape], "Shape not a ghost") - - -- re-register shape. passive shapes were not unregistered above, so if a shape - -- is not passive, it must be registered as active again. - if not self._passive_shapes[shape] then - self._active_shapes[shape] = shape - end - self._ghost_shapes[shape] = nil - return self:setSolid(...) -end +-- the class and the instance +HC = common_local.class('HardonCollider', HC) +local instance = common_local.instance(HC) -- the module -HC = common_local.class("HardonCollider", HC) -local function new(...) - return common_local.instance(HC, ...) -end +return setmetatable({ + new = function(...) return common_local.instance(HC, ...) end, + resetHash = function(...) return instance:resetHash(...) end, + register = function(...) return instance:register(...) end, + remove = function(...) return instance:remove(...) end, -return setmetatable({HardonCollider = HC, new = new}, - {__call = function(_,...) return new(...) end}) + polygon = function(...) return instance:polygon(...) end, + rectangle = function(...) return instance:rectangle(...) end, + circle = function(...) return instance:circle(...) end, + point = function(...) return instance:point(...) end, + + neighbors = function(...) return instance:neighbors(...) end, + collisions = function(...) return instance:collisions(...) end, + hash = function() return instance.hash end, +}, {__call = function(_, ...) return common_local.instance(HC, ...) end}) diff --git a/spatialhash.lua b/spatialhash.lua index eba3794..6be99ae 100644 --- a/spatialhash.lua +++ b/spatialhash.lua @@ -64,7 +64,21 @@ function Spatialhash:cellAt(x,y) return self:cell(self:cellCoords(x,y)) end -function Spatialhash:inRange(x1,y1, x2,y2) + -- get all shapes +function Spatialhash:shapes() + local set = {} + for i,row in pairs(self.cells) do + for k,cell in pairs(row) do + for obj in pairs(cell) do + rawset(set, obj, obj) + end + end + end + return set +end + +-- get all shapes that are in the same cells as the bbox x1,y1 '--. x2,y2 +function Spatialhash:inSameCells(x1,y1, x2,y2) local set = {} x1, y1 = self:cellCoords(x1, y1) x2, y2 = self:cellCoords(x2, y2) @@ -78,11 +92,7 @@ function Spatialhash:inRange(x1,y1, x2,y2) return set end -function Spatialhash:rangeIter(...) - return pairs(self:inRange(...)) -end - -function Spatialhash:insert(obj, x1, y1, x2, y2) +function Spatialhash:register(obj, x1, y1, x2, y2) x1, y1 = self:cellCoords(x1, y1) x2, y2 = self:cellCoords(x2, y2) for i = x1,x2 do