930 lines
38 KiB
Lua
930 lines
38 KiB
Lua
--[[
|
|
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
|
|
|