/zoeplat

To get this branch, use:
bzr branch /bzr/zoeplat

« back to all changes in this revision

Viewing changes to zoetrope/core/sprite.lua

  • Committer: Josh C
  • Date: 2013-03-02 20:40:57 UTC
  • Revision ID: josh@9ix.org-20130302204057-yrra0a51zgtpq2v2
zoetrope 1.3.1

Show diffs side-by-side

added added

removed removed

 
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: onDraw
 
19
-- Called after drawing takes place.
 
20
--
 
21
-- Event: onUpdate
 
22
-- Called once each frame, with the elapsed time since the last frame in seconds.
 
23
--
 
24
-- Event: onBeginFrame
 
25
-- Called once each frame like onUpdate, but guaranteed to fire before any others' onUpdate handlers.
 
26
--
 
27
-- Event: onEndFrame
 
28
-- Called once each frame like onUpdate, but guaranteed to fire after all others' onUpdate handlers.
 
29
--
 
30
-- Event: onCollide
 
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.
 
34
 
 
35
Sprite = Class:extend
 
36
{
 
37
        -- Property: active
 
38
        -- If false, the sprite will not receive an update-related events.
 
39
        active = true,
 
40
 
 
41
        -- Property: visible
 
42
        -- If false, the sprite will not draw itself onscreen.
 
43
        visible = true,
 
44
 
 
45
        -- Property: solid
 
46
        -- If false, the sprite will never be eligible to collide with another one.
 
47
        -- It will also never displace another one.
 
48
        solid = true,
 
49
 
 
50
        -- Property: x
 
51
        -- Horizontal position in pixels. 0 is the left edge of the window.
 
52
        x = 0,
 
53
 
 
54
        -- Property: y
 
55
        -- Vertical position in pixels. 0 is the top edge of the window.
 
56
        y = 0,
 
57
 
 
58
        -- Property: width
 
59
        -- Width in pixels.
 
60
 
 
61
        -- Property: height
 
62
        -- Height in pixels.
 
63
 
 
64
        -- Property: rotation
 
65
        -- Rotation of drawn sprite in radians. This does not affect the bounds
 
66
        -- used during collision checking.
 
67
        rotation = 0,
 
68
 
 
69
        -- Property: velocity
 
70
        -- Motion either along the x or y axes, or rotation about its center, in
 
71
        -- pixels per second.
 
72
        velocity = { x = 0, y = 0, rotation = 0 },
 
73
 
 
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 },
 
78
 
 
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 },
 
83
 
 
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 },
 
88
 
 
89
        -- Property: drag
 
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 },
 
94
 
 
95
        -- Property: scale
 
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.
 
99
        scale = 1,
 
100
 
 
101
        -- Property: distort
 
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 },
 
105
 
 
106
        -- Property: flipX
 
107
        -- If set to true, then the sprite will draw flipped horizontally.
 
108
        flipX = false,
 
109
 
 
110
        -- Property: flipY
 
111
        -- If set to true, then the sprite will draw flipped vertically.
 
112
        flipY = false,
 
113
 
 
114
        -- Property: alpha
 
115
        -- This affects the transparency at which the sprite is drawn onscreen. 1 is fully
 
116
        -- opaque; 0 is completely transparent.
 
117
        alpha = 1,
 
118
 
 
119
        -- Property: tint
 
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
 
122
        -- { 1, 0, 0 }.
 
123
        tint = { 1, 1, 1 },
 
124
 
 
125
        -- Method: die
 
126
        -- Makes the sprite totally inert. It will not receive
 
127
        -- update events, draw anything, or be collided.
 
128
        --
 
129
        -- Arguments:
 
130
        --              none
 
131
        --
 
132
        -- Returns:
 
133
        --              nothing
 
134
 
 
135
        die = function (self)
 
136
                self.active = false
 
137
                self.visible = false
 
138
                self.solid = false
 
139
        end,
 
140
 
 
141
        -- Method: revive
 
142
        -- Makes this sprite completely active. It will receive
 
143
        -- update events, draw itself, and be collided.
 
144
        --
 
145
        -- Arguments:
 
146
        --              none
 
147
        --
 
148
        -- Returns:
 
