/ld27

To get this branch, use:
bzr branch http://9ix.org/bzr/ld27
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
16 by Josh C
improve performance with (sketchy, experimental) sprite batches
30
	spriteBatches = {},
31
	prevStartX = nil,
32
	prevStartY = nil,
33
	prevEndX = nil,
34
	prevEndY = nil,
35
1 by Josh C
zoetrope 1.4
36
	-- Method: empty
37
	-- Creates an empty map.
38
	--
39
	-- Arguments:
40
	--		width - width of the map in sprites
41
	--		height - height of the map in sprites
42
	-- 
43
	-- Returns:
44
	--		self, for chaining
45
46
	empty = function (self, width, height)
47
		local x, y
48
		
49
		-- empty the map
50
51
		for x = 1, width do
52
			self.map[x] = {}
53
			
54
			for y = 1, height do
55
				self.map[x][y] = Map.NO_SPRITE
56
			end
57
		end
58
		
59
		-- set bounds
60
		
61
		self.width = width * self.spriteWidth
62
		self.height = height * self.spriteHeight
63
		
64
		return self
65
	end,
66
67
	-- Method: loadMap
68
	-- Loads map data from a file, typically comma-separated values.
69
	-- Each entry corresponds to an index in self.sprites, and all rows
70
	-- must have the same number of columns.
71
	--
72
	-- Arguments:
73
	--		file - filename of source text to use
74
	--		colSeparator - character to use as separator of columns, default ','
75
	--		rowSeparator - character to use as separator of rows, default newline
76
	--
77
	-- Returns:
78
	--		self, for chaining
79
80
	loadMap = function (self, file, colSeparator, rowSeparator)
81
		colSeparator = colSeparator or ','
82
		rowSeparator = rowSeparator or '\n'
83
		
84
		-- load data
85
		
86
		local x, y
87
		local source = Cached:text(file)
88
		local rows = split(source, rowSeparator)
89
		
90
		for y = 1, #rows do
91
			local cols = split(rows[y], colSeparator)
92
			
93
			for x = 1, #cols do
94
				if not self.map[x] then self.map[x] = {} end
95
				self.map[x][y] = tonumber(cols[x])
96
			end
97
		end
98
		
99
		-- set bounds
100
		
101
		self.width = #self.map * self.spriteWidth
102
		self.height = #self.map[1] * self.spriteHeight
103
		
104
		return self
105
	end,
106
107
	-- Method: loadTiles
108
	--- Loads the sprites group with slices of a source image.
109
	--  By default, this uses the Tile class for sprites, but you
110
	--  may pass as replacement class.
111
	--
112
	--  Arguments:
113
	--		image - source image to use for tiles
114
	--		class - class to create objects with; constructor
115
	--				  will be called with properties: image, width,
116
	--				  height, imageOffset (with x and y sub-properties)
117
	--		startIndex - starting index of tiles in self.sprites, default 0
118
	--
119
	--  Returns:
120
	--		self, for chaining
121
122
	loadTiles = function (self, image, class, startIndex)
123
		assert(self.spriteWidth and self.spriteHeight, 'sprite size must be set before loading tiles')
124
		if type(startIndex) ~= 'number' then startIndex = 0 end
125
		
126
		class = class or Tile
127
		self.sprites = self.sprites or {}
128
		
129
		local imageObj = Cached:image(image)
130
		local imageWidth = imageObj:getWidth()
131
		local imageHeight = imageObj:getHeight()
132
		 
133
		local i = startIndex
134
		
135
		for y = 0, imageHeight - self.spriteHeight, self.spriteHeight do
136
			for x = 0, imageWidth - self.spriteWidth, self.spriteWidth do
137
				self.sprites[i] = class:new{ image = image, width = self.spriteWidth,
138
											  height = self.spriteHeight,
139
											  imageOffset = { x = x, y = y }}
140
				i = i + 1
141
			end
142
		end
143
		
144
		return self
145
	end,
146
147
	-- Method: getMapSize
148
	-- Returns the size of the map in map coordinates.
149
	--
150
	-- Arguments:
151
	--		none
152
	--
153
	-- Returns:
154
	--		width and height in integers
155
156
	getMapSize = function (self)
157
		if #self.map == 0 then
158
			return 0, 0
159
		else
160
			return #self.map, #self.map[1]
161
		end
162
	end,
163
164
	-- Method: pixelToMap
165
	-- Converts pixels to map coordinates.
166
	--
167
	-- Arguments:
168
	--		x - x coordinate in pixels
169
	--		y - y coordinate in pixels
170
	--		clamp - clamp to map bounds? defaults to true
171
	--
172
	-- Returns:
173
	--		x, y map coordinates
174
175
	pixelToMap = function (self, x, y, clamp)
176
		if type(clamp) == 'nil' then clamp = true end
177
178
		-- remember, Lua tables start at index 1
179
180
		local mapX = math.floor(x / self.spriteWidth) + 1
181
		local mapY = math.floor(y / self.spriteHeight) + 1
