1 /** 2 * A map is essentially a grid with additional information about tile positions and sizes. 3 * 4 * Currently, the only map type is `OrthoMap`, but `IsoMap` and `HexMap` may be added in later 5 * versions. 6 * 7 * An `OrthoMap` represents a map of rectangular (usually square) tiles that are arranged 8 * orthogonally. In other words, all tiles in a row are at the same y corrdinate, and all tiles in 9 * a column are at the same x coordinate (as opposed to an Isometric map, where there is an offset). 10 * 11 * An `OrthoMap` provides all of the functionality as `RectGrid`. 12 * It also stores the size of tiles and provides functions to translate between 'grid coordinates' 13 * (row/column) and 'screen coordinates' (x/y pixel positions). 14 * 15 * Authors: <a href="https://github.com/rcorre">rcorre</a> 16 * License: <a href="http://opensource.org/licenses/MIT">MIT</a> 17 * Copyright: Copyright © 2015, Ryan Roden-Corrent 18 */ 19 module dtiled.map; 20 21 import dtiled.coords; 22 import dtiled.grid; 23 24 // need a test here to kickstart the unit tests inside OrthoMap!T 25 unittest { 26 auto map = OrthoMap!int([[1]], 32, 32); 27 } 28 29 /** 30 * Generic Tile Map structure that uses a single layer of tiles in an orthogonal grid. 31 * 32 * This provides a 'flat' representation of multiple tile and object layers. 33 * T can be whatever type you would like to use to represent a single tile within the map. 34 * 35 * An OrthoMap supports all the operations of dtiled.grid for working with RowCol coordinates. 36 * Additionally, it stores information about tile size for operations in pixel coordinate space. 37 */ 38 struct OrthoMap(Tile) { 39 /// The underlying tile grid structure, surfaced with alias this. 40 RectGrid!(Tile[][]) grid; 41 alias grid this; 42 43 private { 44 int _tileWidth; 45 int _tileHeight; 46 } 47 48 /** 49 * Construct an orthogonal tilemap from a rectangular (non-jagged) grid of tiles. 50 * 51 * Params: 52 * tiles = tiles arranged in **row major** order, indexed as tiles[row][col]. 53 * tileWidth = width of each tile in pixels 54 * tileHeight = height of each tile in pixels 55 */ 56 this(Tile[][] tiles, int tileWidth, int tileHeight) { 57 this(rectGrid(tiles), tileWidth, tileHeight); 58 } 59 60 /// ditto 61 this(RectGrid!(Tile[][]) grid, int tileWidth, int tileHeight) { 62 _tileWidth = tileWidth; 63 _tileHeight = tileHeight; 64 65 this.grid = grid; 66 } 67 68 @property { 69 /// Width of each tile in pixels 70 auto tileWidth() { return _tileWidth; } 71 /// Height of each tile in pixels 72 auto tileHeight() { return _tileHeight; } 73 } 74 75 /** 76 * Get the grid location corresponding to a given pixel coordinate. 77 * 78 * If the point is out of map bounds, the returned coord will also be out of bounds. 79 * Use the containsPoint method to check if a point is in bounds. 80 */ 81 auto coordAtPoint(T)(T pos) if (isPixelCoord!T) { 82 import std.math : floor, lround; 83 import std.traits : isFloatingPoint, Select; 84 // if T is not floating, cast to float for operation 85 alias F = Select!(isFloatingPoint!T, T, float); 86 87 RowCol coord; 88 coord.col = floor(pos.x / cast(F) tileWidth).lround; 89 coord.row = floor(pos.y / cast(F) tileHeight).lround; 90 return coord; 91 } 92 93 /// 94 unittest { 95 // 5x3 map, rows from 0 to 4, cols from 0 to 2 96 auto tiles = [ 97 [ 00, 01, 02, 03, 04, ], 98 [ 10, 11, 12, 13, 14, ], 99 [ 20, 21, 22, 23, 24, ], 100 ]; 101 auto map = OrthoMap!int(tiles, 32, 32); 102 103 assert( map.contains(RowCol(0 , 0))); // top left 104 assert( map.contains(RowCol(2 , 4))); // bottom right 105 assert( map.contains(RowCol(1 , 3))); // center 106 assert(!map.contains(RowCol(3 , 0))); // beyond bottom border 107 assert(!map.contains(RowCol(0 , 5))); // beyond right border 108 assert(!map.contains(RowCol(0 ,-1))); // beyond left border 109 assert(!map.contains(RowCol(-1, 0))); // beyond top border 110 } 111 112 /** 113 * True if the pixel position is within the map bounds. 114 */ 115 bool containsPoint(T)(T pos) if (isPixelCoord!T) { 116 return grid.contains(coordAtPoint(pos)); 117 } 118 119 /// 120 unittest { 121 // 3x5 map, pixel bounds are [0, 0, 160, 96] (32*3 = 96, 32*5 = 160) 122 auto grid = [ 123 [ 00, 01, 02, 03, 04, ], 124 [ 10, 11, 12, 13, 14, ], 125 [ 20, 21, 22, 23, 24, ], 126 ]; 127 auto map = OrthoMap!int(grid, 32, 32); 128 129 assert( map.containsPoint(PixelCoord( 0, 0))); // top left 130 assert( map.containsPoint(PixelCoord( 159, 95))); // bottom right 131 assert( map.containsPoint(PixelCoord( 80, 48))); // center 132 assert(!map.containsPoint(PixelCoord( 0, 96))); // beyond right border 133 assert(!map.containsPoint(PixelCoord( 160, 0))); // beyond bottom border 134 assert(!map.containsPoint(PixelCoord( 0, -0.5))); // beyond left border 135 assert(!map.containsPoint(PixelCoord(-0.5, 0))); // beyond top border 136 } 137 138 /** 139 * Get the tile at a given pixel position on the map. Throws if out of bounds. 140 * Params: 141 * T = any pixel-positional point (see isPixelCoord). 142 * pos = pixel location in 2D space 143 */ 144 ref Tile tileAtPoint(T)(T pos) if (isPixelCoord!T) { 145 assert(containsPoint(pos), "position %d,%d out of map bounds: ".format(pos.x, pos.y)); 146 return grid.tileAt(coordAtPoint(pos)); 147 } 148 149 /// 150 unittest { 151 auto grid = [ 152 [ 00, 01, 02, 03, 04, ], 153 [ 10, 11, 12, 13, 14, ], 154 [ 20, 21, 22, 23, 24, ], 155 ]; 156 157 auto map = OrthoMap!int(grid, 32, 32); 158 159 assert(map.tileAtPoint(PixelCoord( 0, 0)) == 00); // corner of top left tile 160 assert(map.tileAtPoint(PixelCoord( 16, 30)) == 00); // inside top left tile 161 assert(map.tileAtPoint(PixelCoord(149, 95)) == 24); // inside bottom right tile 162 } 163 164 /** 165 * Get the pixel offset of the top-left corner of the tile at the given coord. 166 * 167 * Params: 168 * coord = grid location of tile. 169 */ 170 PixelCoord tileOffset(RowCol coord) { 171 return PixelCoord(coord.col * tileWidth, 172 coord.row * tileHeight); 173 } 174 175 /// 176 unittest { 177 // 2 rows, 3 cols, 32x64 tiles 178 auto grid = [ 179 [ 00, 01, 02, ], 180 [ 10, 11, 12, ], 181 ]; 182 auto myMap = OrthoMap!int(grid, 32, 64); 183 184 assert(myMap.tileOffset(RowCol(0, 0)) == PixelCoord(0, 0)); 185 assert(myMap.tileOffset(RowCol(1, 2)) == PixelCoord(64, 64)); 186 } 187 188 /** 189 * Get the pixel offset of the center of the tile at the given coord. 190 * 191 * Params: 192 * coord = grid location of tile. 193 */ 194 PixelCoord tileCenter(RowCol coord) { 195 return PixelCoord(coord.col * tileWidth + tileWidth / 2, 196 coord.row * tileHeight + tileHeight / 2); 197 } 198 199 /// 200 unittest { 201 // 2 rows, 3 cols, 32x64 tiles 202 auto grid = [ 203 [ 00, 01, 02, ], 204 [ 10, 11, 12, ], 205 ]; 206 auto myMap = OrthoMap!int(grid, 32, 64); 207 208 assert(myMap.tileCenter(RowCol(0, 0)) == PixelCoord(16, 32)); 209 assert(myMap.tileCenter(RowCol(1, 2)) == PixelCoord(80, 96)); 210 } 211 } 212 213 /// Foreach over every tile in the map 214 unittest { 215 import std.algorithm : equal; 216 217 auto grid = [ 218 [ 00, 01, 02, ], 219 [ 10, 11, 12, ], 220 ]; 221 auto myMap = OrthoMap!int(grid, 32, 64); 222 223 int[] result; 224 225 foreach(tile ; myMap) result ~= tile; 226 227 assert(result.equal([ 00, 01, 02, 10, 11, 12 ])); 228 } 229 230 /// Use ref with foreach to modify tiles 231 unittest { 232 auto grid = [ 233 [ 00, 01, 02, ], 234 [ 10, 11, 12, ], 235 ]; 236 auto myMap = OrthoMap!int(grid, 32, 64); 237 238 foreach(ref tile ; myMap) tile += 30; 239 240 assert(myMap.tileAt(RowCol(1,1)) == 41); 241 } 242 243 /// Foreach over every (coord, tile) pair in the map 244 unittest { 245 import std.algorithm : equal; 246 247 auto grid = [ 248 [ 00, 01, 02, ], 249 [ 10, 11, 12, ], 250 ]; 251 auto myMap = OrthoMap!int(grid, 32, 64); 252 253 254 foreach(coord, tile ; myMap) assert(myMap.tileAt(coord) == tile); 255 }