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 after drawing takes place.
22
-- Called once each frame, with the elapsed time since the last frame in seconds.
24
-- Event: onBeginFrame
25
-- Called once each frame like onUpdate, but guaranteed to fire before any others' onUpdate handlers.
28
-- Called once each frame like onUpdate, but guaranteed to fire after all others' onUpdate handlers.
31
-- Called when the sprite intersects another during a collide() call. When a collision is detected,
32
-- this event occurs for both sprites. The sprite is passed three arguments: the other sprite, the
33
-- horizontal overlap, and the vertical overlap between the other sprite, in pixels.
38
-- If false, the sprite will not receive an update-related events.
42
-- If false, the sprite will not draw itself onscreen.
46
-- If false, the sprite will never be eligible to collide with another one.
47
-- It will also never displace another one.
51
-- Horizontal position in pixels. 0 is the left edge of the window.
55
-- Vertical position in pixels. 0 is the top edge of the window.
65
-- Rotation of drawn sprite in radians. This does not affect the bounds
66
-- used during collision checking.
70
-- Motion either along the x or y axes, or rotation about its center, in
72
velocity = { x = 0, y = 0, rotation = 0 },
74
-- Property: minVelocity
75
-- No matter what else may affect this sprite's velocity, it
76
-- will never go below these numbers.
77
minVelocity = { x = - math.huge, y = - math.huge, rotation = - math.huge },
79
-- Property: maxVelocity
80
-- No matter what else may affect this sprite's velocity, it will
81
-- never go above these numbers.
82
maxVelocity = { x = math.huge, y = math.huge, rotation = math.huge },
84
-- Property: acceleration
85
-- Acceleration along the x or y axes, or rotation about its center, in
86
-- pixels per second squared.
87
acceleration = { x = 0, y = 0, rotation = 0 },
90
-- This property is only active when the related acceleration is 0. In those
91
-- instances, it applies acceleration towards 0 for the given property. i.e.
92
-- when the velocity is positive, it applies a negative acceleration.
93
drag = { x = 0, y = 0, rotation = 0 },
96
-- This affects how the sprite is drawn onscreen. e.g. a sprite with scale 2 will
97
-- display twice as big. Scaling is centered around the sprite's center. This has
98
-- no effect on collision detection.
102
-- This allows you to scale a sprite in a distorted fashion by defining ratios
103
-- between x and y scales.
104
distort = { x = 1, y = 1 },
107
-- If set to true, then the sprite will draw flipped horizontally.
111
-- If set to true, then the sprite will draw flipped vertically.
115
-- This affects the transparency at which the sprite is drawn onscreen. 1 is fully
116
-- opaque; 0 is completely transparent.
120
-- This tints the sprite a color onscreen. This goes in RGB order; each number affects
121
-- how that particular channel is drawn. e.g. to draw the sprite in red only, set tint to
126
-- Makes the sprite totally inert. It will not receive
127
-- update events, draw anything, or be collided.
135
die = function (self)
142
-- Makes this sprite completely active. It will receive
143
-- update events, draw itself, and be collided.
151
revive = function (self)
157
-- Method: intersects
158
-- Returns whether a point or rectangle intersects this sprite.
161
-- x - top left horizontal coordinate
162
-- y - top left vertical coordinate
163
-- width - width of the rectangle, omit for points
164
-- height - height of the rectangle, omit for points
169
intersects = function (self, x, y, width, height)
170
return self.x < x + (width or 0) and self.x + self.width > x and
171
self.y < y + (height or 0) and self.y + self.height > y
175
-- Returns the horizontal and vertical overlap of this sprite
176
-- and a rectangle. This ignores the sprite's <solid> property
177
-- and does not trigger any <onCollide> events.
180
-- x - top left horizontal coordinate
181
-- y - top left vertical coordinate
182
-- width - width of the rectangle
183
-- height - height of the rectangles
186
-- Two numbers: horizontal overlap in pixels, and vertical overlap in pixels.
188
overlap = function (self, x, y, width, height)
189
local selfRight = self.x + self.width
190
local selfBottom = self.y + self.height
191
local right = x + width
192
local bottom = y + height
194
-- this is cribbed from
195
-- http://frey.co.nz/old/2007/11/area-of-two-rectangles-algorithm/
197
if self.x < right and selfRight > x and
198
self.y < bottom and selfBottom > y then
199
return math.min(selfRight, right) - math.max(self.x, x),
200
math.min(selfBottom, bottom) - math.max(self.y, y)
207
-- Checks whether sprites collide by checking rectangles. If a collision is detected,
208
-- onCollide() is called on both this sprite and the one it collides with, passing
209
-- the amount of horizontal and vertical overlap between the sprites in pixels.
212
-- other - <Sprite> or <Group> to collide
215
-- boolean, whether any collision was detected
217
collide = function (self, other)
218
if not self.solid or not other.solid or self == other then return false end
220
if other.sprites then
221
return other:collide(self)
223
local xOverlap, yOverlap = self:overlap(other.x, other.y, other.width, other.height)
225
if xOverlap ~= 0 or yOverlap ~= 0 then
226
if self.onCollide then
227
self:onCollide(other, xOverlap, yOverlap)
230
if other.onCollide then
231
other:onCollide(self, xOverlap, yOverlap)
242
-- Displaces another sprite or group so that it no longer overlaps this one.
243
-- This by default seeks to move the other sprite the least amount possible.
244
-- You can give this function a hint about which way it ought to move the other
245
-- sprite (e.g. by consulting its current motion) through the two optional
246
-- arguments. A single displace() call will *either* move the other sprite
247
-- horizontally or vertically, not along both axes.
249
-- This does *not* cause onCollide events to occur on the sprites.
252
-- other - sprite or group to displace
253
-- xHint - force horizontal displacement in one direction, uses direction constants, optional
254
-- yHint - force vertical displacement in one direction, uses direction constants, optional
259
displace = function (self, other, xHint, yHint)
260
if not self.solid or self == other or not other.solid then return end
261
if STRICT then assert(other:instanceOf(Sprite), 'asked to displace a non-sprite') end
266
if other.sprites then
269
for _, spr in pairs(other.sprites) do
270
self:displace(spr, xHint, yHint)
275
local xOverlap, yOverlap = self:overlap(other.x, other.y, other.width, other.height)
277
-- resolve horizontal overlap
279
if xOverlap ~= 0 then
280
local leftMove = (other.x - self.x) + other.width
281
local rightMove = (self.x + self.width) - other.x
283
if xHint == LEFT then
285
elseif xHint == RIGHT then
288
if leftMove < rightMove then
296
-- resolve vertical overlap
298
if yOverlap ~= 0 then
299
local upMove = (other.y - self.y) + other.height
300
local downMove = (self.y + self.height) - other.y
304
elseif yHint == DOWN then
307
if upMove < downMove then
315
-- choose the option that moves the other sprite the least
317
if math.abs(xChange) > math.abs(yChange) then
318
other.y = other.y + yChange
320
other.x = other.x + xChange
326
-- Moves another sprite as if it had the same motion properties as this one.
329
-- other - other sprite to push
330
-- elapsed - elapsed time to simulate, in seconds
335
push = function (self, other, elapsed)
336
other.x = other.x + self.velocity.x * elapsed
337
other.y = other.y + self.velocity.y * elapsed
340
-- Method: distanceTo
341
-- Returns the distance from this sprite to either another sprite or
342
-- an arbitrary point. This uses the center of sprites to calculate the distance.
345
-- Can be either one argument, a sprite (or any other table with x
346
-- and y properties), or two arguments, which correspond to a point.
349
-- distance in pixels
351
distanceTo = function (self, ...)
353
local midX = self.x + self.width / 2
354
local midY = self.y + self.width / 2
360
assert(type(spr.x) == 'number' and type(spr.y) == 'number', 'asked to calculate distance to an object without numeric x and y properties')
363
local sprX = spr.x + spr.width / 2
364
local sprY = spr.y + spr.height / 2
366
return math.sqrt((midX - sprX)^2 + (midY - sprY)^2)
368
return math.sqrt((midX - arg[1])^2 + (midY - arg[2])^2)
372
startFrame = function (self, elapsed)
373
if self.onStartFrame then self:onStartFrame(elapsed) end
376
update = function (self, elapsed)
377
local vel = self.velocity
378
local acc = self.acceleration
379
local drag = self.drag
380
local minVel = self.minVelocity
381
local maxVel = self.maxVelocity
383
-- check existence of properties
386
assert(vel, 'active sprite has no velocity property')
387
assert(acc, 'active sprite has no acceleration property')
388
assert(drag, 'active sprite has no drag property')
389
assert(minVel, 'active sprite has no minVelocity property')
390
assert(maxVel, 'active sprite has no maxVelocity property')
395
vel.rotation = vel.rotation or 0
399
if vel.x ~= 0 then self.x = self.x + vel.x * elapsed end
400
if vel.y ~= 0 then self.y = self.y + vel.y * elapsed end
401
if vel.rotation ~= 0 then self.rotation = self.rotation + vel.rotation * elapsed end
403
if acc.x and acc.x ~= 0 then
404
vel.x = vel.x + acc.x * elapsed
408
vel.x = vel.x - drag.x * elapsed
409
if vel.x < 0 then vel.x = 0 end
410
elseif vel.x < 0 then
411
vel.x = vel.x + drag.x * elapsed
412
if vel.x > 0 then vel.x = 0 end
417
if acc.y and acc.y ~= 0 then
418
vel.y = vel.y + acc.y * elapsed
422
vel.y = vel.y - drag.y * elapsed
423
if vel.y < 0 then vel.y = 0 end
424
elseif vel.y < 0 then
425
vel.y = vel.y + drag.y * elapsed
426
if vel.y > 0 then vel.y = 0 end
431
if acc.rotation and acc.rotation ~= 0 then
432
vel.rotation = vel.rotation + acc.rotation * elapsed
434
if drag.rotation then
435
if vel.rotation > 0 then
436
vel.rotation = vel.rotation - drag.rotation * elapsed
437
if vel.rotation < 0 then vel.rotation = 0 end
438
elseif vel.rotation < 0 then
439
vel.rotation = vel.rotation + drag.rotation * elapsed
440
if vel.rotation > 0 then vel.rotation = 0 end
445
if minVel.x and vel.x < minVel.x then vel.x = minVel.x end
446
if maxVel.x and vel.x > maxVel.x then vel.x = maxVel.x end
447
if minVel.y and vel.y < minVel.y then vel.y = minVel.y end
448
if maxVel.y and vel.y > maxVel.y then vel.y = maxVel.y end
449
if minVel.rotation and vel.rotation < minVel.rotation then vel.rotation = minVel.rotation end
450
if maxVel.rotation and vel.rotation > maxVel.rotation then vel.rotation = maxVel.rotation end
452
if self.onUpdate then self:onUpdate(elapsed) end
455
endFrame = function (self, elapsed)
456
if self.onEndFrame then self:onEndFrame(elapsed) end
459
draw = function (self, x, y)
460
if self.onDraw then self:onDraw(x, y) end