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 * self.spriteWidth
96
self.height = #self.map[1] * 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: getMapSize
142
-- Returns the size of the map in map coordinates.
148
-- width and height in integers
150
getMapSize = function (self)
151
if #self.map == 0 then
154
return #self.map, #self.map[1]
158
-- Method: pixelToMap
159
-- Converts pixels to map coordinates.
162
-- x - x coordinate in pixels
163
-- y - y coordinate in pixels
164
-- clamp - clamp to map bounds? defaults to true
167
-- x, y map coordinates
169
pixelToMap = function (self, x, y, clamp)
170
if type(clamp) == 'nil' then clamp = true end
172
-- remember, Lua tables start at index 1
174
local mapX = math.floor(x / self.spriteWidth) + 1
175
local mapY = math.floor(y / self.spriteHeight) + 1
177
-- clamp to map bounds
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
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.
196
-- x - x coordinate in map units
197
-- y - y coordinate in map units
200
-- <Sprite> instance. If no sprite is present at these
201
-- coordinates, the method returns nil.
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]]
208
spr.x = (x - 1) * self.spriteWidth
209
spr.y = (y - 1) * self.spriteHeight
213
print('Warning: asked for map sprite at ' .. x .. ', ' .. y ' but map isn\'t that big')
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.
224
-- x - x coordinate in pixels
225
-- y - y coordinate in pixels
228
-- <Sprite> instance. If no sprite is present at these
229
-- coordinates, the method returns nil.
231
spriteAtPixel = function (self, x, y)
232
return self:spriteAtMap((x - 1) * self.spriteWidth, (y - 1) * self.spriteHeight)
235
-- This overrides a method in <Sprite>, passing along
236
-- collidedWith() calls to all sprites in the map touching a
240
-- other - other <Sprite>
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)
252
-- collect collisions against sprites
254
local collisions = {}
256
for x = startX, endX do
257
for y = startY, endY do
258
local spr = self.sprites[self.map[x][y]]
260
if spr and spr.solid then
261
local sprX = self.x + (x - 1) * spriteWidth
262
local sprY = self.y + (y - 1) * spriteHeight
264
local xOverlap, yOverlap = other:overlap(sprX, sprY, spriteWidth, spriteHeight)
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 })
274
-- sort as usual and pass off collidedWith() calls
276
table.sort(collisions, Collision.sortCollisions)
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)
285
-- this is here mainly for completeness; it's better to specify displacement
286
-- in individual map sprites
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
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)
298
-- collect collisions against sprites
300
local collisions = {}
302
for x = startX, endX do
303
for y = startY, endY do
304
local spr = self.sprites[self.map[x][y]]
306
if spr and spr.solid then
307
local sprX = self.x + (x - 1) * spriteWidth
308
local sprY = self.y + (y - 1) * spriteHeight
310
local xOverlap, yOverlap = other:overlap(sprX, sprY, spriteWidth, spriteHeight)
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 })
320
-- sort as usual and displace
322
table.sort(collisions, Collision.sortCollisions)
324
for _, col in ipairs(collisions) do
325
col.a.x, col.a.y = col.ax, col.ay
326
col.a:displace(other)
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])
373
-- makes sure all sprites receive startFrame messages
375
startFrame = function (self, elapsed)
376
for _, spr in pairs(self.sprites) do
377
spr:startFrame(elapsed)
380
Sprite.startFrame(self, elapsed)
383
-- makes sure all sprites receive update messages
385
update = function (self, elapsed)
386
for _, spr in pairs(self.sprites) do
390
Sprite.update(self, elapsed)
393
-- makes sure all sprites receive endFrame messages
395
endFrame = function (self, elapsed)
396
for _, spr in pairs(self.sprites) do
397
spr:endFrame(elapsed)
400
Sprite.endFrame(self, elapsed)
403
__tostring = function (self)
404
local result = 'Map (x: ' .. self.x .. ', y: ' .. self.y ..
405
', w: ' .. self.width .. ', h: ' .. self.height .. ', '
408
result = result .. 'active, '
410
result = result .. 'inactive, '
414
result = result .. 'visible, '
416
result = result .. 'invisible, '
420
result = result .. 'solid'
422
result = result .. 'not solid'