/zoeplat

To get this branch, use:
bzr branch http://9ix.org/bzr/zoeplat
1 by Josh C
zoetrope 1.3.1
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
}