1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
|
--- 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
|