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.
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.
17
-- Constant: NO_SPRITE
18
-- Represents a map entry with no sprite.
22
-- An ordered table of <Sprite> objects to be used in conjunction with the map property.
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].
31
-- Creates an empty map.
34
-- width - width of the map in sprites
35
-- height - height of the map in sprites
40
empty = function (self, width, height)
49
self.map[x][y] = Map.NO_SPRITE
55
self.width = width * self.spriteWidth
56
self.height = height * self.spriteHeight
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.
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
74
loadMap = function (self, file, colSeparator, rowSeparator)
75
colSeparator = colSeparator or ','
76
rowSeparator = rowSeparator or '\n'
81
local source = Cached:text(file)
82
local rows = split(source, rowSeparator)
85
local cols = split(rows[y], colSeparator)
88
if not self.map[x] then self.map[x] = {} end
89
self.map[x][y] = tonumber(cols[x])
95
self.width = #self.map[1] * self.spriteWidth
96
self.height = #self.map * self.spriteHeight
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.
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
114
-- self, for chaining
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
120
class = class or Tile
121
self.sprites = self.sprites or {}
123
local imageObj = Cached:image(image)
124
local imageWidth = imageObj:getWidth()
125
local imageHeight = imageObj:getHeight()
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 }}
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.
148
-- other - other <Sprite> or <Group>
151
-- boolean, whether any collision was detected
153
subcollide = function (self, other)
157
if other.sprites then
158
others = other.sprites
163
for _, othSpr in pairs(others) do
165
if othSpr.sprites then
166
-- recurse into subgroups
167
-- order is important here to avoid short-circuiting inappopriately
169
hit = self:subcollide(othSpr.sprites) or hit
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)
176
for x = startX, endX do
177
for y = startY, endY do
178
local spr = self.sprites[self.map[x][y]]
180
if spr and spr.solid then
181
-- position our map sprite as if it were onscreen
183
spr.x = self.x + (x - 1) * self.spriteWidth
184
spr.y = self.y + (y - 1) * self.spriteHeight
186
hit = spr:collide(othSpr) or hit
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.
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
211
subdisplace = function (self, other, xHint, yHint)
214
if other.sprites then
215
others = other.sprites
220
for _, othSpr in pairs(others) do
222
if othSpr.sprites then
223
-- recurse into subgroups
224
-- order is important here to avoid short-circuiting inappopriately
226
self:subdisplace(othSpr.sprites)
228
-- determine sprites we might intersect with
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)
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.
244
-- This is based on the technique described at:
245
-- http://go.colorize.net/xna/2d_collision_response_xna/
247
while hit and loops < 3 do
251
local xVotes, yVotes = 0, 0
252
local minChangeX, minChangeY, absMinChangeX, absMinChangeY
253
local origX, origY = othSpr.x, othSpr.y
255
for x = startX, endX do
256
for y = startY, endY do
257
local spr = self.sprites[self.map[x][y]]
259
if spr and spr.solid then
260
-- position our map sprite as if it were onscreen
262
spr.x = self.x + (x - 1) * self.spriteWidth
263
spr.y = self.y + (y - 1) * self.spriteHeight
265
-- displace and check to see if this displacement
266
-- would result in a smaller shift than any so far
269
local xChange = othSpr.x - origX
270
local yChange = othSpr.y - origY
273
xVotes = xVotes + math.abs(xChange)
276
if not minChangeX or math.abs(xChange) < absMinChangeX then
278
absMinChangeX = math.abs(xChange)
283
yVotes = yVotes + math.abs(yChange)
286
if not minChangeY or math.abs(yChange) < absMinChangeY then
288
absMinChangeY = math.abs(yChange)
292
-- restore sprite to original position
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
313
-- Method: getMapSize
314
-- Returns the size of the map in sprites.
320
-- width and height in integers
322
getMapSize = function (self)
323
if #self.map == 0 then
326
return #self.map, #self.map[1]
330
draw = function (self, x, y)
331
-- lock our x/y coordinates to integers
332
-- to avoid gaps in the tiles
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
339
-- determine drawing bounds
340
-- we draw to fill the entire app windoow
342
local startX, startY = self:pixelToMap(-x, -y)
343
local endX, endY = self:pixelToMap(the.app.width - x, the.app.height - y)
345
-- queue each sprite drawing operation
349
for drawY = startY, endY do
350
for drawX = startX, endX do
351
local sprite = self.sprites[self.map[drawX][drawY]]
353
if sprite and sprite.visible then
354
if not toDraw[sprite] then
358
table.insert(toDraw[sprite], { x + (drawX - 1) * self.spriteWidth,
359
y + (drawY - 1) * self.spriteHeight })
364
-- draw each sprite in turn
366
for sprite, list in pairs(toDraw) do
367
for _, coords in pairs(list) do
368
sprite:draw(coords[1], coords[2])
375
-- Method: pixelToMap
376
-- Converts pixels to map coordinates.
379
-- x - x coordinate in pixels
380
-- y - y coordinate in pixels
381
-- clamp - clamp to map bounds? defaults to true
384
-- x, y map coordinates
386
pixelToMap = function (self, x, y, clamp)
387
if type(clamp) == 'nil' then clamp = true end
389
-- remember, Lua tables start at index 1
391
local mapX = math.floor(x / self.spriteWidth) + 1
392
local mapY = math.floor(y / self.spriteHeight) + 1
394
-- clamp to map bounds
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
406
-- makes sure all sprites receive startFrame messages
408
startFrame = function (self, elapsed)
409
for _, spr in pairs(self.sprites) do
410
spr:startFrame(elapsed)
413
Sprite.startFrame(self, elapsed)
416
-- makes sure all sprites receive update messages
418
update = function (self, elapsed)
419
for _, spr in pairs(self.sprites) do
423
Sprite.update(self, elapsed)
426
-- makes sure all sprites receive endFrame messages
428
endFrame = function (self, elapsed)
429
for _, spr in pairs(self.sprites) do
430
spr:endFrame(elapsed)
433
Sprite.endFrame(self, elapsed)
436
__tostring = function (self)
437
local result = 'Map (x: ' .. self.x .. ', y: ' .. self.y ..
438
', w: ' .. self.width .. ', h: ' .. self.height .. ', '
441
result = result .. 'active, '
443
result = result .. 'inactive, '
447
result = result .. 'visible, '
449
result = result .. 'invisible, '
453
result = result .. 'solid'
455
result = result .. 'not solid'