/zoeplat

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

« back to all changes in this revision

Viewing changes to zoetrope/sprites/map.lua

  • Committer: Josh C
  • Date: 2013-03-16 19:31:02 UTC
  • Revision ID: josh@9ix.org-20130316193102-68imraus0srbj653
don't go off the edge

Show diffs side-by-side

added added

removed removed

Lines of Context:
92
92
                
93
93
                -- set bounds
94
94
                
95
 
                self.width = #self.map[1] * self.spriteWidth
96
 
                self.height = #self.map * self.spriteHeight
 
95
                self.width = #self.map * self.spriteWidth
 
96
                self.height = #self.map[1] * self.spriteHeight
97
97
                
98
98
                return self
99
99
        end,
138
138
                return self
139
139
        end,
140
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
141
        -- Method: getMapSize
314
 
        -- Returns the size of the map in sprites.
 
142
        -- Returns the size of the map in map coordinates.
315
143
        --
316
144
        -- Arguments:
317
145
        --              none
327
155
                end
328
156
        end,
329
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
330
        draw = function (self, x, y)
331
331
                -- lock our x/y coordinates to integers
332
332
                -- to avoid gaps in the tiles
368
368
                                sprite:draw(coords[1], coords[2])
369
369
                        end
370
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
371
        end,
405
372
 
406
373
        -- makes sure all sprites receive startFrame messages