149
        --              nothing
 
150
 
 
151
        revive = function (self)
 
152
                self.active = true
 
153
                self.visible = true
 
154
                self.solid = true
 
155
        end,
 
156
 
 
157
        -- Method: intersects
 
158
        -- Returns whether a point or rectangle intersects this sprite.
 
159
        --
 
160
        -- Arguments:
 
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
 
165
        --
 
166
        -- Returns:
 
167
        --              boolean
 
168
 
 
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
 
172
        end,
 
173
 
 
174
        -- Method: overlap
 
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.
 
178
        --
 
179
        -- Arguments:
 
180
        --              x - top left horizontal coordinate
 
181
        --              y - top left vertical coordinate
 
182
        --              width - width of the rectangle
 
183
        --              height - height of the rectangles
 
184
        --
 
185
        -- Returns:
 
186
        --              Two numbers: horizontal overlap in pixels, and vertical overlap in pixels.
 
187
 
 
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
 
193
 
 
194
                -- this is cribbed from
 
195
                -- http://frey.co.nz/old/2007/11/area-of-two-rectangles-algorithm/
 
196
 
 
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)
 
201
                else
 
202
                        return 0, 0
 
203
                end
 
204
        end,
 
205
 
 
206
        -- Method: collide
 
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.
 
210
        --
 
211
        -- Arguments:
 
212
        --              other - <Sprite> or <Group> to collide
 
213
        --
 
214
        -- Returns:
 
215
        --              boolean, whether any collision was detected
 
216
 
 
217
        collide = function (self, other)
 
218
                if not self.solid or not other.solid or self == other then return false end
 
219
 
 
220
                if other.sprites then
 
221
                        return other:collide(self)
 
222
                else
 
223
                        local xOverlap, yOverlap = self:overlap(other.x, other.y, other.width, other.height)
 
224
 
 
225
                        if xOverlap ~= 0 or yOverlap ~= 0 then  
 
226
                                if self.onCollide then
 
227
                                        self:onCollide(other, xOverlap, yOverlap)
 
228
                                end
 
229
                                
 
230
                                if other.onCollide then
 
231
                                        other:onCollide(self, xOverlap, yOverlap)
 
232
                                end
 
233
 
 
234
                                return true
 
235
                        end
 
236
                end
 
237
 
 
238
                return false
 
239
        end,
 
240
 
 
241
        -- Method: displace
 
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.
 
248
        --
 
249
        -- This does *not* cause onCollide events to occur on the sprites.
 
250
        --
 
251
        -- Arguments:
 
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
 
255
        --
 
256
        -- Returns:
 
257
        --              nothing
 
258
 
 
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
 
262
 
 
263
                local xChange = 0
 
264
                local yChange = 0
 
265
 
 
266
                if other.sprites then
 
267
                        -- handle groups
 
268
 
 
269
                        for _, spr in pairs(other.sprites) do
 
270
                                self:displace(spr, xHint, yHint)
 
271
                        end
 
272
                else
 
273
                        -- handle sprites
 
274
 
 
275
                        local xOverlap, yOverlap = self:overlap(other.x, other.y, other.width, other.height)
 
276
                        
 
277
                        -- resolve horizontal overlap
 
278
 
 
279
                        if xOverlap ~= 0 then
 
280
                                local leftMove = (other.x - self.x) + other.width
 
281
                                local rightMove = (self.x + self.width) - other.x
 
282
                                
 
283
                                if xHint == LEFT then
 
284
                                        xChange = - leftMove
 
285
                                elseif xHint == RIGHT then
 
286
                                        xChange = rightMove
 
287
                                else
 
288
                                        if leftMove < rightMove then
 
289
                                                xChange = - leftMove
 
290
                                        else
 
291
                                                xChange = rightMove
 
292
                                        end
 
293
                                end
 
294
                        end
 
295
                        
 
296
                        -- resolve vertical overlap
 
297
 
 
298
                        if yOverlap ~= 0 then
 
299
                                local upMove = (other.y - self.y) + other.height
 
300
                                local downMove = (self.y + self.height) - other.y
 
301
                                
 
302
                                if yHint == UP then
 
