Monkune/lib/windfield/init.lua

930 lines
38 KiB
Lua
Raw Permalink Normal View History

2021-01-07 01:29:06 -06:00
--[[
The MIT License (MIT)
Copyright (c) 2018 SSYGEN
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.
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 = ... .. '.'
local wf = {}
wf.Math = require(path .. 'mlib.mlib')
World = {}
World.__index = World
function wf.newWorld(xg, yg, sleep)
local world = wf.World.new(wf, xg, yg, sleep)
world.box2d_world:setCallbacks(world.collisionOnEnter, world.collisionOnExit, world.collisionPre, world.collisionPost)
world:collisionClear()
world:addCollisionClass('Default')
-- Points all box2d_world functions to this wf.World object
-- This means that the user can call world:setGravity for instance without having to say world.box2d_world:setGravity
for k, v in pairs(world.box2d_world.__index) do
if k ~= '__gc' and k ~= '__eq' and k ~= '__index' and k ~= '__tostring' and k ~= 'update' and k ~= 'destroy' and k ~= 'type' and k ~= 'typeOf' then
world[k] = function(self, ...)
return v(self.box2d_world, ...)
end
end
end
return world
end
function World.new(wf, xg, yg, sleep)
local self = {}
local settings = settings or {}
self.wf = wf
self.draw_query_for_n_frames = 10
self.query_debug_drawing_enabled = false
self.explicit_collision_events = false
self.collision_classes = {}
self.masks = {}
self.is_sensor_memo = {}
self.query_debug_draw = {}
love.physics.setMeter(32)
self.box2d_world = love.physics.newWorld(xg, yg, sleep)
return setmetatable(self, World)
end
function World:update(dt)
self:collisionEventsClear()
self.box2d_world:update(dt)
end
function World:draw(alpha)
-- get the current color values to reapply
local r, g, b, a = love.graphics.getColor()
-- alpha value is optional
alpha = alpha or 255
-- Colliders debug
love.graphics.setColor(222, 222, 222, alpha)
local bodies = self.box2d_world:getBodies()
for _, body in ipairs(bodies) do
local fixtures = body:getFixtures()
for _, fixture in ipairs(fixtures) do
if fixture:getShape():type() == 'PolygonShape' then
love.graphics.polygon('line', body:getWorldPoints(fixture:getShape():getPoints()))
elseif fixture:getShape():type() == 'EdgeShape' or fixture:getShape():type() == 'ChainShape' then
local points = {body:getWorldPoints(fixture:getShape():getPoints())}
for i = 1, #points, 2 do
if i < #points-2 then love.graphics.line(points[i], points[i+1], points[i+2], points[i+3]) end
end
elseif fixture:getShape():type() == 'CircleShape' then
local body_x, body_y = body:getPosition()
local shape_x, shape_y = fixture:getShape():getPoint()
local r = fixture:getShape():getRadius()
love.graphics.circle('line', body_x + shape_x, body_y + shape_y, r, 360)
end
end
end
love.graphics.setColor(255, 255, 255, alpha)
-- Joint debug
love.graphics.setColor(222, 128, 64, alpha)
local joints = self.box2d_world:getJoints()
for _, joint in ipairs(joints) do
local x1, y1, x2, y2 = joint:getAnchors()
if x1 and y1 then love.graphics.circle('line', x1, y1, 4) end
if x2 and y2 then love.graphics.circle('line', x2, y2, 4) end
end
love.graphics.setColor(255, 255, 255, alpha)
-- Query debug
love.graphics.setColor(64, 64, 222, alpha)
for _, query_draw in ipairs(self.query_debug_draw) do
query_draw.frames = query_draw.frames - 1
if query_draw.type == 'circle' then
love.graphics.circle('line', query_draw.x, query_draw.y, query_draw.r)
elseif query_draw.type == 'rectangle' then
love.graphics.rectangle('line', query_draw.x, query_draw.y, query_draw.w, query_draw.h)
elseif query_draw.type == 'line' then
love.graphics.line(query_draw.x1, query_draw.y1, query_draw.x2, query_draw.y2)
elseif query_draw.type == 'polygon' then
local triangles = love.math.triangulate(query_draw.vertices)
for _, triangle in ipairs(triangles) do love.graphics.polygon('line', triangle) end
end
end
for i = #self.query_debug_draw, 1, -1 do
if self.query_debug_draw[i].frames <= 0 then
table.remove(self.query_debug_draw, i)
end
end
love.graphics.setColor(r, g, b, a)
end
function World:setQueryDebugDrawing(value)
self.query_debug_drawing_enabled = value
end
function World:setExplicitCollisionEvents(value)
self.explicit_collision_events = value
end
function World:addCollisionClass(collision_class_name, collision_class)
if self.collision_classes[collision_class_name] then error('Collision class ' .. collision_class_name .. ' already exists.') end
if self.explicit_collision_events then
self.collision_classes[collision_class_name] = collision_class or {}
else
self.collision_classes[collision_class_name] = collision_class or {}
self.collision_classes[collision_class_name].enter = {}
self.collision_classes[collision_class_name].exit = {}
self.collision_classes[collision_class_name].pre = {}
self.collision_classes[collision_class_name].post = {}
for c_class_name, _ in pairs(self.collision_classes) do
table.insert(self.collision_classes[collision_class_name].enter, c_class_name)
table.insert(self.collision_classes[collision_class_name].exit, c_class_name)
table.insert(self.collision_classes[collision_class_name].pre, c_class_name)
table.insert(self.collision_classes[collision_class_name].post, c_class_name)
end
for c_class_name, _ in pairs(self.collision_classes) do
table.insert(self.collision_classes[c_class_name].enter, collision_class_name)
table.insert(self.collision_classes[c_class_name].exit, collision_class_name)
table.insert(self.collision_classes[c_class_name].pre, collision_class_name)
table.insert(self.collision_classes[c_class_name].post, collision_class_name)
end
end
self:collisionClassesSet()
end
function World:collisionClassesSet()
self:generateCategoriesMasks()
self:collisionClear()
local collision_table = self:getCollisionCallbacksTable()
for collision_class_name, collision_list in pairs(collision_table) do
for _, collision_info in ipairs(collision_list) do
if collision_info.type == 'enter' then self:addCollisionEnter(collision_class_name, collision_info.other) end
if collision_info.type == 'exit' then self:addCollisionExit(collision_class_name, collision_info.other) end
if collision_info.type == 'pre' then self:addCollisionPre(collision_class_name, collision_info.other) end
if collision_info.type == 'post' then self:addCollisionPost(collision_class_name, collision_info.other) end
end
end
self:collisionEventsClear()
end
function World:collisionClear()
self.collisions = {}
self.collisions.on_enter = {}
self.collisions.on_enter.sensor = {}
self.collisions.on_enter.non_sensor = {}
self.collisions.on_exit = {}
self.collisions.on_exit.sensor = {}
self.collisions.on_exit.non_sensor = {}
self.collisions.pre = {}
self.collisions.pre.sensor = {}
self.collisions.pre.non_sensor = {}
self.collisions.post = {}
self.collisions.post.sensor = {}
self.collisions.post.non_sensor = {}
end
function World:collisionEventsClear()
local bodies = self.box2d_world:getBodies()
for _, body in ipairs(bodies) do
local collider = body:getFixtures()[1]:getUserData()
collider:collisionEventsClear()
end
end
function World:addCollisionEnter(type1, type2)
if not self:isCollisionBetweenSensors(type1, type2) then
table.insert(self.collisions.on_enter.non_sensor, {type1 = type1, type2 = type2})
else table.insert(self.collisions.on_enter.sensor, {type1 = type1, type2 = type2}) end
end
function World:addCollisionExit(type1, type2)
if not self:isCollisionBetweenSensors(type1, type2) then
table.insert(self.collisions.on_exit.non_sensor, {type1 = type1, type2 = type2})
else table.insert(self.collisions.on_exit.sensor, {type1 = type1, type2 = type2}) end
end
function World:addCollisionPre(type1, type2)
if not self:isCollisionBetweenSensors(type1, type2) then
table.insert(self.collisions.pre.non_sensor, {type1 = type1, type2 = type2})
else table.insert(self.collisions.pre.sensor, {type1 = type1, type2 = type2}) end
end
function World:addCollisionPost(type1, type2)
if not self:isCollisionBetweenSensors(type1, type2) then
table.insert(self.collisions.post.non_sensor, {type1 = type1, type2 = type2})
else table.insert(self.collisions.post.sensor, {type1 = type1, type2 = type2}) end
end
function World:doesType1IgnoreType2(type1, type2)
local collision_ignores = {}
for collision_class_name, collision_class in pairs(self.collision_classes) do
collision_ignores[collision_class_name] = collision_class.ignores or {}
end
local all = {}
for collision_class_name, _ in pairs(collision_ignores) do
table.insert(all, collision_class_name)
end
local ignored_types = {}
for _, collision_class_type in ipairs(collision_ignores[type1]) do
if collision_class_type == 'All' then
for _, collision_class_name in ipairs(all) do
table.insert(ignored_types, collision_class_name)
end
else table.insert(ignored_types, collision_class_type) end
end
for key, _ in pairs(collision_ignores[type1]) do
if key == 'except' then
for _, except_type in ipairs(collision_ignores[type1].except) do
for i = #ignored_types, 1, -1 do
if ignored_types[i] == except_type then table.remove(ignored_types, i) end
end
end
end
end
for _, ignored_type in ipairs(ignored_types) do
if ignored_type == type2 then return true end
end
end
function World:isCollisionBetweenSensors(type1, type2)
if not self.is_sensor_memo[type1] then self.is_sensor_memo[type1] = {} end
if not self.is_sensor_memo[type1][type2] then self.is_sensor_memo[type1][type2] = (self:doesType1IgnoreType2(type1, type2) or self:doesType1IgnoreType2(type2, type1)) end
if self.is_sensor_memo[type1][type2] then return true
else return false end
end
-- https://love2d.org/forums/viewtopic.php?f=4&t=75441
function World:generateCategoriesMasks()
local collision_ignores = {}
for collision_class_name, collision_class in pairs(self.collision_classes) do
collision_ignores[collision_class_name] = collision_class.ignores or {}
end
local incoming = {}
local expanded = {}
local all = {}
for object_type, _ in pairs(collision_ignores) do
incoming[object_type] = {}
expanded[object_type] = {}
table.insert(all, object_type)
end
for object_type, ignore_list in pairs(collision_ignores) do
for key, ignored_type in pairs(ignore_list) do
if ignored_type == 'All' then
for _, all_object_type in ipairs(all) do
table.insert(incoming[all_object_type], object_type)
table.insert(expanded[object_type], all_object_type)
end
elseif type(ignored_type) == 'string' then
if ignored_type ~= 'All' then
table.insert(incoming[ignored_type], object_type)
table.insert(expanded[object_type], ignored_type)
end
end
if key == 'except' then
for _, except_ignored_type in ipairs(ignored_type) do
for i, v in ipairs(incoming[except_ignored_type]) do
if v == object_type then
table.remove(incoming[except_ignored_type], i)
break
end
end
end
for _, except_ignored_type in ipairs(ignored_type) do
for i, v in ipairs(expanded[object_type]) do
if v == except_ignored_type then
table.remove(expanded[object_type], i)
break
end
end
end
end
end
end
local edge_groups = {}
for k, v in pairs(incoming) do
table.sort(v, function(a, b) return string.lower(a) < string.lower(b) end)
end
local i = 0
for k, v in pairs(incoming) do
local str = ""
for _, c in ipairs(v) do
str = str .. c
end
if not edge_groups[str] then i = i + 1; edge_groups[str] = {n = i} end
table.insert(edge_groups[str], k)
end
local categories = {}
for k, _ in pairs(collision_ignores) do
categories[k] = {}
end
for k, v in pairs(edge_groups) do
for i, c in ipairs(v) do
categories[c] = v.n
end
end
for k, v in pairs(expanded) do
local category = {categories[k]}
local current_masks = {}
for _, c in ipairs(v) do
table.insert(current_masks, categories[c])
end
self.masks[k] = {categories = category, masks = current_masks}
end
end
function World:getCollisionCallbacksTable()
local collision_table = {}
for collision_class_name, collision_class in pairs(self.collision_classes) do
collision_table[collision_class_name] = {}
for _, v in ipairs(collision_class.enter or {}) do table.insert(collision_table[collision_class_name], {type = 'enter', other = v}) end
for _, v in ipairs(collision_class.exit or {}) do table.insert(collision_table[collision_class_name], {type = 'exit', other = v}) end
for _, v in ipairs(collision_class.pre or {}) do table.insert(collision_table[collision_class_name], {type = 'pre', other = v}) end
for _, v in ipairs(collision_class.post or {}) do table.insert(collision_table[collision_class_name], {type = 'post', other = v}) end
end
return collision_table
end
local function collEnsure(collision_class_name1, a, collision_class_name2, b)
if a.collision_class == collision_class_name2 and b.collision_class == collision_class_name1 then return b, a
else return a, b end
end
local function collIf(collision_class_name1, collision_class_name2, a, b)
if (a.collision_class == collision_class_name1 and b.collision_class == collision_class_name2) or
(a.collision_class == collision_class_name2 and b.collision_class == collision_class_name1) then
return true
else return false end
end
function World.collisionOnEnter(fixture_a, fixture_b, contact)
local a, b = fixture_a:getUserData(), fixture_b:getUserData()
if fixture_a:isSensor() and fixture_b:isSensor() then
if a and b then
for _, collision in ipairs(a.world.collisions.on_enter.sensor) do
if collIf(collision.type1, collision.type2, a, b) then
a, b = collEnsure(collision.type1, a, collision.type2, b)
table.insert(a.collision_events[collision.type2], {collision_type = 'enter', collider_1 = a, collider_2 = b, contact = contact})
if collision.type1 == collision.type2 then
table.insert(b.collision_events[collision.type1], {collision_type = 'enter', collider_1 = b, collider_2 = a, contact = contact})
end
end
end
end
elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then
if a and b then
for _, collision in ipairs(a.world.collisions.on_enter.non_sensor) do
if collIf(collision.type1, collision.type2, a, b) then
a, b = collEnsure(collision.type1, a, collision.type2, b)
table.insert(a.collision_events[collision.type2], {collision_type = 'enter', collider_1 = a, collider_2 = b, contact = contact})
if collision.type1 == collision.type2 then
table.insert(b.collision_events[collision.type1], {collision_type = 'enter', collider_1 = b, collider_2 = a, contact = contact})
end
end
end
end
end
end
function World.collisionOnExit(fixture_a, fixture_b, contact)
local a, b = fixture_a:getUserData(), fixture_b:getUserData()
if fixture_a:isSensor() and fixture_b:isSensor() then
if a and b then
for _, collision in ipairs(a.world.collisions.on_exit.sensor) do
if collIf(collision.type1, collision.type2, a, b) then
a, b = collEnsure(collision.type1, a, collision.type2, b)
table.insert(a.collision_events[collision.type2], {collision_type = 'exit', collider_1 = a, collider_2 = b, contact = contact})
if collision.type1 == collision.type2 then
table.insert(b.collision_events[collision.type1], {collision_type = 'exit', collider_1 = b, collider_2 = a, contact = contact})
end
end
end
end
elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then
if a and b then
for _, collision in ipairs(a.world.collisions.on_exit.non_sensor) do
if collIf(collision.type1, collision.type2, a, b) then
a, b = collEnsure(collision.type1, a, collision.type2, b)
table.insert(a.collision_events[collision.type2], {collision_type = 'exit', collider_1 = a, collider_2 = b, contact = contact})
if collision.type1 == collision.type2 then
table.insert(b.collision_events[collision.type1], {collision_type = 'exit', collider_1 = b, collider_2 = a, contact = contact})
end
end
end
end
end
end
function World.collisionPre(fixture_a, fixture_b, contact)
local a, b = fixture_a:getUserData(), fixture_b:getUserData()
if fixture_a:isSensor() and fixture_b:isSensor() then
if a and b then
for _, collision in ipairs(a.world.collisions.pre.sensor) do
if collIf(collision.type1, collision.type2, a, b) then
a, b = collEnsure(collision.type1, a, collision.type2, b)
a:preSolve(b, contact)
if collision.type1 == collision.type2 then
b:preSolve(a, contact)
end
end
end
end
elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then
if a and b then
for _, collision in ipairs(a.world.collisions.pre.non_sensor) do
if collIf(collision.type1, collision.type2, a, b) then
a, b = collEnsure(collision.type1, a, collision.type2, b)
a:preSolve(b, contact)
if collision.type1 == collision.type2 then
b:preSolve(a, contact)
end
end
end
end
end
end
function World.collisionPost(fixture_a, fixture_b, contact, ni1, ti1, ni2, ti2)
local a, b = fixture_a:getUserData(), fixture_b:getUserData()
if fixture_a:isSensor() and fixture_b:isSensor() then
if a and b then
for _, collision in ipairs(a.world.collisions.post.sensor) do
if collIf(collision.type1, collision.type2, a, b) then
a, b = collEnsure(collision.type1, a, collision.type2, b)
a:postSolve(b, contact, ni1, ti1, ni2, ti2)
if collision.type1 == collision.type2 then
b:postSolve(a, contact, ni1, ti1, ni2, ti2)
end
end
end
end
elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then
if a and b then
for _, collision in ipairs(a.world.collisions.post.non_sensor) do
if collIf(collision.type1, collision.type2, a, b) then
a, b = collEnsure(collision.type1, a, collision.type2, b)
a:postSolve(b, contact, ni1, ti1, ni2, ti2)
if collision.type1 == collision.type2 then
b:postSolve(a, contact, ni1, ti1, ni2, ti2)
end
end
end
end
end
end
function World:newCircleCollider(x, y, r, settings)
return self.wf.Collider.new(self, 'Circle', x, y, r, settings)
end
function World:newRectangleCollider(x, y, w, h, settings)
return self.wf.Collider.new(self, 'Rectangle', x, y, w, h, settings)
end
function World:newBSGRectangleCollider(x, y, w, h, corner_cut_size, settings)
return self.wf.Collider.new(self, 'BSGRectangle', x, y, w, h, corner_cut_size, settings)
end
function World:newPolygonCollider(vertices, settings)
return self.wf.Collider.new(self, 'Polygon', vertices, settings)
end
function World:newLineCollider(x1, y1, x2, y2, settings)
return self.wf.Collider.new(self, 'Line', x1, y1, x2, y2, settings)
end
function World:newChainCollider(vertices, loop, settings)
return self.wf.Collider.new(self, 'Chain', vertices, loop, settings)
end
-- Internal AABB box2d query used before going for more specific and precise computations.
function World:_queryBoundingBox(x1, y1, x2, y2)
local colliders = {}
local callback = function(fixture)
if not fixture:isSensor() then table.insert(colliders, fixture:getUserData()) end
return true
end
self.box2d_world:queryBoundingBox(x1, y1, x2, y2, callback)
return colliders
end
function World:collisionClassInCollisionClassesList(collision_class, collision_classes)
if collision_classes[1] == 'All' then
local all_collision_classes = {}
for class, _ in pairs(self.collision_classes) do
table.insert(all_collision_classes, class)
end
if collision_classes.except then
for _, except in ipairs(collision_classes.except) do
for i, class in ipairs(all_collision_classes) do
if class == except then
table.remove(all_collision_classes, i)
break
end
end
end
end
for _, class in ipairs(all_collision_classes) do
if class == collision_class then return true end
end
else
for _, class in ipairs(collision_classes) do
if class == collision_class then return true end
end
end
end
function World:queryCircleArea(x, y, radius, collision_class_names)
if not collision_class_names then collision_class_names = {'All'} end
if self.query_debug_drawing_enabled then table.insert(self.query_debug_draw, {type = 'circle', x = x, y = y, r = radius, frames = self.draw_query_for_n_frames}) end
local colliders = self:_queryBoundingBox(x-radius, y-radius, x+radius, y+radius)
local outs = {}
for _, collider in ipairs(colliders) do
if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then
for _, fixture in ipairs(collider.body:getFixtures()) do
if self.wf.Math.polygon.getCircleIntersection(x, y, radius, {collider.body:getWorldPoints(fixture:getShape():getPoints())}) then
table.insert(outs, collider)
break
end
end
end
end
return outs
end
function World:queryRectangleArea(x, y, w, h, collision_class_names)
if not collision_class_names then collision_class_names = {'All'} end
if self.query_debug_drawing_enabled then table.insert(self.query_debug_draw, {type = 'rectangle', x = x, y = y, w = w, h = h, frames = self.draw_query_for_n_frames}) end
local colliders = self:_queryBoundingBox(x, y, x+w, y+h)
local outs = {}
for _, collider in ipairs(colliders) do
if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then
for _, fixture in ipairs(collider.body:getFixtures()) do
if self.wf.Math.polygon.isPolygonInside({x, y, x+w, y, x+w, y+h, x, y+h}, {collider.body:getWorldPoints(fixture:getShape():getPoints())}) then
table.insert(outs, collider)
break
end
end
end
end
return outs
end
function World:queryPolygonArea(vertices, collision_class_names)
if not collision_class_names then collision_class_names = {'All'} end
if self.query_debug_drawing_enabled then table.insert(self.query_debug_draw, {type = 'polygon', vertices = vertices, frames = self.draw_query_for_n_frames}) end
local cx, cy = self.wf.Math.polygon.getCentroid(vertices)
local d_max = 0
for i = 1, #vertices, 2 do
local d = self.wf.Math.line.getLength(cx, cy, vertices[i], vertices[i+1])
if d > d_max then d_max = d end
end
local colliders = self:_queryBoundingBox(cx-d_max, cy-d_max, cx+d_max, cy+d_max)
local outs = {}
for _, collider in ipairs(colliders) do
if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then
for _, fixture in ipairs(collider.body:getFixtures()) do
if self.wf.Math.polygon.isPolygonInside(vertices, {collider.body:getWorldPoints(fixture:getShape():getPoints())}) then
table.insert(outs, collider)
break
end
end
end
end
return outs
end
function World:queryLine(x1, y1, x2, y2, collision_class_names)
if not collision_class_names then collision_class_names = {'All'} end
if self.query_debug_drawing_enabled then
table.insert(self.query_debug_draw, {type = 'line', x1 = x1, y1 = y1, x2 = x2, y2 = y2, frames = self.draw_query_for_n_frames})
end
local colliders = {}
local callback = function(fixture, ...)
if not fixture:isSensor() then table.insert(colliders, fixture:getUserData()) end
return 1
end
self.box2d_world:rayCast(x1, y1, x2, y2, callback)
local outs = {}
for _, collider in ipairs(colliders) do
if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then
table.insert(outs, collider)
end
end
return outs
end
function World:addJoint(joint_type, ...)
local args = {...}
if args[1].body then args[1] = args[1].body end
if type(args[2]) == "table" and args[2].body then args[2] = args[2].body end
local joint = love.physics['new' .. joint_type](unpack(args))
return joint
end
function World:removeJoint(joint)
joint:destroy()
end
function World:destroy()
local bodies = self.box2d_world:getBodies()
for _, body in ipairs(bodies) do
local collider = body:getFixtures()[1]:getUserData()
collider:destroy()
end
local joints = self.box2d_world:getJoints()
for _, joint in ipairs(joints) do joint:destroy() end
self.box2d_world:destroy()
self.box2d_world = nil
end
local Collider = {}
Collider.__index = Collider
local generator = love.math.newRandomGenerator(os.time())
local function UUID()
local fn = function(x)
local r = generator:random(16) - 1
r = (x == "x") and (r + 1) or (r % 4) + 9
return ("0123456789abcdef"):sub(r, r)
end
return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
end
function Collider.new(world, collider_type, ...)
local self = {}
self.id = UUID()
self.world = world
self.type = collider_type
self.object = nil
self.shapes = {}
self.fixtures = {}
self.sensors = {}
self.collision_events = {}
self.collision_stay = {}
self.enter_collision_data = {}
self.exit_collision_data = {}
self.stay_collision_data = {}
local args = {...}
local shape, fixture
if self.type == 'Circle' then
self.collision_class = (args[4] and args[4].collision_class) or 'Default'
self.body = love.physics.newBody(self.world.box2d_world, args[1], args[2], (args[4] and args[4].body_type) or 'dynamic')
shape = love.physics.newCircleShape(args[3])
elseif self.type == 'Rectangle' then
self.collision_class = (args[5] and args[5].collision_class) or 'Default'
self.body = love.physics.newBody(self.world.box2d_world, args[1] + args[3]/2, args[2] + args[4]/2, (args[5] and args[5].body_type) or 'dynamic')
shape = love.physics.newRectangleShape(args[3], args[4])
elseif self.type == 'BSGRectangle' then
self.collision_class = (args[6] and args[6].collision_class) or 'Default'
self.body = love.physics.newBody(self.world.box2d_world, args[1] + args[3]/2, args[2] + args[4]/2, (args[6] and args[6].body_type) or 'dynamic')
local w, h, s = args[3], args[4], args[5]
shape = love.physics.newPolygonShape({
-w/2, -h/2 + s, -w/2 + s, -h/2,
w/2 - s, -h/2, w/2, -h/2 + s,
w/2, h/2 - s, w/2 - s, h/2,
-w/2 + s, h/2, -w/2, h/2 - s
})
elseif self.type == 'Polygon' then
self.collision_class = (args[2] and args[2].collision_class) or 'Default'
self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[2] and args[2].body_type) or 'dynamic')
shape = love.physics.newPolygonShape(unpack(args[1]))
elseif self.type == 'Line' then
self.collision_class = (args[5] and args[5].collision_class) or 'Default'
self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[5] and args[5].body_type) or 'dynamic')
shape = love.physics.newEdgeShape(args[1], args[2], args[3], args[4])
elseif self.type == 'Chain' then
self.collision_class = (args[3] and args[3].collision_class) or 'Default'
self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[3] and args[3].body_type) or 'dynamic')
shape = love.physics.newChainShape(args[1], unpack(args[2]))
end
-- Define collision classes and attach them to fixture and sensor
fixture = love.physics.newFixture(self.body, shape)
if self.world.masks[self.collision_class] then
fixture:setCategory(unpack(self.world.masks[self.collision_class].categories))
fixture:setMask(unpack(self.world.masks[self.collision_class].masks))
end
fixture:setUserData(self)
local sensor = love.physics.newFixture(self.body, shape)
sensor:setSensor(true)
sensor:setUserData(self)
self.shapes['main'] = shape
self.fixtures['main'] = fixture
self.sensors['main'] = sensor
self.shape = shape
self.fixture = fixture
self.preSolve = function() end
self.postSolve = function() end
-- Points all body, fixture and shape functions to this wf.Collider object
-- This means that the user can call collider:setLinearVelocity for instance without having to say collider.body:setLinearVelocity
for k, v in pairs(self.body.__index) do
if k ~= '__gc' and k ~= '__eq' and k ~= '__index' and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type' and k ~= 'typeOf' then
self[k] = function(self, ...)
return v(self.body, ...)
end
end
end
for k, v in pairs(self.fixture.__index) do
if k ~= '__gc' and k ~= '__eq' and k ~= '__index' and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type' and k ~= 'typeOf' then
self[k] = function(self, ...)
return v(self.fixture, ...)
end
end
end
for k, v in pairs(self.shape.__index) do
if k ~= '__gc' and k ~= '__eq' and k ~= '__index' and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type' and k ~= 'typeOf' then
self[k] = function(self, ...)
return v(self.shape, ...)
end
end
end
return setmetatable(self, Collider)
end
function Collider:collisionEventsClear()
self.collision_events = {}
for other, _ in pairs(self.world.collision_classes) do
self.collision_events[other] = {}
end
end
function Collider:setCollisionClass(collision_class_name)
if not self.world.collision_classes[collision_class_name] then error("Collision class " .. collision_class_name .. " doesn't exist.") end
self.collision_class = collision_class_name
for _, fixture in pairs(self.fixtures) do
if self.world.masks[collision_class_name] then
fixture:setCategory(unpack(self.world.masks[collision_class_name].categories))
fixture:setMask(unpack(self.world.masks[collision_class_name].masks))
end
end
end
function Collider:enter(other_collision_class_name)
local events = self.collision_events[other_collision_class_name]
if events and #events >= 1 then
for _, e in ipairs(events) do
if e.collision_type == 'enter' then
if not self.collision_stay[other_collision_class_name] then self.collision_stay[other_collision_class_name] = {} end
table.insert(self.collision_stay[other_collision_class_name], {collider = e.collider_2, contact = e.contact})
self.enter_collision_data[other_collision_class_name] = {collider = e.collider_2, contact = e.contact}
return true
end
end
end
end
function Collider:getEnterCollisionData(other_collision_class_name)
return self.enter_collision_data[other_collision_class_name]
end
function Collider:exit(other_collision_class_name)
local events = self.collision_events[other_collision_class_name]
if events and #events >= 1 then
for _, e in ipairs(events) do
if e.collision_type == 'exit' then
if self.collision_stay[other_collision_class_name] then
for i = #self.collision_stay[other_collision_class_name], 1, -1 do
local collision_stay = self.collision_stay[other_collision_class_name][i]
if collision_stay.collider.id == e.collider_2.id then table.remove(self.collision_stay[other_collision_class_name], i) end
end
end
self.exit_collision_data[other_collision_class_name] = {collider = e.collider_2, contact = e.contact}
return true
end
end
end
end
function Collider:getExitCollisionData(other_collision_class_name)
return self.exit_collision_data[other_collision_class_name]
end
function Collider:stay(other_collision_class_name)
if self.collision_stay[other_collision_class_name] then
if #self.collision_stay[other_collision_class_name] >= 1 then
return true
end
end
end
function Collider:getStayCollisionData(other_collision_class_name)
return self.collision_stay[other_collision_class_name]
end
function Collider:setPreSolve(callback)
self.preSolve = callback
end
function Collider:setPostSolve(callback)
self.postSolve = callback
end
function Collider:setObject(object)
self.object = object
end
function Collider:getObject()
return self.object
end
function Collider:addShape(shape_name, shape_type, ...)
if self.shapes[shape_name] or self.fixtures[shape_name] then error("Shape/fixture " .. shape_name .. " already exists.") end
local args = {...}
local shape = love.physics['new' .. shape_type](unpack(args))
local fixture = love.physics.newFixture(self.body, shape)
if self.world.masks[self.collision_class] then
fixture:setCategory(unpack(self.world.masks[self.collision_class].categories))
fixture:setMask(unpack(self.world.masks[self.collision_class].masks))
end
fixture:setUserData(self)
local sensor = love.physics.newFixture(self.body, shape)
sensor:setSensor(true)
sensor:setUserData(self)
self.shapes[shape_name] = shape
self.fixtures[shape_name] = fixture
self.sensors[shape_name] = sensor
end
function Collider:removeShape(shape_name)
if not self.shapes[shape_name] then return end
self.shapes[shape_name] = nil
self.fixtures[shape_name]:setUserData(nil)
self.fixtures[shape_name]:destroy()
self.fixtures[shape_name] = nil
self.sensors[shape_name]:setUserData(nil)
self.sensors[shape_name]:destroy()
self.sensors[shape_name] = nil
end
function Collider:destroy()
self.collision_stay = nil
self.enter_collision_data = nil
self.exit_collision_data = nil
self:collisionEventsClear()
self:setObject(nil)
for name, _ in pairs(self.fixtures) do
self.shapes[name] = nil
self.fixtures[name]:setUserData(nil)
self.fixtures[name] = nil
self.sensors[name]:setUserData(nil)
self.sensors[name] = nil
end
self.body:destroy()
self.body = nil
end
wf.World = World
wf.Collider = Collider
return wf