/zoeplat

To get this branch, use:
bzr branch /bzr/zoeplat

« back to all changes in this revision

Viewing changes to zoetrope/core/group.lua

  • Committer: Josh C
  • Date: 2013-03-02 20:40:57 UTC
  • Revision ID: josh@9ix.org-20130302204057-yrra0a51zgtpq2v2
zoetrope 1.3.1

Show diffs side-by-side

added added

removed removed

 
1
-- Class: Group
 
2
-- A group is a set of sprites. Groups can be used to
 
3
-- implement layers or keep categories of sprites together.
 
4
--
 
5
-- Extends:
 
6
--              <Class>
 
7
--
 
8
-- Event: onDraw
 
9
-- Called after all member sprites are drawn onscreen.
 
10
--
 
11
-- Event: onUpdate
 
12
-- Called once each frame, with the elapsed time since the last frame in seconds.
 
13
--
 
14
-- Event: onBeginFrame
 
15
-- Called once each frame like onUpdate, but guaranteed to fire before any others' onUpdate handlers.
 
16
--
 
17
-- Event: onEndFrame
 
18
-- Called once each frame like onUpdate, but guaranteed to fire after all others' onUpdate handlers.
 
19
 
 
20
Group = Class:extend
 
21
{
 
22
        -- Property: active
 
23
        -- If false, none of its member sprites will receive update-related events.
 
24
        active = true,
 
25
 
 
26
        -- Property: visible
 
27
        -- If false, none of its member sprites will be drawn.
 
28
        visible = true,
 
29
 
 
30
        -- Property: solid
 
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.
 
34
        solid = true,
 
35
 
 
36
        -- Property: sprites
 
37
        -- A table of member sprites, in drawing order.
 
38
        sprites = {},
 
39
 
 
40
        -- Property: timeScale
 
41
        -- Multiplier for elapsed time; 1.0 is normal, 0 is completely frozen.
 
42
        timeScale = 1,
 
43
 
 
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 },
 
48
        
 
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 },
 
54
 
 
55
        -- Property: gridSize
 
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.
 
60
        gridSize = 50,
 
61
 
 
62
        -- Method: add
 
63
        -- Adds a sprite to the group.
 
64
        --
 
65
        -- Arguments:
 
66
        --              sprite - <Sprite> to add
 
67
        --
 
68
        -- Returns:
 
69
        --              nothing
 
70
 
 
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")
 
74
        
 
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 .. ')')
 
79
                end
 
80
 
 
81
                table.insert(self.sprites, sprite)
 
82
        end,
 
83
 
 
84
        -- Method: remove
 
85
        -- Removes a sprite from the group. If the sprite is
 
86
        -- not in the group, this does nothing.
 
87
        -- 
 
88
        -- Arguments:
 
89
        --              sprite - <Sprite> to remove
 
90
        -- 
 
91
        -- Returns:
 
92
        --              nothing
 
93
 
 
94
        remove = function (self, sprite)
 
95
                for i, spr in ipairs(self.sprites) do
 
96
                        if spr == sprite then
 
97
                                table.remove(self.sprites, i)
 
98
                                return
 
99
                        end
 
100
                end
 
101
                
 
102
                if STRICT then
 
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 .. ')')
 
106
                end
 
107
        end,
 
108
 
 
109
        -- Method: collide
 
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.
 
113
        --
 
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.
 
116
        --
 
117
        -- Arguments:
 
118
        --              other - <Sprite> or <Group> to collide with, default self
 
119
        -- 
 
120
        -- Returns:
 
121
        --              boolean, whether any collision was detected
 
122
        --
 
123
        -- See Also:
 
124
        --              <Sprite.collide>
 
125
 
 
126
        collide = function (self, other)
 
127
                other = other or self
 
128
 
 
129
                if STRICT then
 
130
                        assert(other:instanceOf(Group) or other:instanceOf(Sprite), 'asked to collide non-group/sprite ' ..
 
131
                                   type(other))
 
132
                end
 
133
 
 
134
                if not self.solid or not other.solid then return false end
 
135
                local hit = false
 
136
 
 
137
                if other.sprites then
 
138
                        local grid = self:grid()
 
139
                        local gridSize = self.gridSize
 
140
 
 
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)
 
146
 
 
147
                                for x = startX, endX do
 
148
                                        if grid[x] then
 
149
                                                for y = startY, endY do
 
150
                                                        if grid[x][y] then
 
151
                                                                for _, spr in pairs(grid[x][y]) do
 
152
                                                                        hit = spr:collide(othSpr) or hit
 
