/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-05 00:14:58 UTC
  • Revision ID: josh@9ix.org-20130305001458-206jxv29dgeenj3n
jump

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 * self.spriteWidth
96
 
                self.height = #self.map[1] * self.spriteHeight
 
95
                self.width = #self.map[1] * self.spriteWidth
 
96
                self.height = #self.map * 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
 
141
313
        -- Method: getMapSize
142
 
        -- Returns the size of the map in map coordinates.
 
314
        -- Returns the size of the map in sprites.
143
315
        --
144
316
        -- Arguments:
145
317
        --              none
155
327
                end
156
328
        end,
157
329
 
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
371
404
        end,
372
405
 
373
406
        -- makes sure all sprites receive startFrame messages