182
		
183
		-- clamp to map bounds
184
		
185
		if clamp then
186
			if mapX < 1 then mapX = 1 end
187
			if mapY < 1 then mapY = 1 end
188
			if mapX > #self.map then mapX = #self.map end
189
			if mapY > #self.map[1] then mapY = #self.map[1] end
190
		end
191
192
		return mapX, mapY
193
	end,
194
195
	-- Method: spriteAtMap
196
	-- Returns the sprite at a given set of map coordinates, with
197
	-- the correct pixel position for that sprite. Remember that
198
	-- sprites in maps are shared, so any changes you make to one
199
	-- sprite will carry over to all instances of that sprite in the map.
200
	--
201
	-- Arguments:
202
	--		x - x coordinate in map units
203
	--		y - y coordinate in map units
204
	--
205
	-- Returns:
206
	--		<Sprite> instance. If no sprite is present at these
207
	--		coordinates, the method returns nil.
208
209
	spriteAtMap = function (self, x, y)
210
		if self.map[x] and self.map[x][y] then
211
			local spr = self.sprites[self.map[x][y]]
212
213
			if spr then 
214
				spr.x = (x - 1) * self.spriteWidth
215
				spr.y = (y - 1) * self.spriteHeight
216
				return spr
217
			end
218
		elseif STRICT then
219
			print('Warning: asked for map sprite at ' .. x .. ', ' .. y ' but map isn\'t that big')
220
		end
221
	end,
222
223
	-- Method: spriteAtPixel
224
	-- Returns the sprite at a given set of pixel coordinates, with
225
	-- the correct pixel position for that sprite. Remember that
226
	-- sprites in maps are shared, so any changes you make to one
227
	-- sprite will carry over to all instances of that sprite in the map.
228
	--
229
	-- Arguments:
230
	--		x - x coordinate in pixels
231
	--		y - y coordinate in pixels
232
	--
233
	-- Returns:
234
	--		<Sprite> instance. If no sprite is present at these
235
	--		coordinates, the method returns nil.
236
237
	spriteAtPixel = function (self, x, y)
238
		return self:spriteAtMap((x - 1) * self.spriteWidth, (y - 1) * self.spriteHeight)
239
	end,
240
241
	-- This overrides a method in <Sprite>, passing along
242
	-- collidedWith() calls to all sprites in the map touching a
243
	-- sprite.
244
	--
245
	-- Arguments:
246
	--		other - other <Sprite>
247
	--
248
	-- Returns:
249
	--		nothing
250
251
	collidedWith = function (self, other)
252
		local spriteWidth = self.spriteWidth
253
		local spriteHeight = self.spriteHeight
254
		local startX, startY = self:pixelToMap(other.x - self.x, other.y - self.y)
255
		local endX, endY = self:pixelToMap(other.x + other.width - self.x,
256
										   other.y + other.height - self.y)
257
258
		-- collect collisions against sprites
259
260
		local collisions = {}
261
		
262
		for x = startX, endX do 
263
			for y = startY, endY do
264
				local spr = self.sprites[self.map[x][y]]
265
				
266
				if spr and spr.solid then
267
					local sprX = self.x + (x - 1) * spriteWidth
268
					local sprY = self.y + (y - 1) * spriteHeight
269
					
270
					local xOverlap, yOverlap = other:overlap(sprX, sprY, spriteWidth, spriteHeight)
271
272
					if xOverlap ~= 0 or yOverlap ~= 0 then
273
						table.insert(collisions, { area = xOverlap * yOverlap, x = xOverlap, y = yOverlap,
274
												   a = spr, ax = sprX, ay = sprY })
275
					end
276
				end
277
			end
278
		end
279
280
		-- sort as usual and pass off collidedWith() calls
281
282
		table.sort(collisions, Collision.sortCollisions)
283
284
		for _, col in ipairs(collisions) do
285
			col.a.x, col.a.y = col.ax, col.ay
286
			col.a:collidedWith(other, col.x, col.y)
287
			other:collidedWith(col.a, col.x, col.y)
288
		end
289
	end,
290
291
	-- this is here mainly for completeness; it's better to specify displacement
292
	-- in individual map sprites
293
294
	displace = function (self, other, xHint, yHint)	
295
		if not self.solid or self == other or not other.solid then return end
296
		if STRICT then assert(other:instanceOf(Sprite), 'asked to displace a non-sprite') end
297
298
		local spriteWidth = self.spriteWidth
299
		local spriteHeight = self.spriteHeight
300
		local startX, startY = self:pixelToMap(other.x - self.x, other.y - self.y)
301
		local endX, endY = self:pixelToMap(other.x + other.width - self.x,
302
										   other.y + other.height - self.y)
303
304
		-- collect collisions against sprites
305
306
		local collisions = {}
307
		
308
		for x = startX, endX do 
309
			for y = startY, endY do
310
				local spr = self.sprites[self.map[x][y]]
311
				
312
				if spr and spr.solid then
