/zoeplat

To get this branch, use:
bzr branch http://9ix.org/bzr/zoeplat
1 by Josh C
zoetrope 1.3.1
1
-- Class: Sprite
2
-- A sprite receives all update-related events and draws
3
-- itself onscreen with its draw() method. It is defined
4
-- by a rectangle; nothing it draws should be outside that
5
-- rectangle.
6
--
7
-- In most cases, you don't want to create a sprite directly.
8
-- Instead, you'd want to use a subclass tailored to your needs.
9
-- Create a new subclass if you need to heavily customize how a 
10
-- sprite is drawn onscreen.
11
--
12
-- If you don't need something to display onscreen, just
13
-- to listen to updates, set the sprite's visible property to false.
14
--
15
-- Extends:
16
--		<Class>
17
--
18
-- Event: onDraw
19
-- Called after drawing takes place.
20
--
21
-- Event: onUpdate
22
-- Called once each frame, with the elapsed time since the last frame in seconds.
23
--
24
-- Event: onBeginFrame
25
-- Called once each frame like onUpdate, but guaranteed to fire before any others' onUpdate handlers.
26
--
27
-- Event: onEndFrame
28
-- Called once each frame like onUpdate, but guaranteed to fire after all others' onUpdate handlers.
29
--
30
-- Event: onCollide
31
-- Called when the sprite intersects another during a collide() call. When a collision is detected,
32
-- this event occurs for both sprites. The sprite is passed three arguments: the other sprite, the
33
-- horizontal overlap, and the vertical overlap between the other sprite, in pixels.
34
35
Sprite = Class:extend
36
{
37
	-- Property: active
38
	-- If false, the sprite will not receive an update-related events.
39
	active = true,
40
41
	-- Property: visible
42
	-- If false, the sprite will not draw itself onscreen.
43
	visible = true,
44
45
	-- Property: solid
46
	-- If false, the sprite will never be eligible to collide with another one.
47
	-- It will also never displace another one.
48
	solid = true,
49
50
	-- Property: x
51
	-- Horizontal position in pixels. 0 is the left edge of the window.
52
	x = 0,
53
54
	-- Property: y
55
	-- Vertical position in pixels. 0 is the top edge of the window.
56
	y = 0,
57
58
	-- Property: width
59
	-- Width in pixels.
60
61
	-- Property: height
62
	-- Height in pixels.
63
64
	-- Property: rotation
65
	-- Rotation of drawn sprite in radians. This does not affect the bounds
66
	-- used during collision checking.
67
	rotation = 0,
68
69
	-- Property: velocity
70
	-- Motion either along the x or y axes, or rotation about its center, in
71
	-- pixels per second.
72
	velocity = { x = 0, y = 0, rotation = 0 },
73
74
	-- Property: minVelocity
75
	-- No matter what else may affect this sprite's velocity, it
76
	-- will never go below these numbers.
77
	minVelocity = { x = - math.huge, y = - math.huge, rotation = - math.huge },
78
79
	-- Property: maxVelocity
80
	-- No matter what else may affect this sprite's velocity, it will
81
	-- never go above these numbers.
82
	maxVelocity = { x = math.huge, y = math.huge, rotation = math.huge },
83
84
	-- Property: acceleration
85
	-- Acceleration along the x or y axes, or rotation about its center, in
86
	-- pixels per second squared.
87
	acceleration = { x = 0, y = 0, rotation = 0 },
88
89
	-- Property: drag
90
	-- This property is only active when the related acceleration is 0. In those
91
	-- instances, it applies acceleration towards 0 for the given property. i.e.
92
	-- when the velocity is positive, it applies a negative acceleration.
93
	drag = { x = 0, y = 0, rotation = 0 },
94
95
	-- Property: scale
96
	-- This affects how the sprite is drawn onscreen. e.g. a sprite with scale 2 will
97
	-- display twice as big. Scaling is centered around the sprite's center. This has
98
	-- no effect on collision detection.
99
	scale = 1,
100
101
	-- Property: distort
102
	-- This allows you to scale a sprite in a distorted fashion by defining ratios
103
	-- between x and y scales.
104
	distort = { x = 1, y = 1 },
105
106
	-- Property: flipX
107
	-- If set to true, then the sprite will draw flipped horizontally.
108
	flipX = false,
109
110
	-- Property: flipY
111
	-- If set to true, then the sprite will draw flipped vertically.
112
	flipY = false,
113
114
	-- Property: alpha
115
	-- This affects the transparency at which the sprite is drawn onscreen. 1 is fully
116
	-- opaque; 0 is completely transparent.
117
	alpha = 1,
118
119
	-- Property: tint
120
	-- This tints the sprite a color onscreen. This goes in RGB order; each number affects
121
	-- how that particular channel is drawn. e.g. to draw the sprite in red only, set tint to
122
	-- { 1, 0, 0 }.
123
	tint = { 1, 1, 1 },
124
125
	-- Method: die
126
	-- Makes the sprite totally inert. It will not receive
127
	-- update events, draw anything, or be collided.
128
	--
129
	-- Arguments:
130
	--		none
131
	--
132
	-- Returns:
133
	-- 		nothing
134
135
	die = function (self)
136
		self.active = false
137
		self.visible = false
138
		self.solid = false
139
	end,
140
141
	-- Method: revive
142
	-- Makes this sprite completely active. It will receive
143
	-- update events, draw itself, and be collided.
144
	--
145
	-- Arguments:
146
	--		none
147
	--
148
	-- Returns:
149
	-- 		nothing
150
151
	revive = function (self)
152
		self.active = true
153
		self.visible = true
154
		self.solid = true
155
	end,
156
157
	-- Method: intersects
158
	-- Returns whether a point or rectangle intersects this sprite.
159
	--
160
	-- Arguments:
161
	--		x - top left horizontal coordinate
162
	--		y - top left vertical coordinate
163
	--		width - width of the rectangle, omit for points
164
	--		height - height of the rectangle, omit for points
165
	--
166
	-- Returns:
167
	--		boolean
168
169
	intersects = function (self, x, y, width, height)
170
		return self.x < x + (width or 0) and self.x + self.width > x and
171
			   self.y < y + (height or 0) and self.y + self.height > y
172
	end,
173
174
	-- Method: overlap
175
	-- Returns the horizontal and vertical overlap of this sprite
176
	-- and a rectangle. This ignores the sprite's <solid> property
177
	-- and does not trigger any <onCollide> events.
178
	--
179
	-- Arguments:
180
	--		x - top left horizontal coordinate
181
	--		y - top left vertical coordinate
182
	--		width - width of the rectangle
183
	--		height - height of the rectangles
184
	--
185
	-- Returns:
186
	--		Two numbers: horizontal overlap in pixels, and vertical overlap in pixels.
187
188
	overlap = function (self, x, y, width, height)
189
		local selfRight = self.x + self.width
190
		local selfBottom = self.y + self.height
191
		local right = x + width
192
		local bottom = y + height
193
194
		-- this is cribbed from
195
		-- http://frey.co.nz/old/2007/11/area-of-two-rectangles-algorithm/
196
197
		if self.x < right and selfRight > x and
198
		   self.y < bottom and selfBottom > y then
199
			return math.min(selfRight, right) - math.max(self.x, x),
200
				   math.min(selfBottom, bottom) - math.max(self.y, y)
201
		else
202
			return 0, 0
203
		end
204
	end,
205
206
	-- Method: collide
207
	-- Checks whether sprites collide by checking rectangles. If a collision is detected,
208
	-- onCollide() is called on both this sprite and the one it collides with, passing
209
	-- the amount of horizontal and vertical overlap between the sprites in pixels.
210
	--
211
	-- Arguments:
212
	--		other - <Sprite> or <Group> to collide
213
	--
214
	-- Returns:
215
	--		boolean, whether any collision was detected
216
217
	collide = function (self, other)
218
		if not self.solid or not other.solid or self == other then return false end
219
220
		if other.sprites then
221
			return other:collide(self)
222
		else
223
			local xOverlap, yOverlap = self:overlap(other.x, other.y, other.width, other.height)
224
225
			if xOverlap ~= 0 or yOverlap ~= 0 then	
226
				if self.onCollide then
227
					self:onCollide(other, xOverlap, yOverlap)
228
				end
229
				
230
				if other.onCollide then
231
					other:onCollide(self, xOverlap, yOverlap)
232
				end
233
234
				return true
235
			end
236
		end
237
238
		return false
239
	end,
240
241
	-- Method: displace
242
	-- Displaces another sprite or group so that it no longer overlaps this one.
243
	-- This by default seeks to move the other sprite the least amount possible.
244
	-- You can give this function a hint about which way it ought to move the other
245
	-- sprite (e.g. by consulting its current motion) through the two optional
246
	-- arguments. A single displace() call will *either* move the other sprite
247
	-- horizontally or vertically, not along both axes.
248
	--
249
	-- This does *not* cause onCollide events to occur on the sprites.
250
	--
251
	-- Arguments:
252
	--		other - sprite or group to displace
253
	-- 		xHint - force horizontal displacement in one direction, uses direction constants, optional
254
	--		yHint - force vertical displacement in one direction, uses direction constants, optional
255
	--
256
	-- Returns:
257
	--		nothing
258
259
	displace = function (self, other, xHint, yHint)	
260
		if not self.solid or self == other or not other.solid then return end
261
		if STRICT then assert(other:instanceOf(Sprite), 'asked to displace a non-sprite') end
262
263
		local xChange = 0
264
		local yChange = 0
265
266
		if other.sprites then
267
			-- handle groups
268
269
			for _, spr in pairs(other.sprites) do
270
				self:displace(spr, xHint, yHint)
271
			end
272
		else
273
			-- handle sprites
274
275
			local xOverlap, yOverlap = self:overlap(other.x, other.y, other.width, other.height)
276
			
277
			-- resolve horizontal overlap
278
279
			if xOverlap ~= 0 then
280
				local leftMove = (other.x - self.x) + other.width
281
				local rightMove = (self.x + self.width) - other.x
282
				
283
				if xHint == LEFT then
284
					xChange = - leftMove
285
				elseif xHint == RIGHT then
286
					xChange = rightMove
287
				else
288
					if leftMove < rightMove then
289
						xChange = - leftMove
290
					else
291
						xChange = rightMove
292
					end
293
				end
294
			end
295
			
296
			-- resolve vertical overlap
297
298
			if yOverlap ~= 0 then
299
				local upMove = (other.y - self.y) + other.height
300
				local downMove = (self.y + self.height) - other.y
301
				
302
				if yHint == UP then
303
					yChange = - upMove
304
				elseif yHint == DOWN then
305
					yChange = downMove
306
				else
307
					if upMove < downMove then
308
						yChange = - upMove
309
					else
310
						yChange = downMove
311
					end
312
				end
313
			end
314
			
315
			-- choose the option that moves the other sprite the least
316
			
317
			if math.abs(xChange) > math.abs(yChange) then
318
				other.y = other.y + yChange
319
			else
320
				other.x = other.x + xChange
321
			end
322
		end
323
	end,
324
325
	-- Method: push
326
	-- Moves another sprite as if it had the same motion properties as this one.
327
	--
328
	-- Arguments:
329
	--		other - other sprite to push
330
	--		elapsed - elapsed time to simulate, in seconds
331
	--
332
	-- Returns:
333
	--		nothing
334
335
	push = function (self, other, elapsed)
336
		other.x = other.x + self.velocity.x * elapsed
337
		other.y = other.y + self.velocity.y * elapsed
338
	end,
339
340
	-- Method: distanceTo
341
	-- Returns the distance from this sprite to either another sprite or 
342
	-- an arbitrary point. This uses the center of sprites to calculate the distance.
343
	--
344
	-- Arguments:
345
	--		Can be either one argument, a sprite (or any other table with x
346
	--		and y properties), or two arguments, which correspond to a point.
347
	--
348
	-- Returns:
349
	--		distance in pixels
350
351
	distanceTo = function (self, ...)
352
		local arg = {...}
353
		local midX = self.x + self.width / 2
354
		local midY = self.y + self.width / 2
355
356
		if #arg == 1 then
357
			local spr = arg[1]
358
359
			if STRICT then
360
				assert(type(spr.x) == 'number' and type(spr.y) == 'number', 'asked to calculate distance to an object without numeric x and y properties')
361
			end
362
363
			local sprX = spr.x + spr.width / 2
364
			local sprY = spr.y + spr.height / 2
365
366
			return math.sqrt((midX - sprX)^2 + (midY - sprY)^2)
367
		else
368
			return math.sqrt((midX - arg[1])^2 + (midY - arg[2])^2)
369
		end
370
	end,
371
372
	startFrame = function (self, elapsed)
373
		if self.onStartFrame then self:onStartFrame(elapsed) end
374
	end,
375
376
	update = function (self, elapsed)
377
		local vel = self.velocity
378
		local acc = self.acceleration
379
		local drag = self.drag
380
		local minVel = self.minVelocity
381
		local maxVel = self.maxVelocity
382
383
		-- check existence of properties
384
385
		if STRICT then
386
			assert(vel, 'active sprite has no velocity property')
387
			assert(acc, 'active sprite has no acceleration property')
388
			assert(drag, 'active sprite has no drag property')
389
			assert(minVel, 'active sprite has no minVelocity property')
390
			assert(maxVel, 'active sprite has no maxVelocity property')
391
		end
392
393
		vel.x = vel.x or 0
394
		vel.y = vel.y or 0
395
		vel.rotation = vel.rotation or 0
396
397
		-- physics
398
			
399
		if vel.x ~= 0 then self.x = self.x + vel.x * elapsed end
400
		if vel.y ~= 0 then self.y = self.y + vel.y * elapsed end
401
		if vel.rotation ~= 0 then self.rotation = self.rotation + vel.rotation * elapsed end
402
		
403
		if acc.x and acc.x ~= 0 then
404
			vel.x = vel.x + acc.x * elapsed
405
		else
406
			if drag.x then
407
				if vel.x > 0 then
408
					vel.x = vel.x - drag.x * elapsed
409
					if vel.x < 0 then vel.x = 0 end
410
				elseif vel.x < 0 then
411
					vel.x = vel.x + drag.x * elapsed
412
					if vel.x > 0 then vel.x = 0 end
413
				end
414
			end
415
		end
416
		
417
		if acc.y and acc.y ~= 0 then
418
			vel.y = vel.y + acc.y * elapsed
419
		else
420
			if drag.y then
421
				if vel.y > 0 then
422
					vel.y = vel.y - drag.y * elapsed
423
					if vel.y < 0 then vel.y = 0 end
424
				elseif vel.y < 0 then
425
					vel.y = vel.y + drag.y * elapsed
426
					if vel.y > 0 then vel.y = 0 end
427
				end
428
			end
429
		end
430
		
431
		if acc.rotation and acc.rotation ~= 0 then
432
			vel.rotation = vel.rotation + acc.rotation * elapsed
433
		else
434
			if drag.rotation then
435
				if vel.rotation > 0 then
436
					vel.rotation = vel.rotation - drag.rotation * elapsed
437
					if vel.rotation < 0 then vel.rotation = 0 end
438
				elseif vel.rotation < 0 then
439
					vel.rotation = vel.rotation + drag.rotation * elapsed
440
					if vel.rotation > 0 then vel.rotation = 0 end
441
				end
442
			end
443
		end
444
445
		if minVel.x and vel.x < minVel.x then vel.x = minVel.x end
446
		if maxVel.x and vel.x > maxVel.x then vel.x = maxVel.x end
447
		if minVel.y and vel.y < minVel.y then vel.y = minVel.y end
448
		if maxVel.y and vel.y > maxVel.y then vel.y = maxVel.y end
449
		if minVel.rotation and vel.rotation < minVel.rotation then vel.rotation = minVel.rotation end
450
		if maxVel.rotation and vel.rotation > maxVel.rotation then vel.rotation = maxVel.rotation end
451
		
452
		if self.onUpdate then self:onUpdate(elapsed) end
453
	end,
454
455
	endFrame = function (self, elapsed)
456
		if self.onEndFrame then self:onEndFrame(elapsed) end
457
	end,
458
459
	draw = function (self, x, y)
460
		if self.onDraw then self:onDraw(x, y) end
461
	end
462
}