/spacey

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

« back to all changes in this revision

Viewing changes to zoetrope/sprites/map.lua

  • Committer: Josh C
  • Date: 2013-05-04 20:45:17 UTC
  • Revision ID: josh@9ix.org-20130504204517-1rfp92svud12kg42
zoetrope 1.4

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 * self.spriteWidth
 
96
                self.height = #self.map[1] * 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: getMapSize
 
142
        -- Returns the size of the map in map coordinates.
 
143
        --
 
144
        -- Arguments:
 
145
        --              none
 
146
        --
 
147
        -- Returns:
 
148
        --              width and height in integers
 
149
 
 
150
        getMapSize = function (self)
 
151
                if #self.map == 0 then
 
152
                        return 0, 0
 
153
                else
 
154
                        return #self.map, #self.map[1]
 
155
                end
 
156
        end,
 
157
 
 
158
        -- Method: pixelToMap
 
159
        -- Converts pixels to map coordinates.
 
160
        --
 
161
        -- Arguments:
 
162
        --              x - x coordinate in pixels
 
163
        --              y - y coordinate in pixels
 
164
        --              clamp - clamp to map bounds? defaults to true
 
165
        --
 
166
        -- Returns:
 
167
        --              x, y map coordinates
 
168
 
 
169
        pixelToMap = function (self, x, y, clamp)
 
170
                if type(clamp) == 'nil' then clamp = true end
 
171
 
 
172
                -- remember, Lua tables start at index 1
 
173
 
 
174
                local mapX = math.floor(x / self.spriteWidth) + 1
 
175
                local mapY = math.floor(y / self.spriteHeight) + 1
 
176
                
 
177
                -- clamp to map bounds
 
178
                
 
179
                if clamp then
 
180
                        if mapX < 1 then mapX = 1 end
 
181
                        if mapY < 1 then mapY = 1 end
 
182
                        if mapX > #self.map then mapX = #self.map end
 
183
                        if mapY > #self.map[1] then mapY = #self.map[1] end
 
184
                end
 
185
 
 
186
                return mapX, mapY
 
187
        end,
 
188
 
 
189
        -- Method: spriteAtMap
 
190
        -- Returns the sprite at a given set of map coordinates, with
 
191
        -- the correct pixel position for that sprite. Remember that
 
192
        -- sprites in maps are shared, so any changes you make to one
 
193
        -- sprite will carry over to all instances of that sprite in the map.
 
194
        --
 
195
        -- Arguments:
 
196
        --              x - x coordinate in map units
 
197
        --              y - y coordinate in map units
 
198
        --
 
199
        -- Returns:
 
200
        --              <Sprite> instance. If no sprite is present at these
 
201
        --              coordinates, the method returns nil.
 
202
 
 
203
        spriteAtMap = function (self, x, y)
 
204
                if self.map[x] and self.map[x][y] then
 
205
                        local spr = self.sprites[self.map[x][y]]
 
206
 
 
207
                        if spr then 
 
208
                                spr.x = (x - 1) * self.spriteWidth
 
209
                                spr.y = (y - 1) * self.spriteHeight
 
210
                                return spr
 
211
                        end
 
212
                elseif STRICT then
 
213
                        print('Warning: asked for map sprite at ' .. x .. ', ' .. y ' but map isn\'t that big')
 
214
                end
 
215
        end,
 
216
 
 
217
        -- Method: spriteAtPixel
 
218
        -- Returns the sprite at a given set of pixel coordinates, with
 
219
        -- the correct pixel position for that sprite. Remember that
 
220
        -- sprites in maps are shared, so any changes you make to one
 
221
        -- sprite will carry over to all instances of that sprite in the map.
 
222
        --
 
223
        -- Arguments:
 
224
        --              x - x coordinate in pixels
 
225
        --              y - y coordinate in pixels
 
226
        --
 
227
        -- Returns:
 
228
        --              <Sprite> instance. If no sprite is present at these
 
229
        --              coordinates, the method returns nil.
 
230
 
 
231
        spriteAtPixel = function (self, x, y)
 
232
                return self:spriteAtMap((x - 1) * self.spriteWidth, (y - 1) * self.spriteHeight)
 
233
        end,
 
234
 
 
235
        -- This overrides a method in <Sprite>, passing along
 
236
        -- collidedWith() calls to all sprites in the map touching a
 
237
        -- sprite.
 
238
        --
 
239
        -- Arguments:
 
240
        --              other - other <Sprite>
 
241
        --
 
242
        -- Returns:
 
243
        --              nothing
 
244
 
 
245
        collidedWith = function (self, other)
 
246
                local spriteWidth = self.spriteWidth
 
247
                local spriteHeight = self.spriteHeight
 
248
                local startX, startY = self:pixelToMap(other.x - self.x, other.y - self.y)
 
249
                local endX, endY = self:pixelToMap(other.x + other.width - self.x,
 
250
                                                                                   other.y + other.height - self.y)
 
251
 
 
252
                -- collect collisions against sprites
 
253
 
 
254
                local collisions = {}
 
255
                
 
256
                for x = startX, endX do 
 
257
                        for y = startY, endY do
 