313
					local sprX = self.x + (x - 1) * spriteWidth
314
					local sprY = self.y + (y - 1) * spriteHeight
315
					
316
					local xOverlap, yOverlap = other:overlap(sprX, sprY, spriteWidth, spriteHeight)
317
318
					if xOverlap ~= 0 or yOverlap ~= 0 then
319
						table.insert(collisions, { area = xOverlap * yOverlap, x = xOverlap, y = yOverlap,
320
												   a = spr, ax = sprX, ay = sprY })
321
					end
322
				end
323
			end
324
		end
325
326
		-- sort as usual and displace
327
328
		table.sort(collisions, Collision.sortCollisions)
329
330
		for _, col in ipairs(collisions) do
331
			col.a.x, col.a.y = col.ax, col.ay
332
			col.a:displace(other)
333
		end
334
	end,
335
336
	draw = function (self, x, y)
337
		-- lock our x/y coordinates to integers
338
		-- to avoid gaps in the tiles
339
	
340
		x = math.floor(x or self.x)
341
		y = math.floor(y or self.y)
342
		if not self.visible or self.alpha <= 0 then return end
343
		if not self.spriteWidth or not self.spriteHeight then return end
344
		
345
		-- determine drawing bounds
346
		-- we draw to fill the entire app windoow
347
		
16 by Josh C
improve performance with (sketchy, experimental) sprite batches
348
		--local startX, startY = self:pixelToMap(-x, -y)
349
		--local endX, endY = self:pixelToMap(the.app.width - x, the.app.height - y)
350
		local startX, startY = 1, 1
351
		local endX, endY = self:getMapSize()
352
353
		-- if using spritebatches, check if anything has changed
354
		-- TODO: encapsulate this check?
355
		if startX ~= self.prevStartX or
356
			startY ~= self.prevStartY or
357
			endX ~= self.prevEndX or
358
			endY ~= self.prevEndY then
359
360
			--print('building sprite batch')
361
362
			self.prevStartX, self.prevStartY = startX, startY
363
			self.prevEndX, self.prevEndY = endX, endY
364
365
			for k, batch in pairs(self.spriteBatches) do
366
				--batch:bind()
367
				batch:clear()
368
			end
369
370
			-- queue each sprite drawing operation
371
372
			local toDraw = {}
373
374
			for drawY = startY, endY do
375
				for drawX = startX, endX do
376
					local sprite = self.sprites[self.map[drawX][drawY]]
377
378
					if sprite and sprite.visible then
379
						if not toDraw[sprite] then
380
							toDraw[sprite] = {}
381
						end
382
383
						table.insert(toDraw[sprite], { (drawX - 1) * self.spriteWidth,
384
						                               (drawY - 1) * self.spriteHeight })
1 by Josh C
zoetrope 1.4
385
					end
16 by Josh C
improve performance with (sketchy, experimental) sprite batches
386
				end
387
			end
388
389
			-- draw each sprite in turn
390
391
			for sprite, list in pairs(toDraw) do
392
				for _, coords in pairs(list) do
393
					sprite:draw(coords[1], coords[2], self)
1 by Josh C
zoetrope 1.4
394
				end
395
			end
396
		end
16 by Josh C
improve performance with (sketchy, experimental) sprite batches
397
398
		for _, batch in pairs(self.spriteBatches) do
399
			--batch:unbind()
400
			love.graphics.draw(batch, x, y)
1 by Josh C
zoetrope 1.4
401
		end
402
	end,
403
404
	-- makes sure all sprites receive startFrame messages
405
406
	startFrame = function (self, elapsed)
407
		for _, spr in pairs(self.sprites) do
408
			spr:startFrame(elapsed)
409
		end
410
411
		Sprite.startFrame(self, elapsed)
412
	end,
413
414
	-- makes sure all sprites receive update messages
415
416
	update = function (self, elapsed)
417
		for _, spr in pairs(self.sprites) do
418
			spr:update(elapsed)
419
		end
420
421
		Sprite.update(self, elapsed)
422
	end,
423
424
	-- makes sure all sprites receive endFrame messages
425
426
	endFrame = function (self, elapsed)
427
		for _, spr in pairs(self.sprites) do
428
			spr:endFrame(elapsed)
429
		end
430
431
		Sprite.endFrame(self, elapsed)
432
	end,
433
434
	__tostring = function (self)
435
		local result = 'Map (x: ' .. self.x .. ', y: ' .. self.y ..
436
					   ', w: ' .. self.width .. ', h: ' .. self.height .. ', '
437
438
		if self.active then
439
			result = result .. 'active, '
440
		else
441
			result = result .. 'inactive, '
442
		end
443
444
		if self.visible then
445
			result = result .. 'visible, '
446
		else
447
			result = result .. 'invisible, '
448
		end
449
450
		if self.solid then
451
			result = result .. 'solid'
452
		else
453
			result = result .. 'not solid'
454
		end
455
456
		return result .. ')'
457
	end
458
}