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