bzr branch
/bzr/spacey
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 |
}
|