153
                                                                end
 
154
                                                        end
 
155
                                                end
 
156
                                        end
 
157
                                end
 
158
                        end
 
159
                else
 
160
                        for _, spr in pairs(self.sprites) do
 
161
                                hit = spr:collide(other) or hit
 
162
                        end
 
163
                end
 
164
 
 
165
                return hit
 
166
        end,
 
167
 
 
168
        -- Method: displace
 
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.
 
171
        --
 
172
        -- Arguments:
 
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
 
176
        -- 
 
177
        -- Returns:
 
178
        --              nothing
 
179
        --
 
180
        -- See Also:
 
181
        --              <Sprite.displace>
 
182
 
 
183
        displace = function (self, other, xHint, yHint)
 
184
                other = other or self
 
185
 
 
186
                if STRICT then
 
187
                        assert(other:instanceOf(Group) or other:instanceOf(Sprite), 'asked to displace non-group/sprite ' ..
 
188
                                   type(other))
 
189
                end
 
190
 
 
191
                if not self.solid or not other.solid then return false end
 
192
 
 
193
                if other.sprites then
 
194
                        -- group displacing group
 
195
 
 
196
                        local grid = self:grid()
 
197
                        local gridSize = self.gridSize
 
198
 
 
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 = {}
 
205
 
 
206
                                for x = startX, endX do
 
207
                                        if grid[x] then
 
208
                                                for y = startY, endY do
 
209
                                                        if grid[x][y] then
 
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)
 
213
                                                                        end
 
214
                                                                end
 
215
                                                        end
 
216
                                                end
 
217
                                        end
 
218
                                end
 
219
 
 
220
                                -- see Map:subdisplace() for how this is done
 
221
 
 
222
                                if #displacers > 0 then
 
223
                                        local hit = true
 
224
                                        local loops = 0
 
225
 
 
226
                                        while hit and loops < 3 do
 
227
                                                hit = false
 
228
                                                loops = loops + 1
 
229
 
 
230
                                                local xVotes, yVotes = 0, 0
 
231
                                                local minChangeX, minChangeY, absMinChangeX, absMinChangeY
 
232
                                                local origX, origY = othSpr.x, othSpr.y
 
233
 
 
234
                                                for _, spr in pairs(displacers) do
 
235
                                                        spr:displace(othSpr)
 
236
                                                        local xChange = othSpr.x - origX
 
237
                                                        local yChange = othSpr.y - origY
 
238
 
 
239
                                                        if xChange ~= 0 then
 
240
                                                                xVotes = xVotes + math.abs(xChange)
 
241
                                                                hit = true
 
242
 
 
243
                                                                if not minChangeX or math.abs(xChange) < absMinChangeX then
 
244
                                                                        minChangeX = xChange
 
245
                                                                        absMinChangeX = math.abs(xChange)
 
246
                                                                end
 
247
                                                        end
 
248
 
 
249
                                                        if yChange ~= 0 then
 
250
                                                                yVotes = yVotes + math.abs(yChange)
 
251
                                                                hit = true
 
252
 
 
253
                                                                if not minChangeY or math.abs(yChange) < absMinChangeY then
 
254
                                                                        minChangeY = yChange
 
255
                                                                        absMinChangeY = math.abs(yChange)
 
256
                                                                end
 
257
                                                        end
 
258
 
 
259
                                                        -- restore sprite to original position
 
260
 
 
261
                                                        othSpr.x = origX
 
262
                                                        othSpr.y = origY
 
263
                                                end
 
264
 
 
265
                                                if hit then
 
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
 
270
                                                        end
 
271
                                                end
 
272
                                        end
 
273
                                end
 
274
                        end
 
275
                else
 
276
                        -- group displacing sprite
 
277
 
 
278
                        local displacers = {}
 
279
 
 
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)
 
283
                                end
 
284
                        end
 
285
 
 
286
                        -- see Map:subdisplace() for how this is done
 
287
 
 
288
                        if #displacers > 0 then
 
289
                                local hit = true
 
290
                                local loops = 0
 
291
 
 
292
                                while hit and loops < 3 do
 
293
                                        hit = false
 
294
                                        loops = loops + 1
 
295
 
 
296
                                        local xVotes, yVotes = 0, 0
 
297
                                        local minChangeX, minChangeY, absMinChangeX, absMinChangeY
 
298
                                        local origX, origY = other.x, other.y
 
299
 
 
300
                                        for _, spr in pairs(displacers) do
 
301
                                                spr:displace(other)
 
302
                                                local xChange = other.x - origX
 