258
                                local spr = self.sprites[self.map[x][y]]
 
259
                                
 
260
                                if spr and spr.solid then
 
261
                                        local sprX = self.x + (x - 1) * spriteWidth
 
262
                                        local sprY = self.y + (y - 1) * spriteHeight
 
263
                                        
 
264
                                        local xOverlap, yOverlap = other:overlap(sprX, sprY, spriteWidth, spriteHeight)
 
265
 
 
266
                                        if xOverlap ~= 0 or yOverlap ~= 0 then
 
267
                                                table.insert(collisions, { area = xOverlap * yOverlap, x = xOverlap, y = yOverlap,
 
268
                                                                                                   a = spr, ax = sprX, ay = sprY })
 
269
                                        end
 
270
                                end
 
271
                        end
 
272
                end
 
273
 
 
274
                -- sort as usual and pass off collidedWith() calls
 
275
 
 
276
                table.sort(collisions, Collision.sortCollisions)
 
277
 
 
278
                for _, col in ipairs(collisions) do
 
279
                        col.a.x, col.a.y = col.ax, col.ay
 
280
                        col.a:collidedWith(other, col.x, col.y)
 
281
                        other:collidedWith(col.a, col.x, col.y)
 
282
                end
 
283
        end,
 
284
 
 
285
        -- this is here mainly for completeness; it's better to specify displacement
 
286
        -- in individual map sprites
 
287
 
 
288
        displace = function (self, other, xHint, yHint) 
 
289
                if not self.solid or self == other or not other.solid then return end
 
290
                if STRICT then assert(other:instanceOf(Sprite), 'asked to displace a non-sprite') end
 
291
 
 
292
                local spriteWidth = self.spriteWidth
 
293
                local spriteHeight = self.spriteHeight
 
294
                local startX, startY = self:pixelToMap(other.x - self.x, other.y - self.y)
 
295
                local endX, endY = self:pixelToMap(other.x + other.width - self.x,
 
296
                                                                                   other.y + other.height - self.y)
 
297
 
 
298
                -- collect collisions against sprites
 
299
 
 
300
                local collisions = {}
 
301
                
 
302
                for x = startX, endX do 
 
303
                        for y = startY, endY do
 
304
                                local spr = self.sprites[self.map[x][y]]
 
305
                                
 
306
                                if spr and spr.solid then
 
307
                                        local sprX = self.x + (x - 1) * spriteWidth
 
308
                                        local sprY = self.y + (y - 1) * spriteHeight
 
309
                                        
 
310
                                        local xOverlap, yOverlap = other:overlap(sprX, sprY, spriteWidth, spriteHeight)
 
311
 
 
312
                                        if xOverlap ~= 0 or yOverlap ~= 0 then
 
313
                                                table.insert(collisions, { area = xOverlap * yOverlap, x = xOverlap, y = yOverlap,
 
314
                                                                                                   a = spr, ax = sprX, ay = sprY })
 
315
                                        end
 
316
                                end
 
317
                        end
 
318
                end
 
319
 
 
320
                -- sort as usual and displace
 
321
 
 
322
                table.sort(collisions, Collision.sortCollisions)
 
323
 
 
324
                for _, col in ipairs(collisions) do
 
325
                        col.a.x, col.a.y = col.ax, col.ay
 
326
                        col.a:displace(other)
 
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
        end,
 
372
 
 
373
        -- makes sure all sprites receive startFrame messages
 
374
 
 
375
        startFrame = function (self, elapsed)
 
376
                for _, spr in pairs(self.sprites) do
 
377
                        spr:startFrame(elapsed)
 
378
                end
 
379
 
 
380
                Sprite.startFrame(self, elapsed)
 
381
        end,
 
382
 
 
383
        -- makes sure all sprites receive update messages
 
384
 
 
385
        update = function (self, elapsed)
 
386
                for _, spr in pairs(self.sprites) do
 
387
                        spr:update(elapsed)
 
388
                end
 
389
 
 
390
                Sprite.update(self, elapsed)
 
391
        end,
 
392
 
 
393
        -- makes sure all sprites receive endFrame messages
 
394
 
 
395
        endFrame = function (self, elapsed)
 
396
                for _, spr in pairs(self.sprites) do
 
397
                        spr:endFrame(elapsed)
 
398
                end
 
399
 
 
400
                Sprite.endFrame(self, elapsed)
 
401
        end,
 
402
 
 
403
        __tostring = function (self)
 
404
                local result = 'Map (x: ' .. self.x .. ', y: ' .. self.y ..
 
405
                                           ', w: ' .. self.width .. ', h: ' .. self.height .. ', '
 
406
 
 
407
                if self.active then
 
408
                        result = result .. 'active, '
 
409
                else
 
410
                        result = result .. 'inactive, '
 
411
                end
 
412
 
 
413
                if self.visible then
 
414
                        result = result .. 'visible, '
 
415
                else
 
416
                        result = result .. 'invisible, '
 
417
                end
 
418
 
 
419
                if self.solid then
 
420
                        result = result .. 'solid'
 
421
                else
 
422
                        result = result .. 'not solid'
 
423
                end
 
424
 
 
425
                return result .. ')'
 
426
        end
 
427
}