2
-- A group is a set of sprites. Groups can be used to
3
-- implement layers or keep categories of sprites together.
9
-- Called after all member sprites are drawn onscreen.
12
-- Called once each frame, with the elapsed time since the last frame in seconds.
14
-- Event: onBeginFrame
15
-- Called once each frame like onUpdate, but guaranteed to fire before any others' onUpdate handlers.
18
-- Called once each frame like onUpdate, but guaranteed to fire after all others' onUpdate handlers.
23
-- If false, none of its member sprites will receive update-related events.
27
-- If false, none of its member sprites will be drawn.
31
-- If false, nothing will collide against this group, nor will this group
32
-- displace any other sprite. This does not prevent collision checking
33
-- against individual sprites in this group, however.
37
-- A table of member sprites, in drawing order.
40
-- Property: timeScale
41
-- Multiplier for elapsed time; 1.0 is normal, 0 is completely frozen.
44
-- Property: translate
45
-- This table's x and y properties shift member sprites' positions when drawn.
46
-- To draw sprites at their normal position, set both x and y to 0.
47
translate = { x = 0, y = 0 },
49
-- Property: translateScale
50
-- This table's x and y properties multiply member sprites'
51
-- positions, which you can use to simulate parallax scrolling. To draw
52
-- sprites at their normal position, set both x and y to 1.
53
translateScale = { x = 1, y = 1 },
56
-- The size, in pixels, of the grid used for collision detection.
57
-- This partitions off space so that collision checks only need to do real
58
-- checks against a few sprites at a time. If you notice collision detection
59
-- taking a long time, changing this number may help.
63
-- Adds a sprite to the group.
66
-- sprite - <Sprite> to add
71
add = function (self, sprite)
72
assert(sprite, 'asked to add nil to a group')
73
assert(sprite ~= self, "can't add a group to itself")
75
if STRICT and self:contains(sprite) then
76
local info = debug.getinfo(2, 'Sl')
77
print('Warning: adding a sprite to a group it already belongs to (' ..
78
info.short_src .. ' line ' .. info.currentline .. ')')
81
table.insert(self.sprites, sprite)
85
-- Removes a sprite from the group. If the sprite is
86
-- not in the group, this does nothing.
89
-- sprite - <Sprite> to remove
94
remove = function (self, sprite)
95
for i, spr in ipairs(self.sprites) do
97
table.remove(self.sprites, i)
103
local info = debug.getinfo(2, 'Sl')
104
print('Warning: asked to remove a sprite from a group it was not a member of (' ..
105
info.short_src .. ' line ' .. info.currentline .. ')')
110
-- Collides all solid sprites in the group with another sprite or group.
111
-- This calls the <Sprite.onCollide> event handlers on all sprites that
112
-- collide with the same arguments <Sprite.collide> does.
114
-- It's often useful to collide a group with itself, e.g. myGroup:collide().
115
-- This checks for collisions between the sprites that make up the group.
118
-- other - <Sprite> or <Group> to collide with, default self
121
-- boolean, whether any collision was detected
126
collide = function (self, other)
127
other = other or self
130
assert(other:instanceOf(Group) or other:instanceOf(Sprite), 'asked to collide non-group/sprite ' ..
134
if not self.solid or not other.solid then return false end
137
if other.sprites then
138
local grid = self:grid()
139
local gridSize = self.gridSize
141
for _, othSpr in pairs(other.sprites) do
142
local startX = math.floor(othSpr.x / gridSize)
143
local endX = math.floor((othSpr.x + othSpr.width) / gridSize)
144
local startY = math.floor(othSpr.y / gridSize)
145
local endY = math.floor((othSpr.y + othSpr.height) / gridSize)
147
for x = startX, endX do
149
for y = startY, endY do
151
for _, spr in pairs(grid[x][y]) do
152
hit = spr:collide(othSpr) or hit
160
for _, spr in pairs(self.sprites) do
161
hit = spr:collide(other) or hit
169
-- Displaces a sprite or group by all solid sprites in this group. This will *not* cause
170
-- any onCollide event handlers to be called.
173
-- other - <Sprite> or <Group> to collide with, default self
174
-- xHint - force horizontal displacement in one direction, uses direction constants, optional
175
-- yHint - force vertical displacement in one direction, uses direction constants, optional
183
displace = function (self, other, xHint, yHint)
184
other = other or self
187
assert(other:instanceOf(Group) or other:instanceOf(Sprite), 'asked to displace non-group/sprite ' ..
191
if not self.solid or not other.solid then return false end
193
if other.sprites then
194
-- group displacing group
196
local grid = self:grid()
197
local gridSize = self.gridSize
199
for _, othSpr in pairs(other.sprites) do
200
local startX = math.floor(othSpr.x / gridSize)
201
local endX = math.floor((othSpr.x + othSpr.width) / gridSize)
202
local startY = math.floor(othSpr.y / gridSize)
203
local endY = math.floor((othSpr.y + othSpr.height) / gridSize)
204
local displacers = {}
206
for x = startX, endX do
208
for y = startY, endY do
210
for _, spr in pairs(grid[x][y]) do
211
if spr ~= othSpr and spr.solid and spr:intersects(othSpr.x, othSpr.y, othSpr.width, othSpr.height) then
212
table.insert(displacers, spr)
220
-- see Map:subdisplace() for how this is done
222
if #displacers > 0 then
226
while hit and loops < 3 do
230
local xVotes, yVotes = 0, 0
231
local minChangeX, minChangeY, absMinChangeX, absMinChangeY
232
local origX, origY = othSpr.x, othSpr.y
234
for _, spr in pairs(displacers) do
236
local xChange = othSpr.x - origX
237
local yChange = othSpr.y - origY
240
xVotes = xVotes + math.abs(xChange)
243
if not minChangeX or math.abs(xChange) < absMinChangeX then
245
absMinChangeX = math.abs(xChange)
250
yVotes = yVotes + math.abs(yChange)
253
if not minChangeY or math.abs(yChange) < absMinChangeY then
255
absMinChangeY = math.abs(yChange)
259
-- restore sprite to original position
266
if xVotes > 0 and xVotes > yVotes then
267
othSpr.x = othSpr.x + minChangeX
268
elseif yVotes > 0 then
269
othSpr.y = othSpr.y + minChangeY
276
-- group displacing sprite
278
local displacers = {}
280
for _, spr in pairs(self.sprites) do
281
if spr ~= other and spr:intersects(other.x, other.y, other.width, other.height) then
282
table.insert(displacers, spr)
286
-- see Map:subdisplace() for how this is done
288
if #displacers > 0 then
292
while hit and loops < 3 do
296
local xVotes, yVotes = 0, 0
297
local minChangeX, minChangeY, absMinChangeX, absMinChangeY
298
local origX, origY = other.x, other.y
300
for _, spr in pairs(displacers) do
302
local xChange = other.x - origX
303
local yChange = other.y - origY
306
xVotes = xVotes + math.abs(xChange)
309
if not minChangeX or math.abs(xChange) < absMinChangeX then
311
absMinChangeX = math.abs(xChange)
316
yVotes = yVotes + math.abs(yChange)
319
if not minChangeY or math.abs(yChange) < absMinChangeY then
321
absMinChangeY = math.abs(yChange)
325
-- restore sprite to original position
332
if xVotes > 0 and xVotes > yVotes then
333
other.x = other.x + minChangeX
334
elseif yVotes > 0 then
335
other.y = other.y + minChangeY
344
-- Sets a pixel effect to use while drawing sprites in this group.
345
-- See https://love2d.org/wiki/PixelEffect for details on how pixel
346
-- effects work. After this call, the group's effect property will be
347
-- set up so you can send variables to it. Only one pixel effect can
348
-- be active on a group at a time.
351
-- filename - filename of effect source code; if nil, this
352
-- clears any existing pixel effect.
353
-- effectType - either 'screen' (applies the effect to the entire
354
-- group once, via an offscreen canvas), or 'sprite'
355
-- (applies to the effect to each individual draw operation).
356
-- Screen effects use more resources, but certain effects
357
-- need to work on the entire screen to be effective.
360
-- whether the effect was successfully created
362
setEffect = function (self, filename, effectType)
363
effectType = effectType or 'screen'
365
if love.graphics.isSupported('pixeleffect') and
366
(effectType == 'sprite' or love.graphics.isSupported('canvas'))then
368
self.effect = love.graphics.newPixelEffect(Cached:text(filename))
369
self.effectType = effectType
381
-- Counts how many sprites are in this group.
384
-- subgroups - include subgroups?
389
count = function (self, subgroups)
393
for _, spr in pairs(self.sprites) do
394
if spr:instanceOf(Group) then
395
count = count + spr:count(true)
408
-- Makes the group totally inert. It will not receive
409
-- update events, draw anything, or be collided.
417
die = function (self)
424
-- Makes this group completely active. It will receive
425
-- update events, draw itself, and be collided.
433
revive = function (self)
440
-- Returns whether this group contains a sprite.
443
-- sprite - sprite to look for
444
-- recurse - check subgroups? defaults to true
449
contains = function (self, sprite, recurse)
450
if recurse ~= false then recurse = true end
452
for _, spr in pairs(self.sprites) do
453
if spr == sprite then return true end
455
if recurse and spr:instanceOf(Group) and spr:contains(sprite) then
464
-- Creates a table indexed by x and y dimensions, with each
465
-- cell a table of sprites that touch this grid element. For
466
-- example, with a grid size of 50, a sprite at (10, 10) that
467
-- is 50 pixels square would be in the grid at [0][0], [1][0],
468
-- [0][1], and [1][1].
470
-- This can be used to speed up work that involves checking
471
-- for sprites near each other, e.g. collision detection.
474
-- existing - existing grid table to add sprites into,
475
-- optional. Anything you pass must have
476
-- used the same size as the current call.
481
grid = function (self, existing)
482
local result = existing or {}
483
local size = self.gridSize
485
for _, spr in pairs(self.sprites) do
487
local oldSize = spr.gridSize
488
spr.gridSize = self.gridSize
489
result = spr:grid(result)
490
spr.gridSize = oldSize
492
local startX = math.floor(spr.x / size)
493
local endX = math.floor((spr.x + spr.width) / size)
494
local startY = math.floor(spr.y / size)
495
local endY = math.floor((spr.y + spr.height) / size)
497
for x = startX, endX do
498
if not result[x] then result[x] = {} end
500
for y = startY, endY do
501
if not result[x][y] then result[x][y] = {} end
502
table.insert(result[x][y], spr)
511
-- passes startFrame events to member sprites
513
startFrame = function (self, elapsed)
514
if not self.active then return end
515
elapsed = elapsed * self.timeScale
516
if self.onStartFrame then self:onStartFrame(elapsed) end
518
for _, spr in pairs(self.sprites) do
519
if spr.active then spr:startFrame(elapsed) end
523
-- passes update events to member sprites
525
update = function (self, elapsed)
526
if not self.active then return end
527
elapsed = elapsed * self.timeScale
528
if self.onUpdate then self:onUpdate(elapsed) end
530
for _, spr in pairs(self.sprites) do
531
if spr.active then spr:update(elapsed) end
535
-- passes endFrame events to member sprites
537
endFrame = function (self, elapsed)
538
if not self.active then return end
539
elapsed = elapsed * self.timeScale
540
if self.onEndFrame then self:onEndFrame(elapsed) end
542
for _, spr in pairs(self.sprites) do
543
if spr.active then spr:endFrame(elapsed) end
548
-- Draws all visible member sprites onscreen.
551
-- x - x offset in pixels
552
-- y - y offset in pixels
554
draw = function (self, x, y)
555
if not self.visible then return end
556
x = x or self.translate.x
557
y = y or self.translate.y
559
local scrollX = x * self.translateScale.x
560
local scrollY = y * self.translateScale.y
561
local appWidth = the.app.width
562
local appHeight = the.app.height
565
if self.effectType == 'screen' then
566
if not self._canvas then self._canvas = love.graphics.newCanvas() end
568
love.graphics.setCanvas(self._canvas)
569
elseif self.effectType == 'sprite' then
570
love.graphics.setPixelEffect(self.effect)
574
for _, spr in pairs(self.sprites) do
576
if spr.translate then
577
spr:draw(spr.translate.x + scrollX, spr.translate.y + scrollY)
578
elseif spr.x and spr.y and spr.width and spr.height then
579
local sprX = spr.x + scrollX
580
local sprY = spr.y + scrollY
582
if sprX < appWidth and sprX + spr.width > 0 and
583
sprY < appHeight and sprY + spr.height > 0 then
587
spr:draw(scrollX, scrollY)
592
if self.onDraw then self:onDraw() end
595
if self.effectType == 'screen' then
596
love.graphics.setPixelEffect(self.effect)
597
love.graphics.setCanvas()
598
love.graphics.draw(self._canvas)
601
love.graphics.setPixelEffect()
605
__tostring = function (self)
606
local result = 'Group ('
609
result = result .. 'active'
611
result = result .. 'inactive'
615
result = result .. ', visible'
617
result = result .. ', invisible'
621
result = result .. ', solid'
623
result = result .. ', not solid'
626
return result .. ', ' .. self:count(true) .. ' sprites)'