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; 83 import std.traits : isFloatingPoint, Select; 84 85 /* if T is not floating, cast to float for operation. 86 * this ensures that an integral value below zero is rounded more negative, 87 * so anything even slightly out of bounds gets a negative coord. 88 */ 89 alias F = Select!(isFloatingPoint!T, T, float); 90 91 // we need to cast the result back to the integral coordinate type 92 alias coord_t = typeof(RowCol.row); 93 94 return RowCol(cast(coord_t) floor(pos.y / cast(F) tileHeight), 95 cast(coord_t) floor(pos.x / cast(F) tileWidth)); 96 } 97 98 /// 99 unittest { 100 struct Vec { float x, y; } 101 102 // 5x3 map, rows from 0 to 4, cols from 0 to 2 103 auto tiles = [ 104 [ 00, 01, 02, 03, 04, ], 105 [ 10, 11, 12, 13, 14, ], 106 [ 20, 21, 22, 23, 24, ], 107 ]; 108 auto map = OrthoMap!int(tiles, 32, 32); 109 110 assert(map.coordAtPoint(Vec(0 , 0 )) == RowCol(0 , 0 )); 111 assert(map.coordAtPoint(Vec(16 , 16 )) == RowCol(0 , 0 )); 112 assert(map.coordAtPoint(Vec(32 , 0 )) == RowCol(0 , 1 )); 113 assert(map.coordAtPoint(Vec(0 , 45 )) == RowCol(1 , 0 )); 114 assert(map.coordAtPoint(Vec(105 , 170)) == RowCol(5 , 3 )); 115 assert(map.coordAtPoint(Vec(-10 , 0 )) == RowCol(0 , -1)); 116 assert(map.coordAtPoint(Vec(-32 , -33)) == RowCol(-2 , -1)); 117 } 118 119 // test with an int pixel coord type 120 unittest { 121 struct Vec { int x, y; } 122 123 // 5x3 map, rows from 0 to 4, cols from 0 to 2 124 auto tiles = [ 125 [ 00, 01, 02, 03, 04, ], 126 [ 10, 11, 12, 13, 14, ], 127 [ 20, 21, 22, 23, 24, ], 128 ]; 129 auto map = OrthoMap!int(tiles, 32, 32); 130 131 assert(map.coordAtPoint(Vec(0 , 0 )) == RowCol(0 , 0 )); 132 assert(map.coordAtPoint(Vec(16 , 16 )) == RowCol(0 , 0 )); 133 assert(map.coordAtPoint(Vec(32 , 0 )) == RowCol(0 , 1 )); 134 assert(map.coordAtPoint(Vec(0 , 45 )) == RowCol(1 , 0 )); 135 assert(map.coordAtPoint(Vec(105 , 170)) == RowCol(5 , 3 )); 136 assert(map.coordAtPoint(Vec(-10 , 0 )) == RowCol(0 , -1)); 137 assert(map.coordAtPoint(Vec(-32 , -33)) == RowCol(-2 , -1)); 138 } 139 140 /** 141 * True if the pixel position is within the map bounds. 142 */ 143 bool containsPoint(T)(T pos) if (isPixelCoord!T) { 144 return grid.contains(coordAtPoint(pos)); 145 } 146 147 /// 148 unittest { 149 // 3x5 map, pixel bounds are [0, 0, 160, 96] (32*3 = 96, 32*5 = 160) 150 auto grid = [ 151 [ 00, 01, 02, 03, 04, ], 152 [ 10, 11, 12, 13, 14, ], 153 [ 20, 21, 22, 23, 24, ], 154 ]; 155 auto map = OrthoMap!int(grid, 32, 32); 156 157 assert( map.containsPoint(PixelCoord( 0, 0))); // top left 158 assert( map.containsPoint(PixelCoord( 159, 95))); // bottom right 159 assert( map.containsPoint(PixelCoord( 80, 48))); // center 160 assert(!map.containsPoint(PixelCoord( 0, 96))); // beyond right border 161 assert(!map.containsPoint(PixelCoord( 160, 0))); // beyond bottom border 162 assert(!map.containsPoint(PixelCoord( 0, -0.5))); // beyond left border 163 assert(!map.containsPoint(PixelCoord(-0.5, 0))); // beyond top border 164 } 165 166 /** 167 * Get the tile at a given pixel position on the map. Throws if out of bounds. 168 * Params: 169 * T = any pixel-positional point (see isPixelCoord). 170 * pos = pixel location in 2D space 171 */ 172 ref Tile tileAtPoint(T)(T pos) if (isPixelCoord!T) { 173 assert(containsPoint(pos), "position %d,%d out of map bounds: ".format(pos.x, pos.y)); 174 return grid.tileAt(coordAtPoint(pos)); 175 } 176 177 /// 178 unittest { 179 auto grid = [ 180 [ 00, 01, 02, 03, 04, ], 181 [ 10, 11, 12, 13, 14, ], 182 [ 20, 21, 22, 23, 24, ], 183 ]; 184 185 auto map = OrthoMap!int(grid, 32, 32); 186 187 assert(map.tileAtPoint(PixelCoord( 0, 0)) == 00); // corner of top left tile 188 assert(map.tileAtPoint(PixelCoord( 16, 30)) == 00); // inside top left tile 189 assert(map.tileAtPoint(PixelCoord(149, 95)) == 24); // inside bottom right tile 190 } 191 192 /** 193 * Get the pixel offset of the top-left corner of the tile at the given coord. 194 * 195 * Params: 196 * coord = grid location of tile. 197 */ 198 PixelCoord tileOffset(RowCol coord) { 199 return PixelCoord(coord.col * tileWidth, 200 coord.row * tileHeight); 201 } 202 203 /// 204 unittest { 205 // 2 rows, 3 cols, 32x64 tiles 206 auto grid = [ 207 [ 00, 01, 02, ], 208 [ 10, 11, 12, ], 209 ]; 210 auto myMap = OrthoMap!int(grid, 32, 64); 211 212 assert(myMap.tileOffset(RowCol(0, 0)) == PixelCoord(0, 0)); 213 assert(myMap.tileOffset(RowCol(1, 2)) == PixelCoord(64, 64)); 214 } 215 216 /** 217 * Get the pixel offset of the center of the tile at the given coord. 218 * 219 * Params: 220 * coord = grid location of tile. 221 */ 222 PixelCoord tileCenter(RowCol coord) { 223 return PixelCoord(coord.col * tileWidth + tileWidth / 2, 224 coord.row * tileHeight + tileHeight / 2); 225 } 226 227 /// 228 unittest { 229 // 2 rows, 3 cols, 32x64 tiles 230 auto grid = [ 231 [ 00, 01, 02, ], 232 [ 10, 11, 12, ], 233 ]; 234 auto myMap = OrthoMap!int(grid, 32, 64); 235 236 assert(myMap.tileCenter(RowCol(0, 0)) == PixelCoord(16, 32)); 237 assert(myMap.tileCenter(RowCol(1, 2)) == PixelCoord(80, 96)); 238 } 239 } 240 241 /// Foreach over every tile in the map 242 unittest { 243 import std.algorithm : equal; 244 245 auto grid = [ 246 [ 00, 01, 02, ], 247 [ 10, 11, 12, ], 248 ]; 249 auto myMap = OrthoMap!int(grid, 32, 64); 250 251 int[] result; 252 253 foreach(tile ; myMap) result ~= tile; 254 255 assert(result.equal([ 00, 01, 02, 10, 11, 12 ])); 256 } 257 258 /// Use ref with foreach to modify tiles 259 unittest { 260 auto grid = [ 261 [ 00, 01, 02, ], 262 [ 10, 11, 12, ], 263 ]; 264 auto myMap = OrthoMap!int(grid, 32, 64); 265 266 foreach(ref tile ; myMap) tile += 30; 267 268 assert(myMap.tileAt(RowCol(1,1)) == 41); 269 } 270 271 /// Foreach over every (coord, tile) pair in the map 272 unittest { 273 import std.algorithm : equal; 274 275 auto grid = [ 276 [ 00, 01, 02, ], 277 [ 10, 11, 12, ], 278 ]; 279 auto myMap = OrthoMap!int(grid, 32, 64); 280 281 282 foreach(coord, tile ; myMap) assert(myMap.tileAt(coord) == tile); 283 }