Initial commit

This commit is contained in:
Matthias Richter 2011-01-13 14:38:36 +01:00
commit 3ad0242195
7 changed files with 1107 additions and 0 deletions

1
README Normal file
View File

@ -0,0 +1 @@
coming soon...

71
class.lua Normal file
View File

@ -0,0 +1,71 @@
--[[
Copyright (c) 2011 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
module(..., package.seeall)
local function __NULL__() end
function new(constructor)
-- check name and constructor
local name = '<unnamed class>'
if type(constructor) == "table" then
if constructor.name then name = constructor.name end
constructor = constructor[1]
end
assert(not constructor or type(constructor) == "function",
string.format('%s: constructor has to be nil or a function', name))
-- build class
local c = {}
c.__index = c
c.__tostring = function() return string.format("<instance of %s>", name) end
c.construct = constructor or __NULL__
c.Construct = constructor or __NULL__
c.inherit = Inherit
c.Inherit = Inherit
local meta = {
__call = function(self, ...)
local obj = {}
self.construct(obj, ...)
return setmetatable(obj, self)
end,
__tostring = function() return tostring(name) end
}
return setmetatable(c, meta)
end
function Inherit(class, interface, ...)
if not interface then return end
-- __index and construct are not overwritten as for them class[name] is defined
for name, func in pairs(interface) do
if not class[name] and type(func) == "function" then
class[name] = func
end
end
Inherit(class, ...)
end

189
init.lua Normal file
View File

@ -0,0 +1,189 @@
--[[
Copyright (c) 2011 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local _PATH = (...):gsub("(%.init$)?", "").."."
local vector = require(_PATH .. 'vector')
module(..., package.seeall)
require(_PATH .. 'shape')
require(_PATH .. 'polygon')
require(_PATH .. 'spatialhash')
vector = vector.new
local is_initialized = false
local hash = nil
local shapes = {}
local shape_ids = {}
local function __NOT_INIT() error("Not yet initialized") end
local function __NULL() end
local cb_start, cb_persist, cb_stop = __NOT_INIT, __NOT_INIT, __NOT_INIT
function init(cell_size, callback_start, callback_persist, callback_stop)
cb_start = callback_start or __NULL
cb_persist = callback_persist or __NULL
cb_stop = callback_stop or __NULL
hash = Spatialhash(cell_size)
is_initialized = true
end
function setCallbacks(start,persist,stop)
local tbl = start
if type(start) == "function" then
tbl = {start = start, persist = persist, stop = stop}
end
if tbl.start then cb_start = tbl.start end
if tbl.persist then cb_persist = tbl.persist end
if tbl.stop then cb_stop = tbl.stop end
end
local function newShape(shape, ul,lr)
shapes[#shapes+1] = shape
shape_ids[shape] = #shapes
hash:insert(shape, ul,lr)
return shape
end
-- create polygon shape and add it to internal structures
function newPolygonShape(...)
assert(is_initialized, "Not properly initialized!")
local poly = Polygon(...)
local shape
if not poly:isConvex() then
shape = CompoundShape( poly, poly:splitConvex() )
else
shape = PolygonShape( poly )
end
-- replace shape member function with a function that also updates
-- the hash
local function hash_aware_member(oldfunc)
return function(self, ...)
local x1,y1, x2,y2 = self._polygon:getBBox()
oldfunc(self, ...)
local x3,y3, x4,y4 = self._polygon:getBBox()
hash:update(self, vector(x1,y1), vector(x2,y2), vector(x3,y3), vector(x4,y4))
end
end
shape.move = hash_aware_member(shape.move)
shape.rotate = hash_aware_member(shape.rotate)
local x1,y1, x2,y2 = poly:getBBox()
return newShape(shape, vector(x1,y1), vector(x2,y2))
end
-- create new polygon approximation of a circle
function newCircleShape(cx, cy, radius)
assert(is_initialized, "Not properly initialized!")
local shape = CircleShape(cx,cy, radius)
local oldmove = shape.move
function shape:move(x,y)
local r = vector(self._radius, self._radius)
local c1 = self._center
oldmove(self,x,y)
local c2 = self._center
hash:update(self, c1-r, c1+r, c2-r, c2+r)
end
local c,r = shape._center, vector(radius,radius)
return newShape(shape, c-r, c+r)
end
-- get unique indentifier for an unordered pair of shapes, i.e.:
-- collision_id(s,t) = collision_id(t,s)
local function collision_id(s,t)
local i,k = shape_ids[s], shape_ids[t]
if i < k then i,k = k,i end
return string.format("%d,%d", i,k)
end
-- check for collisions
local colliding_last_frame = {}
function update(dt)
-- collect colliding shapes
local tested, colliding = {}, {}
for _,s in pairs(shapes) do
local neighbors
if s._type == Shape.CIRCLE then
local c,r = s._center, vector(s._radius, s._radius)
neighbors = hash:getNeighbors(s, c-r, c+r)
else
local x1,y1, x2,y2 = s._polygon:getBBox()
neighbors = hash:getNeighbors(s, vector(x1,y2), vector(x2,y2))
end
for _,t in ipairs(neighbors) do
-- check if shapes have already been tested for collision
local id = collision_id(s,t)
if not tested[id] then
local collide, sep = s:collidesWith(t)
if collide then
colliding[id] = {s, t, sep.x, sep.y}
end
tested[id] = true
end
end
end
-- call colliding callbacks on colliding shapes
for id,info in pairs(colliding) do
if colliding_last_frame[id] then
colliding_last_frame[id] = nil
cb_persist( dt, unpack(info) )
else
cb_start( dt, unpack(info) )
end
end
-- call stop callback on shapes that do not collide
-- anymore
for _,info in pairs(colliding_last_frame) do
cb_stop( dt, unpack(info) )
end
colliding_last_frame = colliding
end
-- remove shape from internal tables and the hash
function removeShape(shape)
local id = shape_ids[shape]
shapes[id] = nil
shape_ids[shape] = nil
if shape.type == Shape.CIRCLE then
local c,r = shape._center, vector(shape._radius, shape._radius)
hash:remove(shape, c-r, c+r)
else
local x1,y1, x2,y2 = poly:getBBox()
hash:remove(shape, vector(x1,y1), vector(x2,y2))
end
for id,info in pairs(colliding_last_frame) do
if info[1] == shape or info[2] == shape then
colliding_last_frame[id] = nil
end
end
end

316
polygon.lua Normal file
View File

@ -0,0 +1,316 @@
--[[
Copyright (c) 2011 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local _PATH = (...):gsub("polygon$", "")
local Class = require(_PATH .. 'class')
local vector = require(_PATH .. 'vector')
Class = Class.new
vector = vector.new
----------------------------
-- Private helper functions
--
-- create vertex list of coordinate pairs
local function toVertexList(vertices, x,y, ...)
if not x or not y then return vertices end -- no more arguments
vertices[#vertices + 1] = vector(x, y) -- set vertex
return toVertexList(vertices, ...) -- recurse
end
-- returns true if three points lie on a line
local function areCollinear(p,q,r)
return (q - p):cross(r - p) == 0
end
-- remove vertices that lie on a line
local function removeCollinear(vertices)
local ret = {}
for k=1,#vertices do
local i = k > 1 and k - 1 or #vertices
local l = k < #vertices and k + 1 or 1
if not areCollinear(vertices[i], vertices[k], vertices[l]) then
ret[#ret+1] = vertices[k]
end
end
return ret
end
-- get index of rightmost vertex (for testing orientation)
local function getIndexOfleftmost(vertices)
local idx = 1
for i = 2,#vertices do
if vertices[i].x < vertices[idx].x then
idx = i
end
end
return idx
end
-- returns true if three points make a counter clockwise turn
local function ccw(p, q, r)
return (q - p):cross(r - p) >= 0
end
-- unpack vertex coordinates, i.e. {x=p, y=q}, ... -> p,q, ...
local function unpackHelper(v, ...)
if not v then return end
return v.x,v.y,unpackHelper(...)
end
-- test if a point lies inside of a triangle using cramers rule
local function pointInTriangle(q, p1,p2,p3)
local v1,v2 = p2 - p1, p3 - p1
local qp = q - p1
local dv = v1:cross(v2)
local l = qp:cross(v2) / dv
if l <= 0 then return false end
local m = v1:cross(qp) / dv
if m <= 0 then return false end
return l+m < 1
end
-- returns starting indices of shared edge, i.e. if p and q share the
-- edge with indices p1,p2 of p and q1,q2 of q, the return value is p1,q1
local function getSharedEdge(p,q)
local vertices = {}
for i,v in ipairs(q) do vertices[ tostring(v) ] = i end
for i,v in ipairs(p) do
local w = (i == #p) and p[1] or p[i+1]
if vertices[ tostring(v) ] and vertices[ tostring(w) ] then
return i, vertices[ tostring(v) ]
end
end
end
-----------------
-- Polygon class
--
Polygon = Class{name = "Polygon", function(self, ...)
local vertices = removeCollinear( toVertexList({}, ...) )
assert(#vertices >= 3, "Need at least 3 non collinear points to build polygon (got "..#vertices..")")
-- assert polygon is oriented counter clockwise
local r = getIndexOfleftmost(vertices)
local q = r > 1 and r - 1 or #vertices
local s = r < #vertices and r + 1 or 1
if not ccw(vertices[q], vertices[r], vertices[s]) then -- reverse order if polygon is not ccw
local tmp = {}
for i=#vertices,1,-1 do
tmp[#tmp + 1] = vertices[i]
end
vertices = tmp
end
self.vertices = vertices
-- make vertices immutable
setmetatable(self.vertices, {__newindex = function() error("Thou shall not change a polygons vertices!") end})
-- compute polygon area and centroid
self.area = vertices[#vertices]:cross(vertices[1])
for i = 1,#vertices-1 do
self.area = self.area + vertices[i]:cross(vertices[i+1])
end
self.area = self.area / 2
local p,q = vertices[#vertices], vertices[1]
local det = p:cross(q)
self.centroid = vector((p.x+q.x) * det, (p.y+q.y) * det)
for i = 1,#vertices-1 do
p,q = vertices[i], vertices[i+1]
det = p:cross(q)
self.centroid.x = self.centroid.x + (p.x+q.x) * det
self.centroid.y = self.centroid.y + (p.y+q.y) * det
end
self.centroid = self.centroid / (6 * self.area)
end}
-- return vertices as x1,y1,x2,y2, ..., xn,yn
function Polygon:unpack()
return unpackHelper( unpack(self.vertices) )
end
-- deep copy of the polygon
function Polygon:clone()
return Polygon( self:unpack() )
end
-- get bounding box
function Polygon:getBBox()
local ul = self.vertices[1]:clone()
local lr = ul:clone()
for i=2,#self.vertices do
local p = self.vertices[i]
if ul.x > p.x then ul.x = p.x end
if ul.y > p.y then ul.y = p.y end
if lr.x < p.x then lr.x = p.x end
if lr.y < p.y then lr.y = p.y end
end
return ul.x,ul.y, lr.x,lr.y
end
-- a polygon is convex if all edges are oriented ccw
function Polygon:isConvex()
local function isConvex()
local v = self.vertices
if #v == 3 then return true end
if not ccw(v[#v], v[1], v[2]) then
return false
end
for i = 2,#v-1 do
if not ccw(v[i-1], v[i], v[i+1]) then
return false
end
end
if not ccw(v[#v-1], v[#v], v[1]) then
return false
end
return true
end
-- replace function so that this will only be computed once
local status = isConvex()
self.isConvex = function() return status end
return status
end
function Polygon:move(direction)
for i,v in ipairs(self.vertices) do
self.vertices[i] = self.vertices[i] + direction
end
self.centroid = self.centroid + direction
end
function Polygon:rotate(angle, center)
local center = center or self.centroid
for i,v in ipairs(self.vertices) do
self.vertices[i] = (self.vertices[i] - center):rotate_inplace(angle) + center
end
end
-- triangulation by the method of kong
function Polygon:triangulate()
if #self.vertices == 3 then return {self:clone()} end
local triangles = {} -- list of triangles to be returned
local concave = {} -- list of concave edges
local adj = {} -- vertex adjacencies
local vertices = self.vertices
-- retrieve adjacencies as the rest will be easier to implement
for i,p in ipairs(vertices) do
local l = (i == 1) and vertices[#vertices] or vertices[i-1]
local r = (i == #vertices) and vertices[1] or vertices[i+1]
adj[p] = {p = p, l = l, r = r} -- point, left and right neighbor
-- test if vertex is a concave edge
if not ccw(l,p,r) then concave[p] = p end
end
-- and ear is an edge of the polygon that contains no other
-- vertex of the polygon
local function isEar(p1,p2,p3)
if not ccw(p1,p2,p3) then return false end
for q,_ in pairs(concave) do
if pointInTriangle(q, p1,p2,p3) then return false end
end
return true
end
-- main loop
local nPoints, skipped = #vertices, 0
local p = adj[ vertices[2] ]
while nPoints > 3 do
if not concave[p.p] and isEar(p.l, p.p, p.r) then
triangles[#triangles+1] = Polygon( unpackHelper(p.l, p.p, p.r) )
if concave[p.l] and ccw(adj[p.l].l, p.l, p.r) then
concave[p.l] = nil
end
if concave[p.r] and ccw(p.l, p.r, adj[p.r].r) then
concave[p.r] = nil
end
-- remove point from list
adj[p.p] = nil
adj[p.l].r = p.r
adj[p.r].l = p.l
nPoints = nPoints - 1
skipped = 0
p = adj[p.l]
else
p = adj[p.r]
skipped = skipped + 1
assert(skipped <= nPoints, "Cannot triangulate polygon (is the polygon intersecting itself?)")
end
end
triangles[#triangles+1] = Polygon( unpackHelper(p.l, p.p, p.r) )
return triangles
end
-- return merged polygon if possible or nil otherwise
function Polygon:mergedWith(other)
local p,q = getSharedEdge(self.vertices, other.vertices)
if not (p and q) then return nil end
local ret = {}
for i = 1, p do ret[#ret+1] = self.vertices[i] end
for i = 2, #other.vertices-1 do
local k = i + q - 1
if k > #other.vertices then k = k - #other.vertices end
ret[#ret+1] = other.vertices[k]
end
for i = p+1,#self.vertices do ret[#ret+1] = self.vertices[i] end
return Polygon( unpackHelper( unpack(ret) ) )
end
-- split polygon into convex polygons.
-- note that this won't be the optimal split in most cases, as
-- finding the optimal split is a NP hard problem.
-- the method is to first triangulate and then greedily merge
-- the triangles.
function Polygon:splitConvex()
-- edge case: polygon is a triangle or already convex
if #self.vertices <= 3 or self:isConvex() then return {self:clone()} end
local convex = self:triangulate()
local i = 1
repeat
local p = convex[i]
local k = i + 1
while k <= #convex do
local _, merged = pcall(function() return p:mergedWith(convex[k]) end)
if merged and merged:isConvex() then
convex[i] = merged
p = convex[i]
table.remove(convex, k)
else
k = k + 1
end
end
i = i + 1
until i >= #convex
return convex
end

257
shape.lua Normal file
View File

@ -0,0 +1,257 @@
--[[
Copyright (c) 2011 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local _PATH = (...):gsub("shape$", "")
local Class = require(_PATH .. 'class')
local vector = require(_PATH .. 'vector')
Class = Class.new
vector = vector.new
local function combine_axes(a, b)
local in_a = {}
for i = 1,#a do in_a[ tostring(a[i]) ] = true end
for i = 1,#b do
if not in_a[ tostring(b[i]) ] then
a[#a+1] = b[i]
end
end
return a
end
local function SAT(axis_table, shape_one, shape_two)
local sep,min_overlap = vector(0,0),math.huge
for _,axis in ipairs(axis_table) do
local l1,r1 = shape_one:projectOn(axis)
local l2,r2 = shape_two:projectOn(axis)
local a,b = math.max(l1,l2), math.min(r1,r2)
if b < a then
return false
end
local overlap = b-a
if overlap < min_overlap then
sep, min_overlap = axis * -overlap, overlap
end
end
return true, sep
end
---------------
-- Base class
--
Shape = Class{name = 'Shape', function(self, t)
self._type = t
end}
-- supported shapes
Shape.POLYGON = setmetatable({}, {__tostring = function() return 'POLYGON' end})
Shape.COMPOUND = setmetatable({}, {__tostring = function() return 'COMPOUND' end})
Shape.CIRCLE = setmetatable({}, {__tostring = function() return 'CIRCLE' end})
-------------------
-- Convex polygon
--
PolygonShape = Class{name = 'PolygonShape', function(self, polygon)
Shape.construct(self, Shape.POLYGON)
assert(polygon:isConvex(), "Polygon is not convex.")
self._polygon = polygon
end}
PolygonShape:inherit(Shape)
function PolygonShape:getAxes()
local axes = {}
local vert = self._polygon.vertices
for i = 1,#vert-1 do
axes[#axes+1] = (vert[i+1]-vert[i]):perpendicular():normalize_inplace()
end
axes[#axes+1] = (vert[1]-vert[#vert]):perpendicular():normalize_inplace()
return axes
end
function PolygonShape:projectOn(axis)
local vertices = self._polygon.vertices
local left, right = math.huge, -math.huge
for i = 1,#vertices do
local projection = vertices[i] * axis -- same as vertices[i]:projectOn(axis) * axis
if projection < left then
left = projection
end
if projection > right then
right = projection
end
end
return left, right
end
function PolygonShape:collidesWith(other)
if other._type ~= Shape.POLYGON then
return other:collidesWith(self)
end
-- else: type is POLYGON, use the SAT
return SAT(combine_axes(self:getAxes(), other:getAxes()), self, other)
end
function PolygonShape:draw(mode)
local mode = mode or 'line'
love.graphics.polygon(mode, self._polygon:unpack())
end
function PolygonShape:centroid()
return self._polygon.centroid:unpack()
end
function PolygonShape:move(x,y)
-- y not given => x is a vector
if y then x = vector(x,y) end
self._polygon:move(x)
end
function PolygonShape:rotate(angle, center)
self._polygon:rotate(angle, center)
end
---------------------------------
-- Concave (but simple) polygon
--
CompoundShape = Class{name = 'CompoundShape', function(self, poly)
Shape.construct(self, Shape.COMPOUND)
self._polygon = poly
self._shapes = poly:splitConvex()
for i,s in ipairs(self._shapes) do
self._shapes[i] = PolygonShape(s)
end
end}
CompoundShape:inherit(Shape)
function CompoundShape:collidesWith(other)
local sep, collide = vector(0,0), false
for _,s in ipairs(self._shapes) do
local status, separating_vector = s:collidesWith(other)
collide = collide or status
if status then
sep = sep + separating_vector
end
end
return collide, sep
end
function CompoundShape:draw(mode)
local mode = mode or 'line'
if mode == 'line' then
love.graphics.polygon('line', self._polygon:unpack())
else
for _,p in ipairs(self._shapes) do
love.graphics.polygon(mode, p._polygon:unpack())
end
end
end
function CompoundShape:centroid()
return self._polygon.centroid:unpack()
end
function CompoundShape:move(x,y)
-- y not give => x is a vector
if y then x = vector(x,y) end
self._polygon:move(x)
for _,p in ipairs(self._shapes) do
p:move(x)
end
end
function CompoundShape:rotate(angle)
self._polygon:rotate(angle)
for _,p in ipairs(self._shapes) do
p:rotate(angle, self._polygon.centroid)
end
end
-------------------
-- Perfect circle
--
CircleShape = Class{name = 'CircleShape', function(self, cx,cy, radius)
Shape.construct(self, Shape.CIRCLE)
self._center = vector(cx,cy)
self._radius = radius
end}
CircleShape:inherit(Shape)
function CircleShape:collidesWith(other)
if other._type == Shape.CIRCLE then
return SAT({(other._center - self._center):normalize_inplace()}, self, other)
elseif other._type == Shape.COMPOUND then
return other:collidesWith(self)
end
-- else: other._type == POLYGON
-- retrieve closest edge to center
local function getClosest(center, points, distOld, k, i, inc)
local distNew = (points[i] - center):len2()
if distOld < distNew then return points[k],distOld end
k, i = i, i + inc
if i > #points then i = 1 end
if i < 1 then i = #points end
return getClosest(center, points, distNew, k, i, inc)
end
local closestLeft,dl = getClosest(self._center, other._polygon.vertices, math.huge, 1,2, 1)
local closestRight,dr = getClosest(self._center, other._polygon.vertices, math.huge, 2,1, -1)
local closest = dl < dr and closestLeft or closestRight
return SAT(combine_axes(other:getAxes(), {(closest - self._center):normalize_inplace()}), self, other)
end
function CircleShape:draw(mode, segments)
local segments = segments or math.max(3, math.floor(math.pi * math.log(self._radius)))
love.graphics.circle(mode, self._center.x, self._center.y, self._radius, segments)
end
function CircleShape:centroid()
return self._center:unpack()
end
function CircleShape:move(x,y)
-- y not given => x is a vector
if y then x = vector(x,y) end
self._center = self._center + x
end
function CircleShape:rotate(angle)
-- yeah, right
end
function CircleShape:projectOn(axis)
-- v:projectOn(a) * a = v * a (see PolygonShape)
-- therefore: (c +- a*r) * a = c*a +- |a|^2 * r
local center = self._center * axis
local shift = self._radius * axis:len2()
return center - shift, center + shift
end
function CircleShape:centroid()
return self._center:unpack()
end

128
spatialhash.lua Normal file
View File

@ -0,0 +1,128 @@
--[[
Copyright (c) 2011 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local _PATH = (...):gsub("spatialhash$", "")
local Class = require(_PATH .. 'class')
local vector = require(_PATH .. 'vector')
Class = Class.new
vector = vector.new
-- special cell accesor metamethods, so vectors are converted
-- to a string before using as keys
local cell_meta = {}
function cell_meta.__newindex(tbl, key, val)
return rawset(tbl, tostring(key), val)
end
function cell_meta.__index(tbl, key)
local key = tostring(key)
local ret = rawget(tbl, key)
if not ret then
ret = setmetatable({}, {__mode = "kv"})
rawset(tbl, key, ret)
end
return ret
end
Spatialhash = Class{name = 'Spatialhash', function(self, cell_size)
self.cell_size = cell_size or 100
self.cells = setmetatable({}, cell_meta)
end}
function Spatialhash:cellCoords(v)
local v = v / self.cell_size
v.x, v.y = math.floor(v.x), math.floor(v.y)
return v
end
function Spatialhash:cell(v)
return self.cells[ self:cellCoords(v) ]
end
function Spatialhash:insert(obj, ul, lr)
local ul = self:cellCoords(ul)
local lr = self:cellCoords(lr)
for i = ul.x,lr.x do
for k = ul.y,lr.y do
self.cells[vector(i,k)][obj] = obj
end
end
end
function Spatialhash:remove(obj, ul, lr)
-- no bbox given. => must check all cells
if not ul or not lr then
for _,cell in pairs(self.cells) do
cell[obj] = nil
end
return
end
local ul = self:cellCoords(ul)
local lr = self:cellCoords(lr)
-- els: remove only from bbox
for i = ul.x,lr.x do
for k = ul.y,lr.y do
self.cells[vector(i,k)][obj] = nil
end
end
end
-- update an objects position
function Spatialhash:update(obj, ul_old, lr_old, ul_new, lr_new)
-- cells where the object has to be updated
local xmin, xmax = math.min(ul_old.x, ul_new.x), math.max(lr_old.x, lr_new.x)
local ymin, ymax = math.min(ul_old.y, ul_new.y), math.max(lr_old.y, lr_new.y)
-- check for regions that are only occupied by either the old or the new bbox
-- remove or add accordingly
for i = xmin,xmax do
for k = ymin,ymax do
local region_old = i >= ul_old.x and i <= ul_old.x and k >= ul_old.y and k <= ul_old.y
local region_new = i >= ul_new.x and i <= ul_new.x and k >= ul_new.y and k <= ul_new.y
if region_new and not region_old then
self.cells[vector(i,k)][obj] = obj
elseif not region_new and region_old then
self.cells[vector(i,k)][obj] = nil
end
end
end
end
function Spatialhash:getNeighbors(obj, ul, lr)
local ul = self:cellCoords(ul)
local lr = self:cellCoords(lr)
local set,items = {}, {}
for i = ul.x,lr.x do
for k = ul.y,lr.y do
local cell = self.cells[ vector(i,k) ] or {}
for other,_ in pairs(cell) do
if other ~= obj then set[other] = other end
end
end
end
for other,_ in pairs(set) do items[#items+1] = other end
return items
end

145
vector.lua Normal file
View File

@ -0,0 +1,145 @@
--[[
Copyright (c) 2011 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local setmetatable = setmetatable
local tonumber = tonumber
local type = type
local sqrt = math.sqrt
local cos = math.cos
local sin = math.sin
module(...)
local Vector = {}
Vector.__index = Vector
function new(x,y)
local v = {x = x or 0, y = y or 0}
setmetatable(v, Vector)
return v
end
local vector = new
function isvector(v)
return getmetatable(v) == Vector
end
function Vector:clone()
return vector(self.x, self.y)
end
function Vector:unpack()
return self.x, self.y
end
function Vector:__tostring()
return "("..tonumber(self.x)..","..tonumber(self.y)..")"
end
function Vector.__unm(a)
return vector(-a.x, -a.y)
end
function Vector.__add(a,b)
return vector(a.x+b.x, a.y+b.y)
end
function Vector.__sub(a,b)
return vector(a.x-b.x, a.y-b.y)
end
function Vector.__mul(a,b)
if type(a) == "number" then
return vector(a*b.x, a*b.y)
elseif type(b) == "number" then
return vector(b*a.x, b*a.y)
else
return a.x*b.x + a.y*b.y
end
end
function Vector.__div(a,b)
return vector(a.x / b, a.y / b)
end
function Vector.__eq(a,b)
return a.x == b.x and a.y == b.y
end
function Vector.__lt(a,b)
return a.x < b.x or (a.x == b.x and a.y < b.y)
end
function Vector.__le(a,b)
return a.x <= b.x and a.y <= b.y
end
function Vector.permul(a,b)
return vector(a.x*b.x, a.y*b.y)
end
function Vector:len2()
return self * self
end
function Vector:len()
return sqrt(self*self)
end
function Vector.dist(a, b)
return (b-a):len()
end
function Vector:normalize_inplace()
local l = self:len()
self.x, self.y = self.x / l, self.y / l
return self
end
function Vector:normalized()
return self / self:len()
end
function Vector:rotate_inplace(phi)
local c, s = cos(phi), sin(phi)
self.x, self.y = c * self.x - s * self.y, s * self.x + c * self.y
return self
end
function Vector:rotated(phi)
return self:clone():rotate_inplace(phi)
end
function Vector:perpendicular()
return vector(-self.y, self.x)
end
function Vector:projectOn(v)
return (self * v) * v / v:len2()
end
function Vector:cross(other)
return self.x * other.y - self.y * other.x
end