1 /++ 2 Note: much of the functionality of gamehelpers was moved to [arsd.game] on May 3, 2020. 3 If you used that code, change `import arsd.gamehelpers;` to `import arsd.game;` and add 4 game.d to your build (in addition to gamehelpers.d; the new game.d still imports this module) 5 and you should be good to go. 6 7 This module now builds on only [arsd.color] to provide additional algorithm functions 8 and data types that are common in games. 9 10 History: 11 Massive change on May 3, 2020 to move the previous flagship class out and to 12 a new module, [arsd.game], to make this one lighter on dependencies, just 13 containing helpers rather than a consolidated omnibus import. 14 +/ 15 module arsd.gamehelpers; 16 17 deprecated("change `import arsd.gamehelpers;` to `import arsd.game;`") 18 void* create2dWindow(string title, int width = 512, int height = 512) { return null; } 19 20 deprecated("change `import arsd.gamehelpers;` to `import arsd.game;`") 21 class GameHelperBase {} 22 23 import std.math; 24 25 import arsd.color; 26 27 // Some math helpers 28 29 int nextPowerOfTwo(int v) { 30 v--; 31 v |= v >> 1; 32 v |= v >> 2; 33 v |= v >> 4; 34 v |= v >> 8; 35 v |= v >> 16; 36 v++; 37 return v; 38 } 39 40 /++ 41 Calculates the cross product of <u1, u2, u3> and <v1, v2, v3>, putting the result in <s1, s2, s3>. 42 +/ 43 void crossProduct( 44 float u1, float u2, float u3, 45 float v1, float v2, float v3, 46 out float s1, out float s2, out float s3) 47 { 48 s1 = u2 * v3 - u3 * v2; 49 s2 = u3 * v1 - u1 * v3; 50 s3 = u1 * v2 - u2 * v1; 51 } 52 53 /++ 54 3D rotates (x, y, z) theta radians about the axis represented by unit-vector (u, v, w), putting the results in (s1, s2, s3). 55 56 For example, to rotate about the Y axis, pass (0, 1, 0) as (u, v, w). 57 +/ 58 void rotateAboutAxis( 59 float theta, // in RADIANS 60 float x, float y, float z, 61 float u, float v, float w, 62 out float xp, out float yp, out float zp) 63 { 64 xp = u * (u*x + v*y + w*z) * (1 - cos(theta)) + x * cos(theta) + (-w*y + v*z) * sin(theta); 65 yp = v * (u*x + v*y + w*z) * (1 - cos(theta)) + y * cos(theta) + (w*x - u*z) * sin(theta); 66 zp = w * (u*x + v*y + w*z) * (1 - cos(theta)) + z * cos(theta) + (-v*x + u*y) * sin(theta); 67 } 68 69 /++ 70 2D rotates (rotatingX, rotatingY) theta radians about (originX, originY), putting the result in (xp, yp). 71 +/ 72 void rotateAboutPoint( 73 float theta, // in RADIANS 74 float originX, float originY, 75 float rotatingX, float rotatingY, 76 out float xp, out float yp) 77 { 78 if(theta == 0) { 79 xp = rotatingX; 80 yp = rotatingY; 81 return; 82 } 83 84 rotatingX -= originX; 85 rotatingY -= originY; 86 87 float s = sin(theta); 88 float c = cos(theta); 89 90 float x = rotatingX * c - rotatingY * s; 91 float y = rotatingX * s + rotatingY * c; 92 93 xp = x + originX; 94 yp = y + originY; 95 } 96 97 /++ 98 Represents the four basic directions on a grid. You can conveniently use it like: 99 100 --- 101 Point pt = Point(5, 3); 102 pt += Dir.N; // moves up 103 --- 104 105 The opposite direction btw can be gotten with `pt * -1`. 106 107 History: Added May 3, 2020 108 +/ 109 enum Dir { N = Point(0, -1), S = Point(0, 1), W = Point(-1, 0), E = Point(1, 0) } 110 111 /++ 112 The four directions as a static array so you can assign to a local variable 113 then shuffle, etc. 114 115 History: Added May 3, 2020 116 +/ 117 Point[4] directions() { 118 with(Dir) return [N, S, W, E]; 119 } 120 121 /++ 122 A random value off [Dir]. 123 124 History: Added May 3, 2020 125 +/ 126 Point randomDirection() { 127 import std.random; 128 return directions()[uniform(0, 4)]; 129 } 130 131 /++ 132 Cycles through all the directions the given number of times. If you have 133 one cycle, it goes through each direction once in a random order. With two 134 cycles, it will move each direction twice, but in random order - can be 135 W, W, N, E, S, S, N, E, for example; it will not do the cycles in order but 136 upon completion will have gone through them all. 137 138 This can be convenient because if the character's movement is not constrained, 139 it will always return back to where it started after a random movement. 140 141 Returns: an input range of [Point]s. Please note that the current version returns 142 `Point[]`, but I reserve the right to change that in the future; I only promise 143 input range capabilities. 144 145 History: Added May 3, 2020 146 +/ 147 auto randomDirectionCycle(int cycleCount = 1) { 148 Point[] all = new Point[](cycleCount * 4); 149 foreach(c; 0 .. cycleCount) 150 all[c * 4 .. c * 4 + 4] = directions()[]; 151 152 import std.random; 153 return all.randomShuffle; 154 } 155 156 /++ 157 Represents a 2d grid like an array. To encapsulate the whole `[y*width + x]` thing. 158 159 History: 160 Added May 3, 2020 161 +/ 162 struct Grid(T) { 163 private Size size_; 164 private T[] array; 165 166 pure @safe nothrow: 167 168 /// Creates a new GC-backed array 169 this(Size size) { 170 this.size_ = size; 171 array = new T[](size.area); 172 } 173 174 /// ditto 175 this(int width, int height) { 176 this(Size(width, height)); 177 } 178 179 @nogc: 180 181 /// Wraps an existing array. 182 this(T[] array, Size size) { 183 assert(array.length == size.area); 184 this.array = array; 185 this.size_ = size; 186 } 187 188 @property { 189 /// 190 inout(Size) size() inout { return size_; } 191 /// 192 int width() const { return size.width; } 193 /// 194 int height() const { return size.height; } 195 } 196 197 /// Slice operation gives a view into the underlying 1d array. 198 inout(T)[] opIndex() inout { 199 return array; 200 } 201 202 /// 203 ref inout(T) opIndex(int x, int y) inout { 204 return array[y * width + x]; 205 } 206 207 /// 208 ref inout(T) opIndex(const Point pt) inout { 209 return this.opIndex(pt.x, pt.y); 210 } 211 // T[] opSlice 212 213 /// 214 bool inBounds(int x, int y) const { 215 return x >= 0 && y >= 0 && x < width && y < height; 216 } 217 218 /// 219 bool inBounds(const Point pt) const { 220 return inBounds(pt.x, pt.y); 221 } 222 223 /// Supports `if(point in grid) {}` 224 bool opBinaryRight(string op : "in")(Point pt) const { 225 return inBounds(pt); 226 } 227 } 228 229 /++ 230 Directions as a maskable bit flag. 231 232 History: Added May 3, 2020 233 +/ 234 enum DirFlag : ubyte { 235 N = 4, 236 S = 8, 237 W = 1, 238 E = 2 239 } 240 241 /++ 242 History: Added May 3, 2020 243 +/ 244 DirFlag dirFlag(Dir dir) { 245 assert(dir.x >= -1 && dir.x <= 1); 246 assert(dir.y >= -1 && dir.y <= 1); 247 248 249 /+ 250 (-1 + 3) / 2 = 2 / 2 = 1 251 (1 + 3) / 2 = 4 / 2 = 2 252 253 So the al-gore-rhythm is 254 (x + 3) / 2 255 which is aka >> 1 256 or 257 ((y + 3) / 2) << 2 258 which is aka >> 1 << 2 aka << 1 259 So: 260 1 = left 261 2 = right 262 4 = up 263 8 = down 264 +/ 265 266 ubyte dirFlag; 267 if(dir.x) dirFlag |= ((dir.x + 3) >> 1); 268 if(dir.y) dirFlag |= ((dir.y + 3) << 1); 269 return cast(DirFlag) dirFlag; 270 } 271 272 // this is public but like i don't want do document it since it can so easily fail the asserts. 273 DirFlag dirFlag(Point dir) { 274 return dirFlag(cast(Dir) dir); 275 } 276 277 /++ 278 Generates a maze. 279 280 The returned array is a grid of rooms, with a bit flag pattern of directions you can travel from each room. See [DirFlag] for bits. 281 282 History: Added May 3, 2020 283 +/ 284 Grid!ubyte generateMaze(int mazeWidth, int mazeHeight) { 285 import std.random; 286 287 Point[] cells; 288 cells ~= Point(uniform(0, mazeWidth), uniform(0, mazeHeight)); 289 290 auto grid = Grid!ubyte(mazeWidth, mazeHeight); 291 292 Point[4] directions = .directions; 293 294 while(cells.length) { 295 auto index = cells.length - 1; // could also be 0 or uniform or whatever too 296 Point p = cells[index]; 297 bool added; 298 foreach(dir; directions[].randomShuffle) { 299 auto n = p + dir; 300 if(n !in grid) 301 continue; 302 303 if(grid[n]) 304 continue; 305 306 grid[p] |= dirFlag(dir); 307 grid[n] |= dirFlag(dir * -1); 308 309 cells ~= n; 310 311 added = true; 312 break; 313 } 314 315 if(!added) { 316 foreach(i; index .. cells.length - 1) 317 cells[index] = cells[index + 1]; 318 cells = cells[0 .. $-1]; 319 } 320 } 321 322 return grid; 323 } 324 325 326 /++ 327 Implements the A* path finding algorithm on a grid. 328 329 Params: 330 start = starting point on the grid 331 goal = destination point on the grid 332 size = size of the grid 333 isPassable = used to determine if the tile at the given coordinates are passible 334 d = weight function to the A* algorithm. If null, assumes all will be equal weight. Returned value must be greater than or equal to 1. 335 h = heuristic function to the A* algorithm. Gives an estimation of how many steps away the goal is from the given point to speed up the search. If null, assumes "taxicab distance"; the number of steps based solely on distance without diagonal movement. If you want to disable this entirely, pass `p => 0`. 336 Returns: 337 A list of waypoints to reach the destination, or `null` if impossible. The waypoints are returned in reverse order, starting from the goal and working back to the start. 338 339 So to get to the goal from the starting point, follow the returned array in $(B backwards). 340 341 The waypoints will not necessarily include every step but also may not only list turns, but if you follow 342 them you will get get to the destination. 343 344 Bugs: 345 The current implementation uses more memory than it really has to; it will eat like 8 MB of scratch space RAM on a 512x512 grid. 346 347 It doesn't consider wraparound possible so it might ask you to go all the way around the world unnecessarily. 348 349 History: 350 Added May 2, 2020. 351 +/ 352 Point[] pathfind(Point start, Point goal, Size size, scope bool delegate(Point) isPassable, scope int delegate(Point, Point) d = null, scope int delegate(Point) h = null) { 353 354 Point[] reconstruct_path(scope Point[] cameFrom, Point current) { 355 Point[] totalPath; 356 totalPath ~= current; 357 358 auto cf = cameFrom[current.y * size.width + current.x]; 359 while(cf != Point(int.min, int.min)) { 360 current = cf; 361 cf = cameFrom[current.y * size.width + current.x]; 362 totalPath ~= current; 363 } 364 return totalPath; 365 } 366 367 // weighting thing..... 368 static int d_default(Point a, Point b) { 369 return 1; 370 } 371 372 if(d is null) 373 d = (Point a, Point b) => d_default(a, b); 374 375 if(h is null) 376 h = (Point a) { return abs(a.y - goal.x) + abs(a.y - goal.y); }; 377 378 Point[] openSet = [start]; 379 380 Point[] cameFrom = new Point[](size.area); 381 cameFrom[] = Point(int.min, int.min); 382 383 int[] gScore = new int[](size.area); 384 gScore[] = int.max; 385 gScore[start.y * size.width + start.x] = 0; 386 387 int[] fScore = new int[](size.area); 388 fScore[] = int.max; 389 fScore[start.y * size.width + start.x] = h(start); 390 391 while(openSet.length) { 392 Point current; 393 size_t currentIdx; 394 int currentFscore = int.max; 395 foreach(idx, pt; openSet) { 396 auto p = fScore[pt.y * size.width + pt.x]; 397 if(p <= currentFscore) { 398 currentFscore = p; 399 current = pt; 400 currentIdx = idx; 401 } 402 } 403 404 if(current == goal) { 405 /+ 406 import std.stdio; 407 foreach(y; 0 .. size.height) 408 writefln("%(%02d,%)", gScore[y * size.width .. y * size.width + size.width]); 409 +/ 410 return reconstruct_path(cameFrom, current); 411 } 412 413 openSet[currentIdx] = openSet[$-1]; 414 openSet = openSet[0 .. $-1]; 415 416 Point[4] neighborsBuffer; 417 int neighborsBufferLength = 0; 418 419 420 // FIXME: would be kinda cool to make this a more generic graph traversal like for subway routes too 421 422 if(current.x + 1 < size.width && isPassable(current + Point(1, 0))) 423 neighborsBuffer[neighborsBufferLength++] = current + Point(1, 0); 424 if(current.x && isPassable(current + Point(-1, 0))) 425 neighborsBuffer[neighborsBufferLength++] = current + Point(-1, 0); 426 if(current.y && isPassable(current + Point(0, -1))) 427 neighborsBuffer[neighborsBufferLength++] = current + Point(0, -1); 428 if(current.y + 1 < size.height && isPassable(current + Point(0, 1))) 429 neighborsBuffer[neighborsBufferLength++] = current + Point(0, 1); 430 431 foreach(neighbor; neighborsBuffer[0 .. neighborsBufferLength]) { 432 auto tentative_gScore = gScore[current.y * size.width + current.x] + d(current, neighbor); 433 if(tentative_gScore < gScore[neighbor.y * size.width + neighbor.x]) { 434 cameFrom[neighbor.y * size.width + neighbor.x] = current; 435 gScore[neighbor.y * size.width + neighbor.x] = tentative_gScore; 436 fScore[neighbor.y * size.width + neighbor.x] = tentative_gScore + h(neighbor); 437 // this linear thing might not be so smart after all 438 bool found = false; 439 foreach(o; openSet) 440 if(o == neighbor) { found = true; break; } 441 if(!found) 442 openSet ~= neighbor; 443 } 444 } 445 } 446 447 return null; 448 }