--- Box2D plugin for STI -- @module box2d -- @author Landon Manning -- @copyright 2019 -- @license MIT/X11 local love = _G.love local utils = require((...):gsub("plugins.box2d", "utils")) local lg = require((...):gsub("plugins.box2d", "graphics")) return { box2d_LICENSE = "MIT/X11", box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation", box2d_VERSION = "2.3.2.7", box2d_DESCRIPTION = "Box2D hooks for STI.", --- Initialize Box2D physics world. -- @param world The Box2D world to add objects to. box2d_init = function(map, world) assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.") local body = love.physics.newBody(world, map.offsetx, map.offsety) local collision = { body = body, } local function addObjectToWorld(objshape, vertices, userdata, object) local shape if objshape == "polyline" then if #vertices == 4 then shape = love.physics.newEdgeShape(unpack(vertices)) else shape = love.physics.newChainShape(false, unpack(vertices)) end else shape = love.physics.newPolygonShape(unpack(vertices)) end local currentBody = body --dynamic are objects/players etc. if userdata.properties.dynamic == true then currentBody = love.physics.newBody(world, map.offsetx, map.offsety, "dynamic") -- static means it shouldn't move. Things like walls/ground. elseif userdata.properties.static == true then currentBody = love.physics.newBody(world, map.offsetx, map.offsety, "static") -- kinematic means that the object is static in the game world but effects other bodies elseif userdata.properties.kinematic == true then currentBody = love.physics.newBody(world, map.offsetx, map.offsety, "kinematic") end local fixture = love.physics.newFixture(currentBody, shape) fixture:setUserData(userdata) -- Set some custom properties from userdata (or use default set by box2d) fixture:setFriction(userdata.properties.friction or 0.2) fixture:setRestitution(userdata.properties.restitution or 0.0) fixture:setSensor(userdata.properties.sensor or false) fixture:setFilterData( userdata.properties.categories or 1, userdata.properties.mask or 65535, userdata.properties.group or 0 ) local obj = { object = object, body = currentBody, shape = shape, fixture = fixture, } table.insert(collision, obj) end local function getPolygonVertices(object) local vertices = {} for _, vertex in ipairs(object.polygon) do table.insert(vertices, vertex.x) table.insert(vertices, vertex.y) end return vertices end local function calculateObjectPosition(object, tile) local o = { shape = object.shape, x = (object.dx or object.x) + map.offsetx, y = (object.dy or object.y) + map.offsety, w = object.width, h = object.height, polygon = object.polygon or object.polyline or object.ellipse or object.rectangle, } local userdata = { object = o, properties = object.properties, } o.r = object.rotation or 0 if o.shape == "rectangle" then local cos = math.cos(math.rad(o.r)) local sin = math.sin(math.rad(o.r)) local oy = 0 if object.gid then local tileset = map.tilesets[map.tiles[object.gid].tileset] local lid = object.gid - tileset.firstgid local t = {} -- This fixes a height issue o.y = o.y + map.tiles[object.gid].offset.y oy = o.h for _, tt in ipairs(tileset.tiles) do if tt.id == lid then t = tt break end end if t.objectGroup then for _, obj in ipairs(t.objectGroup.objects) do -- Every object in the tile calculateObjectPosition(obj, object) end return else o.w = map.tiles[object.gid].width o.h = map.tiles[object.gid].height end end o.polygon = { { x = o.x + 0, y = o.y + 0 }, { x = o.x + o.w, y = o.y + 0 }, { x = o.x + o.w, y = o.y + o.h }, { x = o.x + 0, y = o.y + o.h }, } for _, vertex in ipairs(o.polygon) do vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin, oy) end local vertices = getPolygonVertices(o) addObjectToWorld(o.shape, vertices, userdata, tile or object) elseif o.shape == "ellipse" then if not o.polygon then o.polygon = utils.convert_ellipse_to_polygon(o.x, o.y, o.w, o.h) end local vertices = getPolygonVertices(o) local triangles = love.math.triangulate(vertices) for _, triangle in ipairs(triangles) do addObjectToWorld(o.shape, triangle, userdata, tile or object) end elseif o.shape == "polygon" then -- Recalculate collision polygons inside tiles if tile then local cos = math.cos(math.rad(o.r)) local sin = math.sin(math.rad(o.r)) for _, vertex in ipairs(o.polygon) do vertex.x = vertex.x + o.x vertex.y = vertex.y + o.y vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin) end end local vertices = getPolygonVertices(o) local triangles = love.math.triangulate(vertices) for _, triangle in ipairs(triangles) do addObjectToWorld(o.shape, triangle, userdata, tile or object) end elseif o.shape == "polyline" then local vertices = getPolygonVertices(o) addObjectToWorld(o.shape, vertices, userdata, tile or object) end end for _, tile in pairs(map.tiles) do if map.tileInstances[tile.gid] then for _, instance in ipairs(map.tileInstances[tile.gid]) do -- Every object in every instance of a tile if tile.objectGroup then for _, object in ipairs(tile.objectGroup.objects) do if object.properties.collidable == true then object = utils.deepCopy(object) object.dx = instance.x + object.x object.dy = instance.y + object.y calculateObjectPosition(object, instance) end end end -- Every instance of a tile if tile.properties.collidable == true then local object = { shape = "rectangle", x = instance.x, y = instance.y, width = map.tilewidth, height = map.tileheight, properties = tile.properties, } calculateObjectPosition(object, instance) end end end end for _, layer in ipairs(map.layers) do -- Entire layer if layer.properties.collidable == true then if layer.type == "tilelayer" then for gid, tiles in pairs(map.tileInstances) do local tile = map.tiles[gid] local tileset = map.tilesets[tile.tileset] for _, instance in ipairs(tiles) do if instance.layer == layer then local object = { shape = "rectangle", x = instance.x, y = instance.y, width = tileset.tilewidth, height = tileset.tileheight, properties = tile.properties, } calculateObjectPosition(object, instance) end end end elseif layer.type == "objectgroup" then for _, object in ipairs(layer.objects) do calculateObjectPosition(object) end elseif layer.type == "imagelayer" then local object = { shape = "rectangle", x = layer.x or 0, y = layer.y or 0, width = layer.width, height = layer.height, properties = layer.properties, } calculateObjectPosition(object) end end -- Individual objects if layer.type == "objectgroup" then for _, object in ipairs(layer.objects) do if object.properties.collidable == true then calculateObjectPosition(object) end end end end map.box2d_collision = collision end, --- Remove Box2D fixtures and shapes from world. -- @param index The index or name of the layer being removed box2d_removeLayer = function(map, index) local layer = assert(map.layers[index], "Layer not found: " .. index) local collision = map.box2d_collision -- Remove collision objects for i = #collision, 1, -1 do local obj = collision[i] if obj.object.layer == layer then obj.fixture:destroy() table.remove(collision, i) end end end, --- Draw Box2D physics world. -- @param tx Translate on X -- @param ty Translate on Y -- @param sx Scale on X -- @param sy Scale on Y box2d_draw = function(map, tx, ty, sx, sy) local collision = map.box2d_collision lg.push() lg.scale(sx or 1, sy or sx or 1) lg.translate(math.floor(tx or 0), math.floor(ty or 0)) for _, obj in ipairs(collision) do local points = { obj.body:getWorldPoints(obj.shape:getPoints()) } local shape_type = obj.shape:getType() if shape_type == "edge" or shape_type == "chain" then love.graphics.line(points) elseif shape_type == "polygon" then love.graphics.polygon("line", points) else error("sti box2d plugin does not support " .. shape_type .. " shapes") end end lg.pop() end, } --- Custom Properties in Tiled are used to tell this plugin what to do. -- @table Properties -- @field collidable set to true, can be used on any Layer, Tile, or Object -- @field sensor set to true, can be used on any Tile or Object that is also collidable -- @field dynamic set to true, can be used on any Tile or Object -- @field friction can be used to define the friction of any Object -- @field restitution can be used to define the restitution of any Object -- @field categories can be used to set the filter Category of any Object -- @field mask can be used to set the filter Mask of any Object -- @field group can be used to set the filter Group of any Object