/spacey

To get this branch, use:
bzr branch http://9ix.org/bzr/spacey
1 by Josh C
zoetrope 1.4
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
}