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
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.
12
-- If you don't need something to display onscreen, just
13
-- to listen to updates, set the sprite's visible property to false.
19
-- Called once each frame, with the elapsed time since the last frame in seconds.
21
-- Event: onBeginFrame
22
-- Called once each frame like onUpdate, but guaranteed to fire before any others' onUpdate handlers.
25
-- Called once each frame like onUpdate, but guaranteed to fire after all others' onUpdate handlers.
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.
35
-- If false, the sprite will not receive an update-related events.
39
-- If false, the sprite will not draw itself onscreen.
43
-- If false, the sprite will never be eligible to collide with another one.
44
-- It will also never displace another one.
48
-- Horizontal position in pixels. 0 is the left edge of the window.
52
-- Vertical position in pixels. 0 is the top edge of the window.
62
-- Rotation of drawn sprite in radians. This does not affect the bounds
63
-- used during collision checking.
67
-- Motion either along the x or y axes, or rotation about its center, in
69
velocity = { x = 0, y = 0, rotation = 0 },
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 },
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 },
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 },
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 },
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.
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 },
104
-- If set to true, then the sprite will draw flipped horizontally.
108
-- If set to true, then the sprite will draw flipped vertically.
112
-- This affects the transparency at which the sprite is drawn onscreen. 1 is fully
113
-- opaque; 0 is completely transparent.
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
123
-- Makes the sprite totally inert. It will not receive
124
-- update events, draw anything, or be collided.
132
die = function (self)
139
-- Makes this sprite completely active. It will receive
140
-- update events, draw itself, and be collided.
148
revive = function (self)
154
-- Method: intersects
155
-- Returns whether a point or rectangle intersects this sprite.
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
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
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.
177
-- x - top left horizontal coordinate
178
-- y - top left vertical coordinate
179
-- width - width of the rectangle
180
-- height - height of the rectangles
183
-- Two numbers: horizontal overlap in pixels, and vertical overlap in pixels.
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
191
-- this is cribbed from
192
-- http://frey.co.nz/old/2007/11/area-of-two-rectangles-algorithm/
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)
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.
209
-- ... - any number of <Sprite>s or <Group>s to collide with.
214
collide = function (self, ...)
215
Collision:check(self, ...)
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.
226
-- This does *not* cause onCollide events to occur on the sprites.
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
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
243
if other.sprites then
246
for _, spr in pairs(other.sprites) do
247
self:displace(spr, xHint, yHint)
252
local xOverlap, yOverlap = self:overlap(other.x, other.y, other.width, other.height)
254
-- resolve horizontal overlap
256
if xOverlap ~= 0 then
257
local leftMove = (other.x - self.x) + other.width
258
local rightMove = (self.x + self.width) - other.x
260
if xHint == LEFT then
262
elseif xHint == RIGHT then
265
if leftMove < rightMove then
273
-- resolve vertical overlap
275
if yOverlap ~= 0 then
276
local upMove = (other.y - self.y) + other.height
277
local downMove = (self.y + self.height) - other.y
281
elseif yHint == DOWN then
284
if upMove < downMove then
292
-- choose the option that moves the other sprite the least
294
if math.abs(xChange) > math.abs(yChange) then
295
other.y = other.y + yChange
297
other.x = other.x + xChange
303
-- Moves another sprite as if it had the same motion properties as this one.
306
-- other - other sprite to push
307
-- elapsed - elapsed time to simulate, in seconds
312
push = function (self, other, elapsed)
313
other.x = other.x + self.velocity.x * elapsed
314
other.y = other.y + self.velocity.y * elapsed
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.
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.
326
-- distance in pixels
328
distanceTo = function (self, ...)
330
local midX = self.x + self.width / 2
331
local midY = self.y + self.width / 2
337
assert(type(spr.x) == 'number' and type(spr.y) == 'number', 'asked to calculate distance to an object without numeric x and y properties')
340
local sprX = spr.x + spr.width / 2
341
local sprY = spr.y + spr.height / 2
343
return math.sqrt((midX - sprX)^2 + (midY - sprY)^2)
345
return math.sqrt((midX - arg[1])^2 + (midY - arg[2])^2)
349
startFrame = function (self, elapsed)
350
if self.onStartFrame then self:onStartFrame(elapsed) end
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
360
-- check existence of properties
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')
372
vel.rotation = vel.rotation or 0
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
380
if acc.x and acc.x ~= 0 then
381
vel.x = vel.x + acc.x * elapsed
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
394
if acc.y and acc.y ~= 0 then
395
vel.y = vel.y + acc.y * elapsed
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
408
if acc.rotation and acc.rotation ~= 0 then
409
vel.rotation = vel.rotation + acc.rotation * elapsed
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
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
429
if self.onUpdate then self:onUpdate(elapsed) end
432
endFrame = function (self, elapsed)
433
if self.onEndFrame then self:onEndFrame(elapsed) end
436
draw = function (self, x, y)
437
-- subclasses do interesting things here
440
collidedWith = function (self, other, xOverlap, yOverlap)
441
if self.onCollide then self:onCollide(other, xOverlap, yOverlap) end