1 module dgt.tilemap; 2 import dgt.array, dgt.geom; 3 import std.math; 4 5 /** 6 A single tile with an arbitrary value and if the tile is solid or not 7 8 A solid tile will indicate its square is not empty 9 */ 10 struct Tile(T) 11 { 12 T value; 13 bool solid; 14 } 15 16 /** 17 A fixed-size grid of tiles that can be queried 18 */ 19 struct Tilemap(T) 20 { 21 static immutable INVALID_TILE = Tile!T(T(), true); 22 23 private Array!(Tile!T) buffer; 24 private int size, _width, _height; 25 26 @nogc nothrow public: 27 ///Create a tilemap with a given unit width and height and the units for the size of each tile square 28 this(in int mapWidth, in int mapHeight, in int size) 29 { 30 this._width = mapWidth; 31 this._height = mapHeight; 32 this.size = size; 33 buffer = Array!(Tile!T)((width / size) * (height / size)); 34 for(size_t i = 0; i < width; i += size) 35 for(size_t j = 0; j < height; j += size) 36 buffer.add(Tile!T(T(), false)); 37 } 38 39 ///Free the underlying tilemap memory 40 void destroy() 41 { 42 buffer.destroy(); 43 } 44 45 pure: 46 ///Get a tile from a location 47 Tile!T opIndex(in float x, in float y) const 48 { 49 return valid(x, y) ? buffer[cast(int)((x / size) * height / size + (y / size))] : INVALID_TILE; 50 } 51 ///Get a tile from a location 52 Tile!T opIndex(in Vector vec) const { return this[vec.x, vec.y]; } 53 54 ///Set a tile from a location 55 ref Tile!T opIndexAssign(in Tile!T tile, in float x, in float y) 56 { 57 return buffer[cast(int)((x / size) * height / size + (y / size))] = tile; 58 } 59 ///Set a tile from a location 60 ref Tile!T opIndexAssign(in Tile!T tile, in Vector vec) { return this[vec.x, vec.y] = tile; } 61 62 ///Checks if a point falls within a tilemap 63 bool valid(in float x, in float y) const 64 { 65 return x >= 0 && y >= 0 && x < width && y < height; 66 } 67 ///Checks if a point falls within a tilemap 68 bool valid(in Vector vec) const { return valid(vec.x, vec.y); } 69 70 ///Checks if a point is both valid and empty 71 bool empty(in float x, in float y) const 72 { 73 return !this[x, y].solid; 74 } 75 ///Checks if a point is both valid and empty 76 bool empty(in Vector vec) const { return empty(vec.x, vec.y); } 77 78 ///Checks if a region is both valid and empty 79 bool empty(in float x, in float y, in float width, in float height) const 80 { 81 for(float i = x; i < x + width; i += size) 82 for(float j = y; j < y + height; j += size) 83 if(!empty(i, j)) 84 return false; 85 return empty(x + width, y) && empty(x, y + height) && empty(x + width, y + height); 86 } 87 ///Checks if a region is both valid and empty 88 bool empty(in Rectangle rect) const { return empty(rect.x, rect.y, rect.width, rect.height); } 89 90 ///Determine the furthest a region can move without hitting a wall 91 Vector slideContact(in float x, in float y, in float width, in float height, in Vector v) const 92 { 93 //Objects embedded in walls cannot move 94 if(!empty(x, y, width, height)) 95 return Vector(0, 0); 96 float tryX = x + v.x; 97 float tryY = y + v.y; 98 //The object can move unobstructed 99 if(empty(tryX, tryY, width, height)) 100 return v; 101 //The left side is embedded in a wall 102 if(!empty(tryX, tryY) && !empty(tryX, tryY + height)) 103 tryX = (cast(int)tryX / size) * size; 104 //The right side is embedded in a wall 105 if(!empty(tryX + width, tryY) && !empty(tryX + width, tryY + height)) 106 tryX = (cast(int)(tryX + width) / size) * size - width; 107 //The tpp side is embedded in a wall 108 if(!empty(tryX, tryY) && !empty(tryX + width, tryY)) 109 tryY = (cast(int)tryY / size) * size; 110 //The bottom side is embedded in a wall 111 if(!empty(tryX, tryY + height) && !empty(tryX + width, tryY + height)) 112 tryY = (cast(int)(tryY + height) / size) * size - height; 113 return Vector(tryX - x, tryY - y); 114 } 115 ///Determine the furthest a region can move without hitting a wall 116 Vector slideContact(in Rectangle rect, in Vector vec) const 117 { 118 return slideContact(rect.x, rect.y, rect.width, rect.height, vec); 119 } 120 121 ///The width of the map in units 122 @property int width() const { return _width; } 123 ///The height of the map in units 124 @property int height() const { return _height; } 125 ///The size of a tile in units (both width and height) 126 @property int tileSize() const { return size; } 127 } 128 129 unittest 130 { 131 Tilemap!int map = Tilemap!int(640, 480, 32); 132 map[35, 35] = Tile!int(5, true); 133 assert(map[-1, 0].solid); 134 assert(!map[35, 0].solid); 135 assert(map[35, 35].value == 5); 136 auto moved = map.slideContact(300, 5, 32, 32, Vector(0, -10)); 137 assert(moved.x == 0 && moved.y == -5); 138 moved = map.slideContact(80, 10, 16, 16, Vector(1, -20)); 139 assert(moved.x == 1 && moved.y == -10); 140 moved = map.slideContact(50, 50, 10, 10, Vector(20, 30)); 141 assert(moved.x == 20 && moved.y == 30); 142 moved = map.slideContact(600, 10, 30, 10, Vector(15, 10)); 143 assert(moved.x == 10 && moved.y == 10); 144 auto movedf = map.slideContact(10.0, 5, 5, 5, Vector(2, 2)); 145 assert(movedf.x == 2 && movedf.y == 2); 146 movedf = map.slideContact(5.0, 5, 10, 10, Vector(-7.2, 0)); 147 assert(movedf.x == -5 && movedf.y == 0); 148 movedf = map.slideContact(600, 0, 25, 10, Vector(20, 0)); 149 import dgt.io; 150 println(movedf); 151 assert(movedf.x == 15 && movedf.y == 0); 152 }