/ld26

To get this branch, use:
bzr branch http://9ix.org/bzr/ld26

« back to all changes in this revision

Viewing changes to zoetrope/core/group.lua

  • Committer: Josh C
  • Date: 2013-04-27 16:36:06 UTC
  • Revision ID: josh@9ix.org-20130427163606-0eef0f5v9wbzoi0j
zoetrope 1.4

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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: onUpdate
 
9
-- Called once each frame, with the elapsed time since the last frame in seconds.
 
10
--
 
11
-- Event: onBeginFrame
 
12
-- Called once each frame like onUpdate, but guaranteed to fire before any others' onUpdate handlers.
 
13
--
 
14
-- Event: onEndFrame
 
15
-- Called once each frame like onUpdate, but guaranteed to fire after all others' onUpdate handlers.
 
16
 
 
17
Group = Class:extend
 
18
{
 
19
        -- Property: active
 
20
        -- If false, none of its member sprites will receive update-related events.
 
21
        active = true,
 
22
 
 
23
        -- Property: visible
 
24
        -- If false, none of its member sprites will be drawn.
 
25
        visible = true,
 
26
 
 
27
        -- Property: solid
 
28
        -- If false, nothing will collide against this group, nor will this group
 
29
        -- displace any other sprite. This does not prevent collision checking
 
30
        -- against individual sprites in this group, however.
 
31
        solid = true,
 
32
 
 
33
        -- Property: sprites
 
34
        -- A table of member sprites, in drawing order.
 
35
        sprites = {},
 
36
 
 
37
        -- Property: timeScale
 
38
        -- Multiplier for elapsed time; 1.0 is normal, 0 is completely frozen.
 
39
        timeScale = 1,
 
40
 
 
41
        -- Property: translate
 
42
        -- This table's x and y properties shift member sprites' positions when drawn.
 
43
        -- To draw sprites at their normal position, set both x and y to 0.
 
44
        translate = { x = 0, y = 0 },
 
45
        
 
46
        -- Property: translateScale
 
47
        -- This table's x and y properties multiply member sprites'
 
48
        -- positions, which you can use to simulate parallax scrolling. To draw
 
49
        -- sprites at their normal position, set both x and y to 1.
 
50
        translateScale = { x = 1, y = 1 },
 
51
 
 
52
        -- Method: add
 
53
        -- Adds a sprite to the group.
 
54
        --
 
55
        -- Arguments:
 
56
        --              sprite - <Sprite> to add
 
57
        --
 
58
        -- Returns:
 
59
        --              nothing
 
60
 
 
61
        add = function (self, sprite)
 
62
                assert(sprite, 'asked to add nil to a group')
 
63
                assert(sprite ~= self, "can't add a group to itself")
 
64
        
 
65
                if STRICT and self:contains(sprite) then
 
66
                        local info = debug.getinfo(2, 'Sl')
 
67
                        print('Warning: adding a sprite to a group it already belongs to (' ..
 
68
                                  info.short_src .. ' line ' .. info.currentline .. ')')
 
69
                end
 
70
 
 
71
                table.insert(self.sprites, sprite)
 
72
        end,
 
73
 
 
74
        -- Method: remove
 
75
        -- Removes a sprite from the group. If the sprite is
 
76
        -- not in the group, this does nothing.
 
77
        -- 
 
78
        -- Arguments:
 
79
        --              sprite - <Sprite> to remove
 
80
        -- 
 
81
        -- Returns:
 
82
        --              nothing
 
83
 
 
84
        remove = function (self, sprite)
 
85
                for i, spr in ipairs(self.sprites) do
 
86
                        if spr == sprite then
 
87
                                table.remove(self.sprites, i)
 
88
                                return
 
89
                        end
 
90
                end
 
91
                
 
92
                if STRICT then
 
93
                        local info = debug.getinfo(2, 'Sl')
 
94
                        print('Warning: asked to remove a sprite from a group it was not a member of (' ..
 
95
                                  info.short_src .. ' line ' .. info.currentline .. ')')
 
96
                end
 
97
        end,
 
98
 
 
99
        -- Method: moveToFront
 
100
        -- Moves a sprite in the group so that it is drawn on top
 
101
        -- of all other sprites in the group.
 
102
        --
 
103
        -- Arguments:
 
104
        --              sprite - <Sprite> to move, should already be a member of the group
 
105
        --
 
106
        -- Returns:
 
107
        --              nothing
 
108
 
 
109
        moveToFront = function (self, sprite)
 
110
                for i, spr in ipairs(self.sprites) do
 
111
                        if spr == sprite then
 
112
                                table.remove(self.sprites, i)
 
113
                                table.insert(self.sprites, sprite)
 
114
                                return
 
115
                        end
 
116
                end
 
117
 
 
118
                if STRICT then
 
119
                        print('Warning: asked to move sprite to front of group, but is not a member: ' .. sprite)
 
120
                end
 
121
        end,
 
122
 
 
123
        -- Method: moveToBack
 
124
        -- Moves a sprite in the group so that it is drawn below
 
125
        -- all other sprites in the group.
 
126
        --
 
127
        -- Arguments:
 
128
        --              sprite - <Sprite> to move, should already be a member of the group
 
129
        --
 
130
        -- Returns:
 
131
        --              nothing
 
132
        
 
133
        moveToBack = function (self, sprite)
 
134
                for i, spr in ipairs(self.sprites) do
 
135
                        if spr == sprite then
 
136
                                table.remove(self.sprites, i)
 
137
                                table.insert(self.sprites, 1, sprite)
 
138
                                return
 
139
                        end
 
140
                end
 
141
 
 
142
                if STRICT then
 
143
                        print('Asked to move sprite to back of group, but is not a member: ' .. sprite)
 
144
                end
 
145
        end,
 
146
 
 
147
        -- Method: sort
 
148
        -- Sorts members into a new draw sequence.
 
149
        --
 
150
        -- Arguments:
 
151
        --              func - function to perform the sort. This will receive two <Sprite>s as arguments;
 
152
        --                         the function must return whether the first should be drawn below the second.
 
153
        --
 
154
        -- Returns:
 
155
        --              nothing
 
156
 
 
157
        sort = function (self, func)
 
158
                table.sort(self.sprites, func)
 
159
        end,
 
160
 
 
161
        -- Method: collide
 
162
        -- Collides all solid sprites in the group with another sprite or group.
 
163
        -- This calls the <Sprite.onCollide> event handlers on all sprites that
 
164
        -- collide with the same arguments <Sprite.collide> does.
 
165
        --
 
166
        -- It's often useful to collide a group with itself, e.g. myGroup:collide().
 
167
        -- This checks for collisions between the sprites that make up the group.
 
168
        --
 
169
        -- Arguments:
 
170
        --              ... - any number of <Sprite>s or <Group>s to collide with. If none
 
171
        --                        are specified, the group collides with itself.
 
172
        -- 
 
173
        -- Returns:
 
174
        --              nothing
 
175
        --
 
176
        -- See Also:
 
177
        --              <Sprite.collide>
 
178
 
 
179
        collide = function (self, ...)
 
180
                local list = {...}
 
181
 
 
182
                if #list > 0 then
 
183
                        if STRICT then
 
184
                                for _, other in pairs(list) do
 
185
                                        assert(other:instanceOf(Group) or other:instanceOf(Sprite), 'asked to collide non-group/sprite ' ..
 
186
                                                   type(other))
 
187
                                end
 
188
                        end
 
189
 
 
190
                        Collision:check(self, ...)
 
191
                else
 
192
                        Collision:check(self, self)
 
193
                end
 
194
        end,
 
195
 
 
196
        -- Method: setEffect
 
197
        -- Sets a pixel effect to use while drawing sprites in this group.
 
198
        -- See https://love2d.org/wiki/PixelEffect for details on how pixel
 
199
        -- effects work. After this call, the group's effect property will be
 
200
        -- set up so you can send variables to it. Only one pixel effect can
 
201
        -- be active on a group at a time.
 
202
        --
 
203
        -- Arguments:
 
204
        --              filename - filename of effect source code; if nil, this
 
205
        --                                 clears any existing pixel effect.
 
206
        --              effectType - either 'screen' (applies the effect to the entire
 
207
        --                                       group once, via an offscreen canvas), or 'sprite'
 
208
        --                                       (applies to the effect to each individual draw operation).
 
209
        --                                       Screen effects use more resources, but certain effects
 
210
        --                                       need to work on the entire screen to be effective.
 
211
        --
 
212
        -- Returns:
 
213
        --              whether the effect was successfully created
 
214
 
 
215
        setEffect = function (self, filename, effectType)
 
216
                effectType = effectType or 'screen'
 
217
 
 
218
                if love.graphics.isSupported('pixeleffect') and
 
219
                   (effectType == 'sprite' or love.graphics.isSupported('canvas'))then
 
220
                        if filename then
 
221
                                self.effect = love.graphics.newPixelEffect(Cached:text(filename))
 
222
                                self.effectType = effectType
 
223
                        else
 
224
                                self.effect = nil
 
225
                        end
 
226
 
 
227
                        return true
 
228
                else
 
229
                        return false
 
230
                end
 
231
        end,
 
232
 
 
233
        -- Method: count
 
234
        -- Counts how many sprites are in this group.
 
235
        -- 
 
236
        -- Arguments:
 
237
        --              subgroups - include subgroups?
 
238
        -- 
 
239
        -- Returns:
 
240
        --              integer count
 
241
 
 
242
        count = function (self, subgroups)
 
243
                if subgroups then
 
244
                        local count = 0
 
245
 
 
246
                        for _, spr in pairs(self.sprites) do
 
247
                                if spr:instanceOf(Group) then
 
248
                                        count = count + spr:count(true)
 
249
                                else
 
250
                                        count = count + 1
 
251
                                end
 
252
                        end
 
253
 
 
254
                        return count
 
255
                else
 
256
                        return #self.sprites
 
257
                end
 
258
        end,
 
259
 
 
260
        -- Method: die
 
261
        -- Makes the group totally inert. It will not receive
 
262
        -- update events, draw anything, or be collided.
 
263
        --
 
264
        -- Arguments:
 
265
        --              none
 
266
        --
 
267
        -- Returns:
 
268
        --              nothing
 
269
 
 
270
        die = function (self)
 
271
                self.active = false
 
272
                self.visible = false
 
273
                self.solid = false
 
274
        end,
 
275
 
 
276
        -- Method: revive
 
277
        -- Makes this group completely active. It will receive
 
278
        -- update events, draw itself, and be collided.
 
279
        --
 
280
        -- Arguments:
 
281
        --              none
 
282
        --
 
283
        -- Returns:
 
284
        --              nothing
 
285
 
 
286
        revive = function (self)
 
287
                self.active = true
 
288
                self.visible = true
 
289
                self.solid = true
 
290
        end,
 
291
 
 
292
        -- Method: contains
 
293
        -- Returns whether this group contains a sprite.
 
294
        --
 
295
        -- Arguments:
 
296
        --              sprite - sprite to look for
 
297
        --              recurse - check subgroups? defaults to true
 
298
        --
 
299
        -- Returns:
 
300
        --              boolean
 
301
 
 
302
        contains = function (self, sprite, recurse)
 
303
                if recurse ~= false then recurse = true end
 
304
 
 
305
                for _, spr in pairs(self.sprites) do
 
306
                        if spr == sprite then return true end
 
307
 
 
308
                        if recurse and spr:instanceOf(Group) and spr:contains(sprite) then
 
309
                                return true
 
310
                        end
 
311
                end
 
312
 
 
313
                return false
 
314
        end,
 
315
 
 
316
        -- Method: loadLayers
 
317
        -- Loads layers from a Lua source file (as generated by Tiled -- http://mapeditor.org).
 
318
        -- Each layer is created as a <Group> belonging to this one and added to preserve its
 
319
        -- ordering. Tile layers are created as <Map> instances; object layers will try to create
 
320
        -- instances of a class named by the object's name property. If no class exists by
 
321
        -- this name, or the object has no name property, a gray fill will be created instead,
 
322
        -- as a placeholder. If the object has a property named _the, then this will set
 
323
        -- the.[whatever] to it.
 
324
        --
 
325
        -- Arguments:
 
326
        --              file - filename to load
 
327
        --              tileClass - class to create tiles in tile layers with; constructor
 
328
        --                                  will be called with properties: image, width,
 
329
        --                                  height, imageOffset (with x and y sub-properties)
 
330
        --
 
331
        -- Returns:
 
332
        --              nothing
 
333
 
 
334
        loadLayers = function (self, file, tileClass)
 
335
                local ok, data = pcall(loadstring(Cached:text(file)))
 
336
                local _, _, directory = string.find(file, '^(.*[/\\])')
 
337
                directory = directory or ''
 
338
 
 
339
                if ok then
 
340
                        -- store tile properties by gid
 
341
                        
 
342
                        local tileProtos = {}
 
343
 
 
344
                        for _, tileset in pairs(data.tilesets) do
 
345
                                for _, tile in pairs(tileset.tiles) do
 
346
                                        local id = tileset.firstgid + tile.id
 
347
                                        
 
348
                                        for key, value in pairs(tile.properties) do
 
349
                                                tile.properties[key] = tovalue(value)
 
350
                                        end
 
351
 
 
352
                                        tileProtos[id] = tile
 
353
                                        tileProtos[id].width = tileset.tilewidth
 
354
                                        tileProtos[id].height = tileset.tileheight
 
355
                                end
 
356
                        end
 
357
 
 
358
                        for _, layer in pairs(data.layers) do
 
359
                                if self.prototype[layer.name] then
 
360
                                        error('The class you are loading layers into reserves the ' .. layer.name .. ' property for its own use; you cannot load a layer with that name')
 
361
                                end
 
362
 
 
363
                                if STRICT and self[layer.name] then
 
364
                                        local info = debug.getinfo(2, 'Sl')
 
365
                                        print('Warning: a property named ' .. layer.name .. ' already exists in this group (' ..
 
366
                                                  info.short_src .. ', line ' .. info.currentline .. ')')
 
367
                                end
 
368
 
 
369
                                if layer.type == 'tilelayer' then
 
370
                                        local map = Map:new{ spriteWidth = data.tilewidth, spriteHeight = data.tileheight }
 
371
                                        map:empty(layer.width, layer.height)
 
372
 
 
373
                                        -- load tiles
 
374
 
 
375
                                        for _, tiles in pairs(data.tilesets) do
 
376
                                                map:loadTiles(directory .. tiles.image, tileClass or Tile, tiles.firstgid)
 
377
 
 
378
                                                -- and mix in properties where applicable
 
379
 
 
380
                                                for id, tile in pairs(tileProtos) do
 
381
                                                        if map.sprites[id] then
 
382
                                                                map.sprites[id]:mixin(tile.properties)
 
383
                                                        end
 
384
                                                end
 
385
                                        end
 
386
 
 
387
                                        -- load tile data
 
388
 
 
389
                                        local x = 1
 
390
                                        local y = 1
 
391
 
 
392
                                        for _, val in ipairs(layer.data) do
 
393
                                                map.map[x][y] = val
 
394
                                                x = x + 1
 
395
 
 
396
                                                if x > layer.width then
 
397
                                                        x = 1
 
398
                                                        y = y + 1
 
399
                                                end
 
400
                                        end
 
401
 
 
402
                                        self[layer.name] = map
 
403
                                        self:add(map)
 
404
                                elseif layer.type == 'objectgroup' then
 
405
                                        local group = Group:new()
 
406
 
 
407
                                        for _, obj in pairs(layer.objects) do
 
408
                                                -- roll in tile properties if based on a tile
 
409
 
 
410
                                                if obj.gid and tileProtos[obj.gid] then
 
411
                                                        local tile = tileProtos[obj.gid]
 
412
 
 
413
                                                        obj.name = tile.properties.name
 
414
                                                        obj.width = tile.width
 
415
                                                        obj.height = tile.height
 
416
 
 
417
                                                        for key, value in pairs(tile.properties) do
 
418
                                                                obj.properties[key] = tovalue(value)
 
419
                                                        end
 
420
 
 
421
                                                        -- Tiled tile-based objects measure their y
 
422
                                                        -- position at their lower-left corner, instead
 
423
                                                        -- of their upper-left corner as usual
 
424
 
 
425
                                                        obj.y = obj.y - obj.height
 
426
                                                end
 
427
 
 
428
                                                -- create a new object if the class does exist
 
429
 
 
430
                                                local spr
 
431
 
 
432
                                                if _G[obj.name] then
 
433
                                                        obj.properties.x = obj.x
 
434
                                                        obj.properties.y = obj.y
 
435
                                                        obj.properties.width = obj.width
 
436
                                                        obj.properties.height = obj.height
 
437
 
 
438
                                                        spr = _G[obj.name]:new(obj.properties)
 
439
                                                else
 
440
                                                        spr = Fill:new{ x = obj.x, y = obj.y, width = obj.width, height = obj.height, fill = { 128, 128, 128 } }
 
441
                                                end
 
442
 
 
443
                                                if obj.properties._the then
 
444
                                                        the[obj.properties._the] = spr
 
445
                                                end
 
446
 
 
447
                                                group:add(spr)
 
448
                                        end
 
449
 
 
450
                                        self[layer.name] = group
 
451
                                        self:add(group)
 
452
                                else
 
453
                                        error("don't know how to create a " .. layer.type .. " layer from file data")
 
454
                                end
 
455
                        end
 
456
                else
 
457
                        error('could not load layers from file: ' .. data)
 
458
                end
 
459
        end,
 
460
 
 
461
        -- passes startFrame events to member sprites
 
462
 
 
463
        startFrame = function (self, elapsed)
 
464
                if not self.active then return end
 
465
                elapsed = elapsed * self.timeScale
 
466
                
 
467
                for _, spr in pairs(self.sprites) do
 
468
                        if spr.active then spr:startFrame(elapsed) end
 
469
                end
 
470
 
 
471
                if self.onStartFrame then self:onStartFrame(elapsed) end
 
472
        end,
 
473
 
 
474
        -- passes update events to member sprites
 
475
 
 
476
        update = function (self, elapsed)
 
477
                if not self.active then return end
 
478
                elapsed = elapsed * self.timeScale
 
479
 
 
480
                for _, spr in pairs(self.sprites) do
 
481
                        if spr.active then spr:update(elapsed) end
 
482
                end
 
483
 
 
484
                if self.onUpdate then self:onUpdate(elapsed) end
 
485
        end,
 
486
 
 
487
        -- passes endFrame events to member sprites
 
488
 
 
489
        endFrame = function (self, elapsed)
 
490
                if not self.active then return end
 
491
                elapsed = elapsed * self.timeScale
 
492
 
 
493
                for _, spr in pairs(self.sprites) do
 
494
                        if spr.active then spr:endFrame(elapsed) end
 
495
                end
 
496
 
 
497
                if self.onEndFrame then self:onEndFrame(elapsed) end
 
498
        end,
 
499
 
 
500
        -- Method: draw
 
501
        -- Draws all visible member sprites onscreen.
 
502
        --
 
503
        -- Arguments:
 
504
        --              x - x offset in pixels
 
505
        --              y - y offset in pixels
 
506
 
 
507
        draw = function (self, x, y)
 
508
                if not self.visible then return end
 
509
                x = x or self.translate.x
 
510
                y = y or self.translate.y
 
511
                
 
512
                local scrollX = x * self.translateScale.x
 
513
                local scrollY = y * self.translateScale.y
 
514
                local appWidth = the.app.width
 
515
                local appHeight = the.app.height
 
516
 
 
517
                if self.effect then
 
518
                        if self.effectType == 'screen' then
 
519
                                if not self._canvas then self._canvas = love.graphics.newCanvas() end
 
520
                                self._canvas:clear()
 
521
                                love.graphics.setCanvas(self._canvas)
 
522
                        elseif self.effectType == 'sprite' then
 
523
                                love.graphics.setPixelEffect(self.effect)
 
524
                        end
 
525
                end
 
526
                
 
527
                for _, spr in pairs(self.sprites) do    
 
528
                        if spr.visible then
 
529
                                if spr.translate then
 
530
                                        spr:draw(spr.translate.x + scrollX, spr.translate.y + scrollY)
 
531
                                elseif spr.x and spr.y and spr.width and spr.height then
 
532
                                        local sprX = spr.x + scrollX
 
533
                                        local sprY = spr.y + scrollY
 
534
 
 
535
                                        if sprX < appWidth and sprX + spr.width > 0 and
 
536
                                           sprY < appHeight and sprY + spr.height > 0 then
 
537
                                                spr:draw(sprX, sprY)
 
538
                                        end
 
539
                                else
 
540
                                        spr:draw(scrollX, scrollY)
 
541
                                end
 
542
                        end
 
543
                end
 
544
                        
 
545
                if self.effect then
 
546
                        if self.effectType == 'screen' then
 
547
                                love.graphics.setPixelEffect(self.effect)
 
548
                                love.graphics.setCanvas()
 
549
                                love.graphics.draw(self._canvas)
 
550
                        end
 
551
 
 
552
                        love.graphics.setPixelEffect()
 
553
                end
 
554
        end,
 
555
 
 
556
        __tostring = function (self)
 
557
                local result = 'Group ('
 
558
 
 
559
                if self.active then
 
560
                        result = result .. 'active'
 
561
                else
 
562
                        result = result .. 'inactive'
 
563
                end
 
564
 
 
565
                if self.visible then
 
566
                        result = result .. ', visible'
 
567
                else
 
568
                        result = result .. ', invisible'
 
569
                end
 
570
 
 
571
                if self.solid then
 
572
                        result = result .. ', solid'
 
573
                else
 
574
                        result = result .. ', not solid'
 
575
                end
 
576
 
 
577
                return result .. ', ' .. self:count(true) .. ' sprites)'
 
578
        end
 
579
}