/zoeplat

To get this branch, use:
bzr branch http://9ix.org/bzr/zoeplat
1 by Josh C
zoetrope 1.3.1
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[1] * self.spriteWidth
96
		self.height = #self.map * 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: 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
313
	-- Method: getMapSize
314
	-- Returns the size of the map in sprites.
315
	--
316
	-- Arguments:
317
	--		none
318
	--
319
	-- Returns:
320
	--		width and height in integers
321
322
	getMapSize = function (self)
323
		if #self.map == 0 then
324
			return 0, 0
325
		else
326
			return #self.map, #self.map[1]
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
		
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
404
	end,
405
406
	-- makes sure all sprites receive startFrame messages
407
408
	startFrame = function (self, elapsed)
409
		for _, spr in pairs(self.sprites) do
410
			spr:startFrame(elapsed)
411
		end
412
413
		Sprite.startFrame(self, elapsed)
414
	end,
415
416
	-- makes sure all sprites receive update messages
417
418
	update = function (self, elapsed)
419
		for _, spr in pairs(self.sprites) do
420
			spr:update(elapsed)
421
		end
422
423
		Sprite.update(self, elapsed)
424
	end,
425
426
	-- makes sure all sprites receive endFrame messages
427
428
	endFrame = function (self, elapsed)
429
		for _, spr in pairs(self.sprites) do
430
			spr:endFrame(elapsed)
431
		end
432
433
		Sprite.endFrame(self, elapsed)
434
	end,
435
436
	__tostring = function (self)
437
		local result = 'Map (x: ' .. self.x .. ', y: ' .. self.y ..
438
					   ', w: ' .. self.width .. ', h: ' .. self.height .. ', '
439
440
		if self.active then
441
			result = result .. 'active, '
442
		else
443
			result = result .. 'inactive, '
444
		end
445
446
		if self.visible then
447
			result = result .. 'visible, '
448
		else
449
			result = result .. 'invisible, '
450
		end
451
452
		if self.solid then
453
			result = result .. 'solid'
454
		else
455
			result = result .. 'not solid'
456
		end
457
458
		return result .. ')'
459
	end
460
}