303
                                                local yChange = other.y - origY
 
304
 
 
305
                                                if xChange ~= 0 then
 
306
                                                        xVotes = xVotes + math.abs(xChange)
 
307
                                                        hit = true
 
308
 
 
309
                                                        if not minChangeX or math.abs(xChange) < absMinChangeX then
 
310
                                                                minChangeX = xChange
 
311
                                                                absMinChangeX = math.abs(xChange)
 
312
                                                        end
 
313
                                                end
 
314
 
 
315
                                                if yChange ~= 0 then
 
316
                                                        yVotes = yVotes + math.abs(yChange)
 
317
                                                        hit = true
 
318
 
 
319
                                                        if not minChangeY or math.abs(yChange) < absMinChangeY then
 
320
                                                                minChangeY = yChange
 
321
                                                                absMinChangeY = math.abs(yChange)
 
322
                                                        end
 
323
                                                end
 
324
 
 
325
                                                -- restore sprite to original position
 
326
 
 
327
                                                other.x = origX
 
328
                                                other.y = origY
 
329
                                        end
 
330
 
 
331
                                        if hit then
 
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
 
336
                                                end
 
337
                                        end
 
338
                                end
 
339
                        end
 
340
                end
 
341
        end,
 
342
 
 
343
        -- Method: setEffect
 
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.
 
349
        --
 
350
        -- Arguments:
 
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.
 
358
        --
 
359
        -- Returns:
 
360
        --              whether the effect was successfully created
 
361
 
 
362
        setEffect = function (self, filename, effectType)
 
363
                effectType = effectType or 'screen'
 
364
 
 
365
                if love.graphics.isSupported('pixeleffect') and
 
366
                   (effectType == 'sprite' or love.graphics.isSupported('canvas'))then
 
367
                        if filename then
 
368
                                self.effect = love.graphics.newPixelEffect(Cached:text(filename))
 
369
                                self.effectType = effectType
 
370
                        else
 
371
                                self.effect = nil
 
372
                        end
 
373
 
 
374
                        return true
 
375
                else
 
376
                        return false
 
377
                end
 
378
        end,
 
379
 
 
380
        -- Method: count
 
381
        -- Counts how many sprites are in this group.
 
382
        -- 
 
383
        -- Arguments:
 
384
        --              subgroups - include subgroups?
 
385
        -- 
 
386
        -- Returns:
 
387
        --              integer count
 
388
 
 
389
        count = function (self, subgroups)
 
390
                if subgroups then
 
391
                        local count = 0
 
392
 
 
393
                        for _, spr in pairs(self.sprites) do
 
394
                                if spr:instanceOf(Group) then
 
395
                                        count = count + spr:count(true)
 
396
                                else
 
397
                                        count = count + 1
 
398
                                end
 
399
                        end
 
400
 
 
401
                        return count
 
402
                else
 
403
                        return #self.sprites
 
404
                end
 
405
        end,
 
406
 
 
407
        -- Method: die
 
408
        -- Makes the group totally inert. It will not receive
 
409
        -- update events, draw anything, or be collided.
 
410
        --
 
411
        -- Arguments:
 
412
        --              none
 
413
        --
 
414
        -- Returns:
 
415
        --              nothing
 
416
 
 
417
        die = function (self)
 
418
                self.active = false
 
419
                self.visible = false
 
420
                self.solid = false
 
421
        end,
 
422
 
 
423
        -- Method: revive
 
424
        -- Makes this group completely active. It will receive
 
425
        -- update events, draw itself, and be collided.
 
426
        --
 
427
        -- Arguments:
 
428
        --              none
 
429
        --
 
430
        -- Returns:
 
431
        --              nothing
 
432
 
 
433
        revive = function (self)
 
434
                self.active = true
 
435
                self.visible = true
 
436
                self.solid = true
 
437
        end,
 
438
 
 
439
        -- Method: contains
 
440
        -- Returns whether this group contains a sprite.
 
441
        --
 
442
        -- Arguments:
 
443
        --              sprite - sprite to look for
 
444
        --              recurse - check subgroups? defaults to true
 
445
        --
 
446
        -- Returns:
 
447
        --              boolean
 
448
 
 
449
        contains = function (self, sprite, recurse)
 
450
                if recurse ~= false then recurse = true end
 
451
 
 
452
                for _, spr in pairs(self.sprites) do
 
453
                        if spr == sprite then return true end
 
454
 
 
455
                        if recurse and spr:instanceOf(Group) and spr:contains(sprite) then
 
456
                                return true
 
