/ld27

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