1 /**
  2  * The Render Engine
  3  * TileMap
  4  *
  5  * @fileoverview A rectangular map of tiles.
  6  *
  7  * @author: Brett Fattori (brettf@renderengine.com)
  8  * @author: $Author: bfattori $
  9  * @version: $Revision: 1556 $
 10  *
 11  * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com)
 12  *
 13  * Permission is hereby granted, free of charge, to any person obtaining a copy
 14  * of this software and associated documentation files (the "Software"), to deal
 15  * in the Software without restriction, including without limitation the rights
 16  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 17  * copies of the Software, and to permit persons to whom the Software is
 18  * furnished to do so, subject to the following conditions:
 19  *
 20  * The above copyright notice and this permission notice shall be included in
 21  * all copies or substantial portions of the Software.
 22  *
 23  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 24  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 25  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 26  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 27  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 28  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 29  * THE SOFTWARE.
 30  *
 31  */
 32 
 33 // The class this file defines and its required classes
 34 R.Engine.define({
 35     "class":"R.resources.types.TileMap",
 36     "requires":[
 37         "R.engine.GameObject",
 38         "R.resources.types.Tile",
 39         "R.rendercontexts.CanvasContext",
 40         "R.math.Rectangle2D",
 41         "R.util.RenderUtil"
 42     ]
 43 });
 44 
 45 /**
 46  * @class A 2d tile map, comprised of many tiles.  Tiles in a map are all the same
 47  *        size.
 48  *
 49  * @constructor
 50  * @param name {String} The name of the tilemap
 51  * @description A tile map is a collection of tiles, all the same dimensions.
 52  * @extends R.engine.GameObject
 53  */
 54 R.resources.types.TileMap = function () {
 55     return R.engine.GameObject.extend(/** @scope R.resources.types.TileMap.prototype */{
 56 
 57         baseTile:null,
 58         tilemap:null,
 59         animatedTiles:null,
 60         image:null,
 61         width:0,
 62         height:0,
 63         tileScale:null,
 64         zIndex:0,
 65         parallax:null,
 66         dimensions:null,
 67         isHTMLContext:false,
 68         isRendered:false,
 69         tilemapImage:null,
 70         alwaysRender:false,
 71 
 72         /** @private */
 73         constructor:function (name, width, height) {
 74             this.base(name);
 75             this.baseTile = null;
 76             this.zIndex = 0;
 77             this.parallax = R.math.Point2D.create(1, 1);
 78             this.isHTMLContext = false;
 79             this.isRendered = false;
 80             this.tilemapImage = null;
 81             this.alwaysRender = false;
 82 
 83             // The tile map is a dense array
 84             this.tilemap = [];
 85             R.engine.Support.fillArray(this.tilemap, width * height, null);
 86             this.dimensions = R.math.Point2D.create(width, height);
 87 
 88             // A list of tiles which are animated and need to be updated each frame
 89             this.animatedTiles = [];
 90 
 91             // The image that will contain the rendered tile map
 92             this.image = null;
 93             this.width = width;
 94             this.height = height;
 95             this.tileScale = R.math.Vector2D.create(1, 1);
 96         },
 97 
 98         /**
 99          * Destroy the tilemap instance
100          */
101         destroy:function () {
102             this.base();
103             this.tilemap = [];
104         },
105 
106         /**
107          * Release the tilemap back into the pool for reuse
108          */
109         release:function () {
110             this.base();
111             this.tilemap = null;
112             this.isRendered = false;
113         },
114 
115         afterAdd:function (renderContext) {
116             this.isHTMLContext = !!(renderContext instanceof R.rendercontexts.HTMLElementContext);
117         },
118 
119         /**
120          * Set the dimensions of the tile map.  Setting the dimensions will clear the tile map.
121          * @param x {Number|R.math.Point2D}
122          * @param y {Number}
123          */
124         setDimensions:function (x, y) {
125             this.dimensions.set(x, y);
126             this.tilemap = [];
127             R.engine.Support.fillArray(this.tilemap, this.dimensions.x * this.dimensions.y, null);
128         },
129 
130         /**
131          * Get the basis tile for the tile map.  The first tile within a tile map determines
132          * the basis of all tiles.  Thus, if you drop a 32x32 tile into the tile map, all tiles
133          * must be divisible by 32 along each axis.
134          * @return {R.resources.types.Tile}
135          */
136         getBaseTile:function () {
137             return this.baseTile;
138         },
139 
140         /**
141          * Get the internal representation of the tile map.
142          * @return {Array}
143          * @private
144          */
145         getTileMap:function () {
146             return this.tilemap;
147         },
148 
149         /**
150          * Set the tile at the given position.
151          * @param tile {R.resources.types.Tile} The tile
152          * @param x {Number} The X position of the tile
153          * @param y {Number} The Y position of the tile
154          */
155         setTile:function (tile, x, y) {
156             // Check to see if the tile is the same size as the last added tile
157             var tbb = tile.getBoundingBox();
158             Assert(this.baseTile == null || (tbb.w % this.baseTile.getBoundingBox().w == 0 && tbb.w % this.baseTile.getBoundingBox().w == 0),
159                 "Tiles in a TileMap must be the same size!");
160 
161             this.tilemap[x + y * this.width] = tile;
162             if (!this.baseTile) {
163                 this.baseTile = tile;
164             }
165         },
166 
167         /**
168          * Get the tile at the given position.  The position is a tile location between
169          * zero and the dimensions of the tile map along the X and Y axis.  For a tile map
170          * that is 200 x 200, X and Y would be between 0 and 200.
171          *
172          * @param x {Number} The X position
173          * @param y {Number} The Y position
174          * @return {R.resources.types.Tile}
175          */
176         getTile:function (x, y) {
177             return this.tilemap[x + y * this.width];
178         },
179 
180         /**
181          * Get the tile at the given point.  The point is a world location which will be
182          * transformed into a tile location.  The point will be adjusted to reflect the
183          * position within the tile.
184          *
185          * @param point {R.math.Point2D} The point to retrieve the tile for
186          * @return {R.resources.types.Tile} The tile, or <code>null</code>
187          */
188         getTileAtPoint:function (point) {
189             if (!this.baseTile) {
190                 return null;
191             }
192 
193             var bw = this.baseTile.getBoundingBox().w, bh = this.baseTile.getBoundingBox().h,
194                 x = Math.floor(point.x / bw), y = Math.floor(point.y / bh),
195                 tile = this.getTile(x, y);
196 
197             // If there's no tile at this location, return null
198             if (tile == null) {
199                 return tile;
200             }
201 
202             // Adjust the point to be within the tile's bounding box and return the tile
203             point.set((tile.getBoundingBox().w - bw) + (point.x % bw),
204                 (tile.getBoundingBox().h - bh) + (point.y % bh));
205             return tile;
206         },
207 
208         /**
209          * Clear the tile at the given position, returning the tile that occupied the
210          * position, or <code>null</code> if there was no tile.
211          * @param x {Number} The X position
212          * @param y {Number} The Y position
213          * @return {R.resources.types.Tile}
214          */
215         clearTile:function (x, y) {
216             var tile = this.tilemap[x + y * this.width];
217             this.tilemap[x + y * this.width] = null;
218             return tile;
219         },
220 
221         setAlwaysRender: function(state) {
222             this.alwaysRender = state;
223             this.isRendered = false;
224         },
225 
226         /**
227          * Set the parallax distance of the tile map from the viewer's eye.  Setting the parallax distance
228          * can create the illusion of depth when layers move at different rates along the X
229          * and Y axis.  The distance is a vector which specifies the amount of offset along each
230          * axis, from the viewer's eye, with 1 being the middle plane.  Each value should be a floating
231          * point number with numbers closer to zero meaning closer to the eye (or faster change) and
232          * numbers greater than 1 meaning farther from the eye (or slower change).
233          *
234          * @param xOrPt {Number|R.math.Vector2D} The X offset, or a vector indicating the amount of offset
235          * @param [y] {Number} The Y offset if <code>xOrPt</code> was a number
236          */
237         setParallax:function (xOrPt, y) {
238             this.parallax.set(xOrPt, y);
239         },
240 
241         /**
242          * Returns the parallax distance of the tile map along each axis.
243          * @return {R.math.Vector2D}
244          */
245         getParallax:function () {
246             return this.parallax;
247         },
248 
249         renderStaticTiles: function(renderContext, time, dt) {
250             if (this.isHTMLContext) {
251                 return;
252             }
253 
254             // Render static tiles to an image and set that as the background for the
255             // render context.  First we need to calculate the width and height of the tilemap
256             var baseTileSize = this.baseTile.getBoundingBox(), tileWidth = baseTileSize.w, tileHeight = baseTileSize.h;
257             var renderWidth = tileWidth * this.width, renderHeight = tileHeight * this.height;
258             var tempContext;
259 
260             if (!this.alwaysRender) {
261                 tempContext = R.util.RenderUtil.getTempContext(R.rendercontexts.CanvasContext, renderWidth, renderHeight);
262             } else {
263                 tempContext = renderContext;
264             }
265 
266             var tile, t, rect = R.math.Rectangle2D.create(0, 0, 1, 1), topLeft = R.math.Point2D.create(0, 0);
267 
268             // Render out all of the tiles
269             for (t = 0; t < this.tilemap.length; t++) {
270                 tile = this.tilemap[t];
271                 if (!tile)
272                     continue;
273 
274                 var x = (t % this.width) * tileWidth, y = Math.floor(t / this.height) * tileHeight;
275                 rect.set(x, y, tileWidth, tileHeight);
276 
277                 // Get the frame and draw the tile
278                 var f = tile.getFrame(0, 0),
279                     obj = tempContext.drawImage(rect, tile.getSourceImage(), f,
280                         (tile.isAnimation() ? tile : null));
281 
282                 f.destroy();
283             }
284 
285             if (!this.alwaysRender) {
286                 renderContext.jQ().css("backgroundImage", "url(" + tempContext.getDataURL('image/png') + ")");
287                 tempContext.destroy();
288                 tempContext = null;
289                 this.isRendered = true;
290             }
291 
292             rect.destroy();
293             topLeft.destroy();
294         },
295 
296         /**
297          * Update the tile map, rendering it to the context.
298          *
299          * @param renderContext {R.rendercontexts.AbstractRenderContext} The context the object exists within
300          * @param time {Number} The current engine time, in milliseconds
301          * @param dt {Number} The delta between the world time and the last time the world was updated
302          *          in milliseconds.
303          */
304         update:function (renderContext, time, dt) {
305             if (this.baseTile == null) {
306                 // Nothing to render yet
307                 return;
308             }
309 
310             if (!this.isRendered) {
311                 this.renderStaticTiles(renderContext, time, dt);
312             }
313 
314             renderContext.pushTransform();
315             renderContext.setPosition(R.math.Point2D.ZERO);
316             renderContext.setScale(1);
317 
318             var tile, t, rect = R.math.Rectangle2D.create(0, 0, 1, 1), wp = renderContext.getWorldPosition(),
319                 tileWidth = this.baseTile.getBoundingBox().w, tileHeight = this.baseTile.getBoundingBox().h;
320 
321             var topLeft = R.clone(wp);
322             topLeft.convolve(this.parallax);
323             topLeft.sub(wp);
324 
325             // Render out all of the tiles
326             for (t = 0; t < this.tilemap.length; t++) {
327                 tile = this.tilemap[t];
328                 if (!tile || (tile && !tile.isAnimation()))
329                     continue;
330 
331                 // In an HTML context we only want to render static (non-animated) tiles one time.
332                 // However, animated tiles will need to animate each frame.  For a graphical context,
333                 // we'll render all tiles each frame.
334 
335                 var x = (t % this.width) * tileWidth, y = Math.floor(t / this.height) * tileHeight;
336                 rect.set(x - wp.x, y - wp.y, tileWidth, tileHeight);
337 
338                 rect.add(topLeft);
339 
340                 // If the rect isn't visible, skip it
341 //                    if (!this.isHTMLContext && !rect.isIntersecting(renderContext.getViewport()))
342 //                        continue;
343 
344                 // Get the frame and draw the tile
345                 var f = tile.getFrame(time, dt),
346                     obj = renderContext.drawImage(rect, tile.getSourceImage(), f,
347                         (tile.isAnimation() ? tile : null));
348 
349                 if (this.isHTMLContext && !tile.getElement()) {
350                     // In an HTML context, set the element for the tile so animated tiles can be updated
351                     tile.setElement(obj);
352                 }
353 
354                 f.destroy();
355 
356             }
357 
358 
359             rect.destroy();
360             topLeft.destroy();
361 
362             renderContext.popTransform();
363         },
364 
365         /**
366          * Get the z-index of the tile map.
367          * @return {Number}
368          */
369         getZIndex:function () {
370             return this.zIndex;
371         },
372 
373         /**
374          * Set the z-index of the tile map.
375          * @param zIndex {Number} The z-index (depth) of the tile map.
376          */
377         setZIndex:function (zIndex) {
378             this.zIndex = zIndex;
379         },
380 
381         /**
382          * When editing objects, this method returns an object which
383          * contains the properties with their getter and setter methods.
384          * @return {Object} The properties object
385          */
386         getProperties:function () {
387             var self = this;
388             var prop = this.base(self);
389             return $.extend(prop, {
390                 "Dimensions":[function () {
391                     return self.dimensions.toString()
392                 }, function (i) {
393                     var coords = i.split(",");
394                     self.setDimensions(parseInt(coords[0]), parseInt(coords[1]));
395                 }, true],
396                 "TileScaleX":[function () {
397                     return self.tileScale.x;
398                 }, function (i) {
399                     self.tileScale.setX(parseFloat(i));
400                 }, true],
401                 "TileScaleY":[function () {
402                     return self.tileScale.y;
403                 }, function (i) {
404                     self.tileScale.setY(parseFloat(i));
405                 }, true],
406                 "TileSizeX":[function () {
407                     return self.baseTile ? self.baseTile.getBoundingBox().w : "";
408                 }, null, false],
409                 "TileSizeY":[function () {
410                     return self.baseTile ? self.baseTile.getBoundingBox().h : "";
411                 }, null, false],
412                 "Zindex":[function () {
413                     return self.getZIndex();
414                 }, function (i) {
415                     self.setZIndex(parseInt(i));
416                 }, true],
417                 "Parallax":[function () {
418                     return self.getParallax().toString();
419                 }, function (i) {
420                     var coords = i.split(",");
421                     self.setParallax(parseFloat(coords[0]), parseFloat(coords[1]));
422                 }, true]
423             });
424         }
425 
426     }, /** @scope R.resources.types.TileMap.prototype */{
427         /**
428          * Gets the class name of this object.
429          * @return {String} The string "R.resources.types.TileMap"
430          */
431         getClassName:function () {
432             return "R.resources.types.TileMap";
433         },
434 
435         /** @private */
436         solidityMaps:{},
437 
438         /**
439          * Compute the solidity map for a tile, based on the alpha value of each pixel in the
440          * tile image.  The resource defines what the alpha threshold is.
441          * @param tile {R.resources.types.tile} The tile to compute the map for
442          */
443         computeSolidityMap:function (tile) {
444             // Is there a solidity map for this tile already?
445             var uniqueId = tile.getTileResource().resourceName + tile.getName();
446             if (R.resources.types.TileMap.solidityMaps[uniqueId]) {
447                 return R.resources.types.TileMap.solidityMaps[uniqueId];
448             }
449 
450             // Is the tile a single frame, or animated?
451             var count = tile.getFrameCount();
452             var fSpeed = tile.getFrameSpeed() == -1 ? 0 : tile.getFrameSpeed();
453 
454             // The alpha value above which pixels will be considered solid
455             var threshold = tile.getTileResource().info.transparencyThreshold;
456 
457             // The solidity map is only calculated for the first frame
458             var sMap = {
459                 map:null,
460                 status:R.resources.types.Tile.ALL_MIXED
461             };
462 
463             // Get the image data for the frame
464             var fr = tile.getFrame(0, 0);
465             var imgData = R.util.RenderUtil.extractImageData(tile.getSourceImage(), fr).data;
466 
467             // Compute the map, based on the alpha values
468             var tmpMap = [], opaque = 0;
469             for (var y = 0; y < fr.h; y++) {
470                 for (var x = 0; x < fr.w; x++) {
471                     opaque += imgData[(x + y * fr.w) + 3] > threshold ? 1 : 0;
472                 }
473             }
474 
475             // Determine if either of the short-circuit cases apply
476             if (opaque == 0) {
477                 sMap.status = R.resources.types.Tile.ALL_TRANSPARENT;
478             } else if (opaque == fr.h * fr.w) {
479                 sMap.status = R.resources.types.Tile.ALL_OPAQUE;
480             }
481 
482             // If the map is mixed, store the map for raycast tests
483             if (sMap.status == R.resources.types.Tile.ALL_MIXED) {
484                 sMap.map = tmpMap;
485             }
486 
487             // Store the solidity map
488             R.resources.types.TileMap.solidityMaps[uniqueId] = sMap;
489             return R.resources.types.TileMap.solidityMaps[uniqueId];
490         },
491 
492         /**
493          * Cast a ray through the tile map, looking for collisions along the
494          * ray.  If a collision is found, a {@link R.struct.CollisionData} object
495          * will be returned or <code>null</code> if otherwise.
496          * <p/>
497          * If a collision occurs, the value stored in {@link R.struct.CollisionData#shape1}
498          * is the tile which was collided with.  The value in {@link R.struct.CollisionData#impulseVector}
499          * is a vector to separate the game object from the tile.
500          *
501          * @param tileMap {R.resources.types.TileMap} The tile map to test against
502          * @param rayInfo {R.start.rayInfo} The ray info structure that defines the ray to test
503          * @return {R.struct.rayInfo} The ray info structure passed into the cast method.  If
504          *    a collision occurred, the shape and impact point will be set.
505          */
506         castRay:function (tileMap, rayInfo) {
507             // Get all of the points along the line and test them against the
508             // collision model.  At the first collision, we stop performing any more checks.
509             var begin = R.math.Point2D.create(rayInfo.startPoint), end = R.math.Point2D.create(rayInfo.startPoint),
510                 line, pt = 0, tile, test = R.math.Vector2D.create(0, 0);
511 
512 
513             // Make sure the length isn't greater than the max
514             if (rayInfo.direction.len() > R.resources.types.TileMap.MAX_RAY_LENGTH) {
515                 rayInfo.direction.normalize().mul(R.resources.types.TileMap.MAX_RAY_LENGTH);
516             }
517 
518             end.add(rayInfo.direction);
519 
520             /* pragma:DEBUG_START */
521             if (R.Engine.getDebugMode() && arguments[2]) {
522                 var f = R.clone(begin), t = R.clone(end);
523 
524                 arguments[2].postRender(function () {
525                     this.setLineStyle("orange");
526                     this.setLineWidth(2);
527                     this.drawLine(f, t);
528                     f.destroy();
529                     t.destroy();
530                 });
531             }
532             /* pragma:DEBUG_END */
533 
534             // Use Bresenham's algorithm to calculate the points along the line
535             line = R.math.Math2D.bresenham(begin, end);
536 
537             while (!tile && pt < line.length) {
538                 test.set(line[pt]);
539 
540                 // Find the tile for the current point
541                 tile = tileMap.getTileAtPoint(test);
542 
543                 if (tile && tile.testPoint(test)) {
544                     // A collision occurs at the adjusted point within the tile
545                     rayInfo.set(line[pt], tile, R.clone(test));
546                 }
547 
548                 pt++;
549             }
550 
551             // Clean up a bit
552             begin.destroy();
553             end.destroy();
554             test.destroy();
555 
556             // Destroy the points in the line
557             while (line.length > 0) {
558                 line.shift().destroy();
559             }
560 
561             return rayInfo;
562         },
563 
564         /**
565          * Serialize the tile map into an object.
566          * @param tilemap {R.resources.types.TileMap} The tile map to serialize
567          * @return {Object}
568          */
569         serialize:function (tilemap, defaults) {
570             defaults = defaults || [];
571             var propObj = { properties:R.engine.PooledObject.serialize(tilemap, defaults)},
572                 tmap = [].concat(tilemap.getTileMap()), tmap2 = [], tile;
573 
574             // First pass, convert to zeros (empty) and tile references
575             for (tile = 0; tile < tmap.length; tile++) {
576                 tmap[tile] = tmap[tile] != null ? tmap[tile].getTileResource().resourceName + ":" + tmap[tile].getName() : 0;
577             }
578 
579             // Second pass, collapse tiles using RLE
580             var rle = 0, lastTile = null;
581             for (tile = 0; tile < tmap.length; tile++) {
582                 if (tmap[tile] !== lastTile) {
583                     if (lastTile !== null) {
584                         tmap2.push((lastTile == 0 ? "e:" : lastTile + ":") + rle);
585                     }
586                     rle = 0;
587                     lastTile = tmap[tile];
588                 }
589                 rle++;
590             }
591 
592             // Capture remaining tiles
593             tmap2.push((lastTile == 0 ? "e:" : lastTile + ":") + rle);
594 
595             propObj.map = tmap2;
596             return propObj;
597         },
598 
599         /**
600          * Deserialize the object back into a tile map.
601          * @param obj {Object} The object to deserialize
602          * @param [clazz] {Class} The object class to populate
603          */
604         deserialize:function (obj, tileLoaders, clazz) {
605             // Searches the tile loaders for the resource and tile,
606             // returning the first instance of the tile found
607             function findTile(res, name) {
608                 var tile = null;
609                 for (var tl = 0; tl < tileLoaders.length; tl++) {
610                     tile = tileLoaders[tl].getTile(res, name);
611                     if (tile != null) break;
612                 }
613                 return tile;
614             }
615 
616             // Extract the properties and map from the object
617             var props = obj.properties, map = obj.map;
618             clazz = clazz || R.resources.types.TileMap.create(props.name, 1, 1);
619             R.engine.PooledObject.deserialize(props, clazz);
620 
621             // Repopulate the map
622             var ptr = 0;
623             for (var tile = 0; tile < map.length; tile++) {
624                 // Check for empties
625                 if (map[tile].indexOf("e:") == 0) {
626                     // Skip empties
627                     ptr += parseInt(map[tile].split(":")[1]);
628                 } else {
629                     // Populate tiles
630                     var tileDesc = map[tile].split(":"), resource = tileDesc[0],
631                         tileName = tileDesc[1], qty = parseInt(tileDesc[2]);
632 
633                     var t = findTile(resource, tileName);
634                     if (t != null && clazz.baseTile == null) {
635                         clazz.baseTile = t;
636                     }
637 
638                     for (var c = 0; c < qty; c++) {
639                         // We want to clone animated tiles.  Static tiles can all
640                         // refer to the same tile to save memory
641                         if (t && t.isAnimation()) {
642                             t = R.clone(t);
643                         }
644 
645                         clazz.tilemap[ptr++] = t;
646                     }
647                 }
648             }
649 
650             return clazz;
651         },
652 
653         /**
654          * The maximum length of a cast ray (1000)
655          * @type {Number}
656          */
657         MAX_RAY_LENGTH:1000
658 
659     });
660 };
661