bzr branch
http://9ix.org/bzr/traderous
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 |
} |