303
                                        yChange = - upMove
 
304
                                elseif yHint == DOWN then
 
305
                                        yChange = downMove
 
306
                                else
 
307
                                        if upMove < downMove then
 
308
                                                yChange = - upMove
 
309
                                        else
 
310
                                                yChange = downMove
 
311
                                        end
 
312
                                end
 
313
                        end
 
314
                        
 
315
                        -- choose the option that moves the other sprite the least
 
316
                        
 
317
                        if math.abs(xChange) > math.abs(yChange) then
 
318
                                other.y = other.y + yChange
 
319
                        else
 
320
                                other.x = other.x + xChange
 
321
                        end
 
322
                end
 
323
        end,
 
324
 
 
325
        -- Method: push
 
326
        -- Moves another sprite as if it had the same motion properties as this one.
 
327
        --
 
328
        -- Arguments:
 
329
        --              other - other sprite to push
 
330
        --              elapsed - elapsed time to simulate, in seconds
 
331
        --
 
332
        -- Returns:
 
333
        --              nothing
 
334
 
 
335
        push = function (self, other, elapsed)
 
336
                other.x = other.x + self.velocity.x * elapsed
 
337
                other.y = other.y + self.velocity.y * elapsed
 
338
        end,
 
339
 
 
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.
 
343
        --
 
344
        -- Arguments:
 
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.
 
347
        --
 
348
        -- Returns:
 
349
        --              distance in pixels
 
350
 
 
351
        distanceTo = function (self, ...)
 
352
                local arg = {...}
 
353
                local midX = self.x + self.width / 2
 
354
                local midY = self.y + self.width / 2
 
355
 
 
356
                if #arg == 1 then
 
357
                        local spr = arg[1]
 
358
 
 
359
                        if STRICT then
 
360
                                assert(type(spr.x) == 'number' and type(spr.y) == 'number', 'asked to calculate distance to an object without numeric x and y properties')
 
361
                        end
 
362
 
 
363
                        local sprX = spr.x + spr.width / 2
 
364
                        local sprY = spr.y + spr.height / 2
 
365
 
 
366
                        return math.sqrt((midX - sprX)^2 + (midY - sprY)^2)
 
367
                else
 
368
                        return math.sqrt((midX - arg[1])^2 + (midY - arg[2])^2)
 
369
                end
 
370
        end,
 
371
 
 
372
        startFrame = function (self, elapsed)
 
373
                if self.onStartFrame then self:onStartFrame(elapsed) end
 
374
        end,
 
375
 
 
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
 
382
 
 
383
                -- check existence of properties
 
384
 
 
385
                if STRICT then
 
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')
 
391
                end
 
392
 
 
393
                vel.x = vel.x or 0
 
394
                vel.y = vel.y or 0
 
395
                vel.rotation = vel.rotation or 0
 
396
 
 
397
                -- physics
 
398
                        
 
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
 
402
                
 
403
                if acc.x and acc.x ~= 0 then
 
404
                        vel.x = vel.x + acc.x * elapsed
 
405
                else
 
406
                        if drag.x then
 
407
                                if vel.x > 0 then
 
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
 
413
                                end
 
414
                        end
 
415
                end
 
416
                
 
417
                if acc.y and acc.y ~= 0 then
 
418
                        vel.y = vel.y + acc.y * elapsed
 
419
                else
 
420
                        if drag.y then
 
421
                                if vel.y > 0 then
 
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
 
427
                                end
 
428
                        end
 
429
                end
 
430
                
 
431
                if acc.rotation and acc.rotation ~= 0 then
 
432
                        vel.rotation = vel.rotation + acc.rotation * elapsed
 
433
                else
 
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
 
441
                                end
 
442
                        end
 
443
                end
 
444
 
 
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
 
451
                
 
452
                if self.onUpdate then self:onUpdate(elapsed) end
 
453
        end,
 
454
 
 
455
        endFrame = function (self, elapsed)
 
456
                if self.onEndFrame then self:onEndFrame(elapsed) end
 
457
        end,
 
458
 
 
459
        draw = function (self, x, y)
 
460
                if self.onDraw then self:onDraw(x, y) end
 
461
        end
 
462
}