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