1 /* 2 ******************************************************************************************* 3 * Dgame (a D game framework) - Copyright (c) Randy Schütt 4 * 5 * This software is provided 'as-is', without any express or implied warranty. 6 * In no event will the authors be held liable for any damages arising from 7 * the use of this software. 8 * 9 * Permission is granted to anyone to use this software for any purpose, 10 * including commercial applications, and to alter it and redistribute it 11 * freely, subject to the following restrictions: 12 * 13 * 1. The origin of this software must not be misrepresented; you must not claim 14 * that you wrote the original software. If you use this software in a product, 15 * an acknowledgment in the product documentation would be appreciated but is 16 * not required. 17 * 18 * 2. Altered source versions must be plainly marked as such, and must not be 19 * misrepresented as being the original software. 20 * 21 * 3. This notice may not be removed or altered from any source distribution. 22 ******************************************************************************************* 23 */ 24 module Dgame.Graphics.TileMap; 25 26 private { 27 import std.math : log2, pow, round, ceil, floor, fmax; 28 import std.file : exists; 29 import std.path : dirName; 30 31 import derelict.opengl3.gl; 32 import derelict.sdl2.sdl; 33 34 import Dgame.Internal.Log; 35 36 import Dgame.Math.Vector2; 37 import Dgame.Math.Rect; 38 import Dgame.Graphics.Drawable; 39 import Dgame.Graphics.Transformable; 40 import Dgame.Graphics.Shape; 41 import Dgame.Graphics.Surface; 42 import Dgame.Graphics.Texture; 43 import Dgame.System.VertexBufferObject; 44 } 45 46 /** 47 * This structure stores information about the tile map properties 48 */ 49 struct TileMapInfo { 50 /** 51 * The map width in pixel 52 */ 53 ushort width; 54 /** 55 * The map height in pixel 56 */ 57 ushort height; 58 /** 59 * The map size. 60 * index 0 is width / tileWidth and index 1 is height / tileHeight 61 */ 62 ushort[2] mapSize; 63 /** 64 * The tile width in pixel (for example: 16, 32, 64) 65 */ 66 ubyte tileWidth; 67 /** 68 * The tile height in pixel 69 */ 70 ubyte tileHeight; 71 /** 72 * The map filename 73 */ 74 string source; 75 } 76 77 /** 78 * The Tile structure contains informations about every tile on the map 79 */ 80 struct Tile { 81 /** 82 * The gid is the Tile id. 83 * It contains the positions of this tile on the tileset. 84 * ---- 85 * const uint tilesPerRow = mapWidth / tileWidth; 86 * 87 * uint y = gid / tilesPerRow; 88 * uint x = gid % tilesPerRow; 89 * ---- 90 */ 91 ushort gid; 92 /** 93 * The coordinates in pixel of this tile on the map 94 */ 95 ushort[2] pixelCoords; 96 /** 97 * The coordinates of this tile on the map 98 */ 99 ushort[2] tileCoords; 100 /** 101 * The layer of the tile, if any 102 */ 103 string layer; 104 } 105 106 ushort roundToNext2Pot(ushort dim) { 107 float l = log2(dim); 108 109 return cast(ushort) pow(2, round(l)); 110 } unittest { 111 assert(roundToNext2Pot(512) == 512); 112 assert(roundToNext2Pot(832) == 1024); 113 } 114 115 ushort calcDim(size_t tileNum, ubyte tileDim) { 116 if (tileDim == 0) 117 return 0; 118 if (tileNum == 0) 119 return 0; 120 if (tileNum == 1) 121 return tileDim; 122 123 enforce(tileNum < ubyte.max, "Too large dimension."); 124 125 ubyte dim1 = cast(ubyte) tileNum; 126 ubyte dim2 = 1; 127 128 while (dim1 > dim2) { 129 dim1 = cast(ubyte) ceil(dim1 / 2f); 130 dim2 *= 2; 131 } 132 ///debug writeln("TileNum: ", tileNum, " - Dim1: ", dim1, " - Dim2: ", dim2); 133 version(none) { 134 return roundToNext2Pot(dim1); 135 } else { 136 return cast(ushort)(fmax(dim1, dim2) * tileDim); 137 } 138 } unittest { 139 assert(calcDim(14 , 16) == 64); 140 assert(calcDim(2 , 16) == 32); 141 assert(calcDim(0 , 16) == 0); 142 assert(calcDim(1 , 16) == 16); 143 assert(calcDim(4 , 16) == 32); 144 assert(calcDim(28 , 16) == 128); 145 assert(calcDim(100, 16) == 256); 146 assert(calcDim(96 , 16) == 256); 147 assert(calcDim(63 , 16) == 128); 148 assert(calcDim(65 , 16) == 256); 149 assert(calcDim(46 , 16) == 128); 150 } 151 152 short[2] calcPos(ushort gid, ushort width, ushort tw, ushort th) pure nothrow { 153 int tilesPerRow = width / tw; 154 155 int y = gid / tilesPerRow; 156 int x = gid % tilesPerRow; 157 158 if (x) 159 x -= 1; 160 else { 161 y -= 1; 162 x = tilesPerRow - 1; 163 } 164 165 return [cast(short)(x * tw), cast(short)(y * th)]; 166 } unittest { 167 assert(calcPos(109, 832, 16, 16) == [4 * 16, 2 * 16]); 168 } 169 170 /** 171 * The Tile map consist of tiles which are stored in a XML file (preferably build with tiled) 172 * 173 * Author: rschuett 174 */ 175 class TileMap : Transformable, Drawable { 176 protected: 177 /** 178 * The read method must be overriden by any specialized TileMap. 179 */ 180 abstract void _readTileMap(); 181 182 void _loadTileset() in { 183 assert(this._tmi.tileWidth == this._tmi.tileHeight, "Tile dimensions must be equal."); 184 } body { 185 ShortRect[ushort] used; 186 uint doubly = 0; 187 188 if (!.exists(this._tmi.source)) 189 this._tmi.source = dirName(this._filename) ~ '/' ~ this._tmi.source; 190 191 Surface tileset = Surface(this._tmi.source); 192 193 /// Sammeln der Tiles, die wirklich benötigt werden 194 foreach (ref const Tile t; this._tiles) { 195 if (t.gid !in used) { 196 const short[2] pos = calcPos(t.gid, tileset.width, this._tmi.tileWidth, this._tmi.tileHeight); 197 used[t.gid] = ShortRect(pos[0], pos[1], this._tmi.tileWidth, this._tmi.tileHeight); 198 } else 199 doubly++; 200 } 201 202 debug Log.info("%d are double used and we need %d tiles.", doubly, used.length); 203 204 if (this._doCompress) 205 this._compress(tileset, used); 206 else { 207 //tileset.saveToFile("new_tilset.png"); 208 Texture.Format t_fmt = Texture.Format.None; 209 if (!tileset.isMask(Surface.Mask.Red, 0x000000ff)) 210 t_fmt = tileset.bits == 24 ? Texture.Format.BGR : Texture.Format.BGRA; 211 212 this._tex.loadFromMemory(tileset.pixels, 213 tileset.width, tileset.height, 214 tileset.bits, t_fmt); 215 } 216 217 this._loadTexCoords(used); 218 } 219 220 private void _compress(ref Surface tileset, scope ShortRect[ushort] used) { 221 debug Log.info("Start compress"); 222 223 const ushort dim = calcDim(used.length, this._tmi.tileWidth); 224 Surface newTileset = Surface.make(dim, dim, 32); 225 226 ShortRect src = ShortRect(0, 0, this._tmi.tileWidth, this._tmi.tileHeight); 227 ushort row = 0; 228 ushort col = 0; 229 230 // ushort c = 0; 231 /// Anpassen der Tile Koordinaten 232 foreach (ref ShortRect dst; used) { 233 Surface clip = tileset.subSurface(dst); 234 // clip.saveToFile("tile_" ~ to!string(c++) ~ ".png"); 235 if (!newTileset.blit(clip, null, &src)) { 236 Log.error("An error occured by blitting the tile on the new tileset: %s", 237 to!string(SDL_GetError())); 238 } 239 240 dst.setPosition(col, row); 241 242 col += this._tmi.tileWidth; 243 if (col >= dim) { 244 col = 0; 245 row += this._tmi.tileHeight; 246 } 247 248 src.setPosition(col, row); 249 } 250 251 // newTileset.saveToFile("new_tilset.png"); 252 253 Texture.Format t_fmt = Texture.Format.None; 254 if (!newTileset.isMask(Surface.Mask.Red, 0x000000ff)) 255 t_fmt = newTileset.bits == 24 ? Texture.Format.BGR : Texture.Format.BGRA; 256 257 this._tex.loadFromMemory(newTileset.pixels, 258 newTileset.width, newTileset.height, 259 newTileset.bits, t_fmt); 260 261 debug Log.info("End compress"); 262 } 263 264 void _loadTexCoords(in ShortRect[ushort] used) { 265 /// Sammeln der Textur Koordinaten 266 267 scope Vector2f[] texCoords; 268 const size_t cap = texCoords.reserve(this._tiles.length * 4); 269 270 debug Log.info("TileMap: Reserve %d texCoords (Needed %d).", cap, this._tiles.length * 4); 271 272 const float tsw = this._tex.width; 273 const float tsh = this._tex.height; 274 const float tw = this._tmi.tileWidth; 275 const float th = this._tmi.tileHeight; 276 277 foreach (ref const Tile t; this._tiles) { 278 const ShortRect* src = &used[t.gid]; 279 280 float tx = src.x; 281 float ty = src.y; 282 283 texCoords ~= Vector2f(tx > 0 ? (tx / tsw) : tx, ty > 0 ? (ty / tsh) : ty); /// #1 284 texCoords ~= Vector2f((tx + tw) / tsw, ty > 0 ? (ty / tsh) : ty); /// #2 285 texCoords ~= Vector2f(tx > 0 ? (tx / tsw) : tx, (ty + th) / tsh); /// #3 286 texCoords ~= Vector2f((tx + tw) / tsw, (ty + th) / tsh); /// #4 287 } 288 289 this._vbo.bind(Target.TexCoords); 290 291 if (!this._vbo.isCurrentEmpty()) 292 this._vbo.modify(&texCoords[0], texCoords.length * Vector2f.sizeof); 293 else 294 this._vbo.cache(&texCoords[0], texCoords.length * Vector2f.sizeof); 295 296 this._vbo.unbind(); 297 } 298 299 void _applyViewport() const { 300 if (!this._view.isEmpty()) { 301 if (!glIsEnabled(GL_SCISSOR_TEST)) 302 glEnable(GL_SCISSOR_TEST); 303 304 const int vx = this._view.x + cast(int) super.position.x; 305 const int vy = this._view.y + this._view.height + cast(int) super.position.y; 306 307 SDL_Window* wnd = SDL_GL_GetCurrentWindow(); 308 int w, h; 309 SDL_GetWindowSize(wnd, &w, &h); 310 311 glScissor(vx, h - vy, this._view.width, this._view.height); 312 } 313 } 314 315 void _render() in { 316 assert(this._transform !is null, "Transform is null."); 317 } body { 318 if (!glIsEnabled(GL_TEXTURE_2D)) 319 glEnable(GL_TEXTURE_2D); 320 321 glPushAttrib(GL_ENABLE_BIT); 322 scope(exit) glPopAttrib(); 323 324 glPushMatrix(); 325 scope(exit) glPopMatrix(); 326 327 glDisable(GL_BLEND); 328 scope(exit) glEnable(GL_BLEND); 329 330 this._applyViewport(); 331 super._applyTranslation(); 332 333 this._vbo.bindTexture(this._tex); 334 this._vbo.drawArrays(Shape.Type.TriangleStrip, this._tiles.length * 4); 335 336 this._vbo.disableAllStates(); 337 this._vbo.unbind(); 338 } 339 340 protected: 341 ShortRect _view; 342 TileMapInfo _tmi; 343 Texture _tex; 344 345 Tile[] _tiles; 346 347 string _filename; 348 bool _doCompress; 349 350 VertexBufferObject _vbo; 351 352 public: 353 final: 354 /** 355 * CTor 356 * 357 * If compress is true, only the needed Tiles are stored 358 * (which means that are new tileset is created which contains only the needed tiles) 359 * otherwise the whole tileset is taken. 360 */ 361 this(string filename, bool compress = true) { 362 this._tex = new Texture(); 363 this._vbo = new VertexBufferObject(Target.Vertex | Target.TexCoords); 364 365 this.load(filename, compress); 366 } 367 368 /** 369 * Calculate, store and return the center point. 370 */ 371 override ref const(Vector2s) calculateCenter() pure nothrow { 372 super.setCenter(this._tmi.width / 2, this._tmi.height / 2); 373 374 return super.getCenter(); 375 } 376 377 /** 378 * Fetch the viewport pointer so that it can modified outside. 379 */ 380 inout(ShortRect*) fetchView() inout pure nothrow { 381 return &this._view; 382 } 383 384 /** 385 * Set a new view. 386 */ 387 void setView(short x, short y, short w, short h) pure nothrow { 388 this._view.set(x, y, w, h); 389 } 390 391 /** 392 * Set a new view. 393 */ 394 void setView(ref const ShortRect view) { 395 this._view = view; 396 } 397 398 /** 399 * Rvalue version. 400 */ 401 void setView(const ShortRect view) { 402 this.setView(view); 403 } 404 405 /** 406 * Reset the viewport. 407 */ 408 void resetView() pure nothrow { 409 this._view.collapse(); 410 } 411 412 /** 413 * Adjust the viewport. 414 * The position is shifted about <code>view.x * -1</code> and <code>view.y - 1</code> 415 * so that the left upper corner of the current view is in the left upper corner of the Window. 416 */ 417 void adjustView() { 418 super.setPosition(this._view.x * -1, this._view.y * -1); 419 } 420 421 /** 422 * Load a new TileMap 423 */ 424 void load(string filename, bool compress = true) { 425 if (!exists(filename)) 426 Log.error("Could not find tilemap " ~ filename); 427 428 this._filename = filename; 429 this._doCompress = compress; 430 431 this._vbo.depleteAll(); 432 433 if (this._tiles.length != 0) { 434 .destroy(this._tmi); 435 this._tiles = null; 436 } 437 438 this._readTileMap(); 439 } 440 441 /** 442 * If compress is true, only the needed Tiles are stored 443 * (which means that are new tileset is created which contains only the needed tiles) 444 * otherwise the whole tileset is taken. 445 */ 446 @property 447 bool doCompress() const pure nothrow { 448 return this._doCompress; 449 } 450 451 /** 452 * Convert from pixel coordinates to tile coordinates. 453 */ 454 Vector2s convertCoords(float cx, float cy) const { 455 short x = cx >= this._tmi.tileWidth ? cast(short) .round(cx / this._tmi.tileWidth) : 0; 456 short y = cy >= this._tmi.tileHeight ? cast(short) .floor(cy / this._tmi.tileHeight) : 0; 457 458 return Vector2s(x, y); 459 } 460 461 /** 462 * Convert from pixel coordinates to tile coordinates. 463 */ 464 Vector2s convertCoords(ref const Vector2f vec) const { 465 return this.convertCoords(vec.x, vec.y); 466 } 467 468 /** 469 * Convert from tile coordinates to pixel coordinates. 470 */ 471 Vector2s reconvertCoords(float cx, float cy) const { 472 short x = cx != 0 ? cast(short) round(cx * this._tmi.tileWidth) : 0; 473 short y = cy != 0 ? cast(short) floor(cy * this._tmi.tileHeight) : 0; 474 475 return Vector2s(x, y); 476 } 477 478 /** 479 * Convert from tile coordinates to pixel coordinates. 480 */ 481 Vector2s reconvertCoords(ref const Vector2f vec) const { 482 return this.reconvertCoords(vec.x, vec.y); 483 } 484 485 /** 486 * Adjusted pixel coordinates so that they lie on valid pixel 487 * coordinates based on tile coordinates. 488 */ 489 Vector2s adjustCoords(float cx, float cy) const { 490 const Vector2s convCoords = this.convertCoords(cx, cy); 491 492 return this.reconvertCoords(convCoords.x, convCoords.y); 493 } 494 495 /** 496 * Adjusted pixel coordinates so that they lie on valid pixel coordinates 497 * based on tile coordinates. 498 */ 499 Vector2s adjustCoords(ref const Vector2f vec) const { 500 return this.adjustCoords(vec.x, vec.y); 501 } 502 503 /** 504 * Reload multiple tiles. 505 * The length of coords must be equal to the length of newCoords. 506 * 507 * See: reload for one tile 508 */ 509 void reload(const Vector2s[] coords, const Vector2s[] newCoords) in { 510 assert(coords.length == newCoords.length, 511 "Koordinaten Arrays must have a equal length."); 512 } body { 513 this._vbo.bind(Target.TexCoords); 514 scope(exit) this._vbo.unbind(); 515 516 float* buffer = cast(float*) this._vbo.map(VertexBufferObject.Access.Read); 517 this._vbo.unmap(); 518 519 foreach (uint index, ref const Vector2s coord; coords) { 520 uint srcGid = coord.x * (coord.y + 1) + coord.y; 521 srcGid *= 8; 522 uint dstGid = newCoords[index].x * (newCoords[index].y + 1) + newCoords[index].y; 523 dstGid *= 8; 524 525 buffer[srcGid .. srcGid + 8] = buffer[dstGid .. dstGid + 8]; 526 527 this.replaceTileAt(coord, this.getTileAt(newCoords[index])); 528 } 529 } 530 531 /** 532 * Replace multiple tiles with another. 533 */ 534 void reload(const Vector2s[] coords, ref const Vector2s newCoord) { 535 Tile tile = this.getTileAt(newCoord); 536 537 foreach (ref const Vector2s coord; coords) { 538 this.reload(coord, newCoord); 539 this.replaceTileAt(coord, tile); 540 } 541 } 542 543 /** 544 * Rvalue version 545 */ 546 void reload(const Vector2s[] coords, const Vector2s newCoord) { 547 this.reload(coords, newCoord); 548 } 549 550 /** 551 * Reload one tile, which means that the tile on the coordinates coord 552 * is replaced with the tile (and the tile surface) on the coordinates newCoord 553 */ 554 void reload(ref const Vector2s coord, ref const Vector2s newCoord) { 555 this._vbo.bind(Target.TexCoords); 556 scope(exit) this._vbo.unbind(); 557 558 float* buffer = cast(float*) this._vbo.map(VertexBufferObject.Access.Read); 559 this._vbo.unmap(); 560 561 uint srcGid = coord.x * (coord.y + 1) + coord.y; 562 srcGid *= 8; 563 uint dstGid = newCoord.x * (newCoord.y + 1) + newCoord.y; 564 dstGid *= 8; 565 566 buffer[srcGid .. srcGid + 8] = buffer[dstGid .. dstGid + 8]; 567 568 this.replaceTileAt(coord, this.getTileAt(newCoord)); 569 } 570 571 /** 572 * Rvalue version 573 */ 574 void reload(const Vector2s coord, const Vector2s newCoord) { 575 this.reload(coord, newCoord); 576 } 577 578 /** 579 * Exchange the tileset 580 */ 581 void exchangeTileset(Texture tex) { 582 this._tex = tex; 583 } 584 585 /** 586 * Returns all containing tiles 587 */ 588 inout(Tile[]) getTiles() inout pure nothrow { 589 return this._tiles; 590 } 591 592 /** 593 * Check whether a tile exist on the given Coordinates. 594 * If idx isn't null, the calculated index of the Tile at the given position is stored there. 595 * 596 * Note: The position must be in tile coordinates, not pixel coordinates. 597 */ 598 bool isTileAt(ref const Vector2s vec, uint* idx = null) const pure nothrow { 599 return this.isTileAt(vec.x, vec.y, idx); 600 } 601 602 /** 603 * Check whether a tile exist on the given Coordinates. 604 * If idx isn't null, the calculated index of the Tile at the given position is stored there. 605 * 606 * Note: The position must be in tile coordinates, not pixel coordinates. 607 */ 608 bool isTileAt(short x, short y, uint* idx = null) const pure nothrow { 609 uint index = y * this._tmi.mapSize[0] + x; 610 if (idx) 611 *idx = index; 612 613 return index < this._tiles.length; 614 } 615 616 /** 617 * Replace the tile at the given position with the given new Tile. 618 * If oldtile is not null, the former Tile is stored there. 619 * 620 * Note: This method is designated as helper method for reload 621 * Note: The position must be in tile coordinates, not pixel coordinates. 622 */ 623 void replaceTileAt(ref const Vector2s vec, Tile newTile, Tile* oldTile = null) { 624 this.replaceTileAt(vec.x, vec.y, newTile, oldTile); 625 } 626 627 /** 628 * Replace the tile at the given position with the given new Tile. 629 * If oldtile is not null, the former Tile is stored there. 630 * 631 * Note: This method is designated as helper method for reload 632 * Note: The position must be in tile coordinates, not pixel coordinates. 633 */ 634 void replaceTileAt(short x, short y, ref Tile newTile, Tile* oldTile = null) { 635 uint index = 0; 636 637 if (this.isTileAt(x, y, &index)) { 638 if (oldTile) 639 .memcpy(oldTile, &this._tiles[index], Tile.sizeof); 640 641 this._tiles[index] = newTile; 642 } 643 } 644 645 /** 646 * Returns the tile at the given position, or throw an Exception 647 * Note: The position must be in tile coordinates, not pixel coordinates. 648 */ 649 Tile getTileAt(ref const Vector2s vec) const { 650 return this.getTileAt(vec.x, vec.y); 651 } 652 653 /** 654 * Returns the tile at the given position, or throw an Exception 655 * Note: The position must be in tile coordinates, not pixel coordinates. 656 */ 657 Tile getTileAt(short x, short y) const { 658 uint index = 0; 659 if (!this.isTileAt(x, y, &index)) 660 Log.error("No Tile at position %d:%d", x, y); 661 662 return this._tiles[index]; 663 } 664 665 /** 666 * Returns the information structure of this tilemap 667 */ 668 ref const(TileMapInfo) getInfo() const pure nothrow { 669 return this._tmi; 670 } 671 672 /** 673 * Returns the .xml filename of this tilemap 674 */ 675 string getFilename() const pure nothrow { 676 return this._filename; 677 } 678 }