/ld27

To get this branch, use:
bzr branch http://9ix.org/bzr/ld27

« back to all changes in this revision

Viewing changes to zoetrope/core/sprite.lua

  • Committer: Josh C
  • Date: 2013-08-24 03:11:38 UTC
  • Revision ID: josh@9ix.org-20130824031138-j4ta4ome0upsxcv5
zoetrope 1.4

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
}