457
                        end
 
458
                end
 
459
 
 
460
                return false
 
461
        end,
 
462
 
 
463
        -- Method: grid
 
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].
 
469
        --
 
470
        -- This can be used to speed up work that involves checking
 
471
        -- for sprites near each other, e.g. collision detection.
 
472
        --
 
473
        -- Arguments:
 
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.
 
477
        --
 
478
        -- Returns:
 
479
        --              table
 
480
 
 
481
        grid = function (self, existing)
 
482
                local result = existing or {}
 
483
                local size = self.gridSize
 
484
 
 
485
                for _, spr in pairs(self.sprites) do
 
486
                        if spr.sprites then
 
487
                                local oldSize = spr.gridSize
 
488
                                spr.gridSize = self.gridSize
 
489
                                result = spr:grid(result)
 
490
                                spr.gridSize = oldSize
 
491
                        else
 
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)
 
496
 
 
497
                                for x = startX, endX do
 
498
                                        if not result[x] then result[x] = {} end
 
499
 
 
500
                                        for y = startY, endY do
 
501
                                                if not result[x][y] then result[x][y] = {} end
 
502
                                                table.insert(result[x][y], spr)
 
503
                                        end
 
504
                                end
 
505
                        end
 
506
                end
 
507
 
 
508
                return result
 
509
        end,
 
510
 
 
511
        -- passes startFrame events to member sprites
 
512
 
 
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
 
517
                
 
518
                for _, spr in pairs(self.sprites) do
 
519
                        if spr.active then spr:startFrame(elapsed) end
 
520
                end
 
521
        end,
 
522
 
 
523
        -- passes update events to member sprites
 
524
 
 
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
 
529
 
 
530
                for _, spr in pairs(self.sprites) do
 
531
                        if spr.active then spr:update(elapsed) end
 
532
                end
 
533
        end,
 
534
 
 
535
        -- passes endFrame events to member sprites
 
536
 
 
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
 
541
 
 
542
                for _, spr in pairs(self.sprites) do
 
543
                        if spr.active then spr:endFrame(elapsed) end
 
544
                end
 
545
        end,
 
546
 
 
547
        -- Method: draw
 
548
        -- Draws all visible member sprites onscreen.
 
549
        --
 
550
        -- Arguments:
 
551
        --              x - x offset in pixels
 
552
        --              y - y offset in pixels
 
553
 
 
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
 
558
                
 
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
 
563
 
 
564
                if self.effect then
 
565
                        if self.effectType == 'screen' then
 
566
                                if not self._canvas then self._canvas = love.graphics.newCanvas() end
 
567
                                self._canvas:clear()
 
568
                                love.graphics.setCanvas(self._canvas)
 
569
                        elseif self.effectType == 'sprite' then
 
570
                                love.graphics.setPixelEffect(self.effect)
 
571
                        end
 
572
                end
 
573
                
 
574
                for _, spr in pairs(self.sprites) do    
 
575
                        if spr.visible then
 
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
 
581
 
 
582
                                        if sprX < appWidth and sprX + spr.width > 0 and
 
583
                                           sprY < appHeight and sprY + spr.height > 0 then
 
584
                                                spr:draw(sprX, sprY)
 
585
                                        end
 
586
                                else
 
587
                                        spr:draw(scrollX, scrollY)
 
588
                                end
 
589
                        end
 
590
                end
 
591
                        
 
592
                if self.onDraw then self:onDraw() end
 
593
 
 
594
                if self.effect then
 
595
                        if self.effectType == 'screen' then
 
596
                                love.graphics.setPixelEffect(self.effect)
 
597
                                love.graphics.setCanvas()
 
598
                                love.graphics.draw(self._canvas)
 
599
                        end
 
600
 
 
601
                        love.graphics.setPixelEffect()
 
602
                end
 
603
        end,
 
604
 
 
605
        __tostring = function (self)
 
606
                local result = 'Group ('
 
607
 
 
608
                if self.active then
 
609
                        result = result .. 'active'
 
610
                else
 
611
                        result = result .. 'inactive'
 
612
                end
 
613
 
 
614
                if self.visible then
 
615
                        result = result .. ', visible'
 
616
                else
 
617
                        result = result .. ', invisible'
 
618
                end
 
619
 
 
620
                if self.solid then
 
621
                        result = result .. ', solid'
 
622
                else
 
623
                        result = result .. ', not solid'
 
624
                end
 
625
 
 
626
                return result .. ', ' .. self:count(true) .. ' sprites)'
 
627
        end
 
628
}