/zoeplat

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

« back to all changes in this revision

Viewing changes to zoetrope/sprites/map.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: Map
 
2
-- A map saves memory and CPU time by acting as if it were a grid of sprites.
 
3
-- Each different type of sprite in the grid is represented via a single
 
4
-- object. Each sprite must have the same size, however.
 
5
-- 
 
6
-- This works very similarly to a tilemap, but there is additional flexibility
 
7
-- in using a sprite, e.g. animation and other display effects. (If you want it
 
8
-- to act like a tilemap, use its loadTiles method.) However, changing a sprite's
 
9
-- x or y position has no effect. Changing the scale will have weird effects as
 
10
-- a map expects every sprite to be the same size.
 
11
--
 
12
-- Extends:
 
13
--              <Sprite>
 
14
 
 
15
Map = Sprite:extend
 
16
{
 
17
        -- Constant: NO_SPRITE
 
18
        -- Represents a map entry with no sprite.
 
19
        NO_SPRITE = -1,
 
20
 
 
21
        -- Property: sprites
 
22
        -- An ordered table of <Sprite> objects to be used in conjunction with the map property.
 
23
        sprites = {},
 
24
 
 
25
        -- Property: map
 
26
        -- A two-dimensional table of values, each corresponding to an entry in the sprites property.
 
27
        -- nb. The tile at (0, 0) visually is stored in [1, 1].
 
28
        map = {},
 
29
 
 
30
        -- Method: empty
 
31
        -- Creates an empty map.
 
32
        --
 
33
        -- Arguments:
 
34
        --              width - width of the map in sprites
 
35
        --              height - height of the map in sprites
 
36
        -- 
 
37
        -- Returns:
 
38
        --              self, for chaining
 
39
 
 
40
        empty = function (self, width, height)
 
41
                local x, y
 
42
                
 
43
                -- empty the map
 
44
 
 
45
                for x = 1, width do
 
46
                        self.map[x] = {}
 
47
                        
 
48
                        for y = 1, height do
 
49
                                self.map[x][y] = Map.NO_SPRITE
 
50
                        end
 
51
                end
 
52
                
 
53
                -- set bounds
 
54
                
 
55
                self.width = width * self.spriteWidth
 
56
                self.height = height * self.spriteHeight
 
57
                
 
58
                return self
 
59
        end,
 
60
 
 
61
        -- Method: loadMap
 
62
        -- Loads map data from a file, typically comma-separated values.
 
63
        -- Each entry corresponds to an index in self.sprites, and all rows
 
64
        -- must have the same number of columns.
 
65
        --
 
66
        -- Arguments:
 
67
        --              file - filename of source text to use
 
68
        --              colSeparator - character to use as separator of columns, default ','
 
69
        --              rowSeparator - character to use as separator of rows, default newline
 
70
        --
 
71
        -- Returns:
 
72
        --              self, for chaining
 
73
 
 
74
        loadMap = function (self, file, colSeparator, rowSeparator)
 
75
                colSeparator = colSeparator or ','
 
76
                rowSeparator = rowSeparator or '\n'
 
77
                
 
78
                -- load data
 
79
                
 
80
                local x, y
 
81
                local source = Cached:text(file)
 
82
                local rows = split(source, rowSeparator)
 
83
                
 
84
                for y = 1, #rows do
 
85
                        local cols = split(rows[y], colSeparator)
 
86
                        
 
87
                        for x = 1, #cols do
 
88
                                if not self.map[x] then self.map[x] = {} end
 
89
                                self.map[x][y] = tonumber(cols[x])
 
90
                        end
 
91
                end
 
92
                
 
93
                -- set bounds
 
94
                
 
95
                self.width = #self.map[1] * self.spriteWidth
 
96
                self.height = #self.map * self.spriteHeight
 
97
                
 
98
                return self
 
99
        end,
 
100
 
 
101
        -- Method: loadTiles
 
102
        --- Loads the sprites group with slices of a source image.
 
103
        --  By default, this uses the Tile class for sprites, but you
 
104
        --  may pass as replacement class.
 
105
        --
 
106
        --  Arguments:
 
107
        --              image - source image to use for tiles
 
108
        --              class - class to create objects with; constructor
 
109
        --                                will be called with properties: image, width,
 
110
        --                                height, imageOffset (with x and y sub-properties)
 
111
        --              startIndex - starting index of tiles in self.sprites, default 0
 
112
        --
 
113
        --  Returns:
 
114
        --              self, for chaining
 
115
 
 
116
        loadTiles = function (self, image, class, startIndex)
 
117
                assert(self.spriteWidth and self.spriteHeight, 'sprite size must be set before loading tiles')
 
118
                if type(startIndex) ~= 'number' then startIndex = 0 end
 
119
                
 
120
                class = class or Tile
 
121
                self.sprites = self.sprites or {}
 
122
                
 
123
                local imageObj = Cached:image(image)
 
124
                local imageWidth = imageObj:getWidth()
 
125
                local imageHeight = imageObj:getHeight()
 
126
                 
 
127
                local i = startIndex
 
128
                
 
129
                for y = 0, imageHeight - self.spriteHeight, self.spriteHeight do
 
130
                        for x = 0, imageWidth - self.spriteWidth, self.spriteWidth do
 
131
                                self.sprites[i] = class:new{ image = image, width = self.spriteWidth,
 
132
                                                                                          height = self.spriteHeight,
 
133
                                                                                          imageOffset = { x = x, y = y }}
 
134
                                i = i + 1
 
135
                        end
 
136
                end
 
137
                
 
138
                return self
 
139
        end,
 
140
 
 
141
        -- Method: subcollide
 
142
        -- This acts as a wrapper to multiple collide() calls, as if
 
143
        -- there really were all the sprites in their particular positions.
 
144
        -- This is much more useful than Map:collide(), which simply checks
 
145
        -- if a sprite or group is touching the map at all. 
 
146
        --
 
147
        -- Arguments:
 
148
        --              other - other <Sprite> or <Group>
 
149
        --
 
150
        -- Returns:
 
151
        --              boolean, whether any collision was detected
 
152
 
 
153
        subcollide = function (self, other)
 
154
                local hit = false
 
155
                local others
 
156
 
 
157
                if other.sprites then
 
158
                        others = other.sprites
 
159
                else
 
160
                        others = { other }
 
161
                end
 
162
 
 
163
                for _, othSpr in pairs(others) do
 
164
                        if othSpr.solid then
 
165
                                if othSpr.sprites then
 
166
                                        -- recurse into subgroups
 
167
                                        -- order is important here to avoid short-circuiting inappopriately
 
168
                                
 
169
                                        hit = self:subcollide(othSpr.sprites) or hit
 
170
                                else
 
171
                                        local startX, startY = self:pixelToMap(othSpr.x - self.x, othSpr.y - self.y)
 
172
                                        local endX, endY = self:pixelToMap(othSpr.x + othSpr.width - self.x,
 
173
                                                                                                           othSpr.y + othSpr.height - self.y)
 
174
                                        local x, y
 
175
                                        
 
176
                                        for x = startX, endX do
 
177
                                                for y = startY, endY do
 
178
                                                        local spr = self.sprites[self.map[x][y]]
 
179
                                                        
 
180
                                                        if spr and spr.solid then
 
181
                                                                -- position our map sprite as if it were onscreen
 
182
                                                                
 
183
                                                                spr.x = self.x + (x - 1) * self.spriteWidth
 
184
                                                                spr.y = self.y + (y - 1) * self.spriteHeight
 
185
                                                                
 
186
                                                                hit = spr:collide(othSpr) or hit
 
187
                                                        end
 
188
                                                end
 
189
                                        end
 
190
                                end
 
191
                        end
 
192
                end
 
193
 
 
194
                return hit
 
195
        end,
 
196
 
 
197
        -- Method: subdisplace
 
198
        -- This acts as a wrapper to multiple displace() calls, as if
 
199
        -- there really were all the sprites in their particular positions.
 
200
        -- This is much more useful than Map:displace(), which pushes a sprite or group
 
201
        -- so that it does not touch the map in its entirety. 
 
202
        --
 
203
        -- Arguments:
 
204
        --              other - other <Sprite> or <Group> to displace
 
205
        --              xHint - force horizontal displacement in one direction, uses direction constants
 
206
        --              yHint - force vertical displacement in one direction, uses direction constants
 
207
        --
 
208
        -- Returns:
 
209
        --              nothing
 
210
 
 
211
        subdisplace = function (self, other, xHint, yHint)      
 
212
                local others
 
213
 
 
214
                if other.sprites then
 
215
                        others = other.sprites
 
216
                else
 
217
                        others = { other }
 
218
                end
 
219
 
 
220
                for _, othSpr in pairs(others) do
 
221
                        if othSpr.solid then
 
222
                                if othSpr.sprites then
 
223
                                        -- recurse into subgroups
 
224
                                        -- order is important here to avoid short-circuiting inappopriately
 
225
                                
 
226
                                        self:subdisplace(othSpr.sprites)
 
227
                                else
 
228
                                        -- determine sprites we might intersect with
 
229
 
 
230
                                        local startX, startY = self:pixelToMap(othSpr.x - self.x, othSpr.y - self.y)
 
231
                                        local endX, endY = self:pixelToMap(othSpr.x + othSpr.width - self.x,
 
232
                                                                                                           othSpr.y + othSpr.height - self.y)
 
233
                                        local hit = true
 
234
                                        local loops = 0
 
235
 
 
236
                                        -- We displace the target sprite along the axis that would satisfy the
 
237
                                        -- most map sprites, but at the minimum distance for all of them.
 
238
                                        -- xVotes and yVotes track which axis should be used; this is a
 
239
                                        -- proportional vote, with sprites that have large amounts of overlap
 
240
                                        -- getting more of a chance to overrule the others. We run this loop
 
241
                                        -- repeatedly to make sure we end up with the target sprite not overlapping
 
242
                                        -- anything in the map.
 
243
                                        --
 
244
                                        -- This is based on the technique described at:
 
245
                                        -- http://go.colorize.net/xna/2d_collision_response_xna/
 
246
 
 
247
                                        while hit and loops < 3 do
 
248
                                                hit = false
 
249
                                                loops = loops + 1
 
250
 
 
251
                                                local xVotes, yVotes = 0, 0
 
252
                                                local minChangeX, minChangeY, absMinChangeX, absMinChangeY
 
253
                                                local origX, origY = othSpr.x, othSpr.y
 
254
 
 
255
                                                for x = startX, endX do
 
256
                                                        for y = startY, endY do
 
257
                                                                local spr = self.sprites[self.map[x][y]]
 
258
                                                                
 
259
                                                                if spr and spr.solid then
 
260
                                                                        -- position our map sprite as if it were onscreen
 
261
                                                                        
 
262
                                                                        spr.x = self.x + (x - 1) * self.spriteWidth
 
263
                                                                        spr.y = self.y + (y - 1) * self.spriteHeight
 
264
                                        
 
265
                                                                        -- displace and check to see if this displacement
 
266
                                                                        -- would result in a smaller shift than any so far
 
267
 
 
268
                                                                        spr:displace(othSpr)
 
269
                                                                        local xChange = othSpr.x - origX
 
270
                                                                        local yChange = othSpr.y - origY
 
271
 
 
272
                                                                        if xChange ~= 0 then
 
273
                                                                                xVotes = xVotes + math.abs(xChange)
 
274
                                                                                hit = true
 
275
 
 
276
                                                                                if not minChangeX or math.abs(xChange) < absMinChangeX then
 
277
                                                                                        minChangeX = xChange
 
278
                                                                                        absMinChangeX = math.abs(xChange)
 
279
                                                                                end
 
280
                                                                        end
 
281
 
 
282
                                                                        if yChange ~= 0 then
 
283
                                                                                yVotes = yVotes + math.abs(yChange)
 
284
                                                                                hit = true
 
285
 
 
286
                                                                                if not minChangeY or math.abs(yChange) < absMinChangeY then
 
287
                                                                                        minChangeY = yChange
 
288
                                                                                        absMinChangeY = math.abs(yChange)
 
289
                                                                                end
 
290
                                                                        end
 
291
 
 
292
                                                                        -- restore sprite to original position
 
293
 
 
294
                                                                        othSpr.x = origX
 
295
                                                                        othSpr.y = origY
 
296
                                                                end
 
297
                                                        end
 
298
                                                end
 
299
 
 
300
                                                if hit then
 
301
                                                        if xVotes > 0 and xVotes > yVotes then
 
302
                                                                othSpr.x = othSpr.x + minChangeX
 
303
                                                        elseif yVotes > 0 then
 
304
                                                                othSpr.y = othSpr.y + minChangeY
 
305
                                                        end
 
306
                                                end
 
307
                                        end
 
308
                                end
 
309
                        end
 
310
                end
 
311
        end,
 
312
 
 
313
        -- Method: getMapSize
 
314
        -- Returns the size of the map in sprites.
 
315
        --
 
316
        -- Arguments:
 
317
        --              none
 
318
        --
 
319
        -- Returns:
 
320
        --              width and height in integers
 
321
 
 
322
        getMapSize = function (self)
 
323
                if #self.map == 0 then
 
324
                        return 0, 0
 
325
                else
 
326
                        return #self.map, #self.map[1]
 
327
                end
 
328
        end,
 
329
 
 
330
        draw = function (self, x, y)
 
331
                -- lock our x/y coordinates to integers
 
332
                -- to avoid gaps in the tiles
 
333
        
 
334
                x = math.floor(x or self.x)
 
335
                y = math.floor(y or self.y)
 
336
                if not self.visible or self.alpha <= 0 then return end
 
337
                if not self.spriteWidth or not self.spriteHeight then return end
 
338
                
 
339
                -- determine drawing bounds
 
340
                -- we draw to fill the entire app windoow
 
341
                
 
342
                local startX, startY = self:pixelToMap(-x, -y)
 
343
                local endX, endY = self:pixelToMap(the.app.width - x, the.app.height - y)
 
344
                
 
345
                -- queue each sprite drawing operation
 
346
                
 
347
                local toDraw = {}
 
348
                
 
349
                for drawY = startY, endY do
 
350
                        for drawX = startX, endX do
 
351
                                local sprite = self.sprites[self.map[drawX][drawY]]
 
352
                                
 
353
                                if sprite and sprite.visible then
 
354
                                        if not toDraw[sprite] then
 
355
                                                toDraw[sprite] = {}
 
356
                                        end
 
357
                                        
 
358
                                        table.insert(toDraw[sprite], { x + (drawX - 1) * self.spriteWidth,
 
359
                                                                                                   y + (drawY - 1) * self.spriteHeight })
 
360
                                end
 
361
                        end
 
362
                end
 
363
                
 
364
                -- draw each sprite in turn
 
365
                
 
366
                for sprite, list in pairs(toDraw) do
 
367
                        for _, coords in pairs(list) do
 
368
                                sprite:draw(coords[1], coords[2])
 
369
                        end
 
370
                end
 
371
                
 
372
                Sprite.draw(self)
 
373
        end,
 
374
 
 
375
        -- Method: pixelToMap
 
376
        -- Converts pixels to map coordinates.
 
377
        --
 
378
        -- Arguments:
 
379
        --              x - x coordinate in pixels
 
380
        --              y - y coordinate in pixels
 
381
        --              clamp - clamp to map bounds? defaults to true
 
382
        --
 
383
        -- Returns:
 
384
        --              x, y map coordinates
 
385
 
 
386
        pixelToMap = function (self, x, y, clamp)
 
387
                if type(clamp) == 'nil' then clamp = true end
 
388
 
 
389
                -- remember, Lua tables start at index 1
 
390
 
 
391
                local mapX = math.floor(x / self.spriteWidth) + 1
 
392
                local mapY = math.floor(y / self.spriteHeight) + 1
 
393
                
 
394
                -- clamp to map bounds
 
395
                
 
396
                if clamp then
 
397
                        if mapX < 1 then mapX = 1 end
 
398
                        if mapY < 1 then mapY = 1 end
 
399
                        if mapX > #self.map then mapX = #self.map end
 
400
                        if mapY > #self.map[1] then mapY = #self.map[1] end
 
401
                end
 
402
 
 
403
                return mapX, mapY
 
404
        end,
 
405
 
 
406
        -- makes sure all sprites receive startFrame messages
 
407
 
 
408
        startFrame = function (self, elapsed)
 
409
                for _, spr in pairs(self.sprites) do
 
410
                        spr:startFrame(elapsed)
 
411
                end
 
412
 
 
413
                Sprite.startFrame(self, elapsed)
 
414
        end,
 
415
 
 
416
        -- makes sure all sprites receive update messages
 
417
 
 
418
        update = function (self, elapsed)
 
419
                for _, spr in pairs(self.sprites) do
 
420
                        spr:update(elapsed)
 
421
                end
 
422
 
 
423
                Sprite.update(self, elapsed)
 
424
        end,
 
425
 
 
426
        -- makes sure all sprites receive endFrame messages
 
427
 
 
428
        endFrame = function (self, elapsed)
 
429
                for _, spr in pairs(self.sprites) do
 
430
                        spr:endFrame(elapsed)
 
431
                end
 
432
 
 
433
                Sprite.endFrame(self, elapsed)
 
434
        end,
 
435
 
 
436
        __tostring = function (self)
 
437
                local result = 'Map (x: ' .. self.x .. ', y: ' .. self.y ..
 
438
                                           ', w: ' .. self.width .. ', h: ' .. self.height .. ', '
 
439
 
 
440
                if self.active then
 
441
                        result = result .. 'active, '
 
442
                else
 
443
                        result = result .. 'inactive, '
 
444
                end
 
445
 
 
446
                if self.visible then
 
447
                        result = result .. 'visible, '
 
448
                else
 
449
                        result = result .. 'invisible, '
 
450
                end
 
451
 
 
452
                if self.solid then
 
453
                        result = result .. 'solid'
 
454
                else
 
455
                        result = result .. 'not solid'
 
456
                end
 
457
 
 
458
                return result .. ')'
 
459
        end
 
460
}