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: 108 Added May 3, 2020 109 110 The `Direction` alias was added November 29, 2025 111 +/ 112 enum Dir { N = Point(0, -1), S = Point(0, 1), W = Point(-1, 0), E = Point(1, 0) } 113 114 /// ditto 115 alias Direction = Dir; 116 117 /++ 118 The four directions as a static array so you can assign to a local variable 119 then shuffle, etc. 120 121 History: Added May 3, 2020 122 +/ 123 Point[4] directions() { 124 with(Dir) return [N, S, W, E]; 125 } 126 127 /++ 128 A random value off [Dir]. 129 130 History: Added May 3, 2020 131 +/ 132 Point randomDirection() { 133 import std.random; 134 return directions()[uniform(0, 4)]; 135 } 136 137 /++ 138 Cycles through all the directions the given number of times. If you have 139 one cycle, it goes through each direction once in a random order. With two 140 cycles, it will move each direction twice, but in random order - can be 141 W, W, N, E, S, S, N, E, for example; it will not do the cycles in order but 142 upon completion will have gone through them all. 143 144 This can be convenient because if the character's movement is not constrained, 145 it will always return back to where it started after a random movement. 146 147 Returns: an input range of [Point]s. Please note that the current version returns 148 `Point[]`, but I reserve the right to change that in the future; I only promise 149 input range capabilities. 150 151 History: Added May 3, 2020 152 +/ 153 auto randomDirectionCycle(int cycleCount = 1) { 154 Point[] all = new Point[](cycleCount * 4); 155 foreach(c; 0 .. cycleCount) 156 all[c * 4 .. c * 4 + 4] = directions()[]; 157 158 import std.random; 159 return all.randomShuffle; 160 } 161 162 /++ 163 Represents a 2d grid like an array. To encapsulate the whole `[y*width + x]` thing. 164 165 History: 166 Added May 3, 2020 167 +/ 168 struct Grid(T) { 169 private Size size_; 170 private T[] array; 171 172 pure @safe nothrow: 173 174 /// Creates a new GC-backed array 175 this(Size size) { 176 this.size_ = size; 177 array = new T[](size.area); 178 } 179 180 /// ditto 181 this(int width, int height) { 182 this(Size(width, height)); 183 } 184 185 @nogc: 186 187 /// Wraps an existing array. 188 this(T[] array, Size size) { 189 assert(array.length == size.area); 190 this.array = array; 191 this.size_ = size; 192 } 193 194 @property { 195 /// 196 inout(Size) size() inout { return size_; } 197 /// 198 int width() const { return size.width; } 199 /// 200 int height() const { return size.height; } 201 } 202 203 /// Slice operation gives a view into the underlying 1d array. 204 inout(T)[] opIndex() inout { 205 return array; 206 } 207 208 /// 209 ref inout(T) opIndex(int x, int y) inout { 210 return array[y * width + x]; 211 } 212 213 /// 214 ref inout(T) opIndex(const Point pt) inout { 215 return this.opIndex(pt.x, pt.y); 216 } 217 // T[] opSlice 218 219 /// 220 bool inBounds(int x, int y) const { 221 return x >= 0 && y >= 0 && x < width && y < height; 222 } 223 224 /// 225 bool inBounds(const Point pt) const { 226 return inBounds(pt.x, pt.y); 227 } 228 229 /// Supports `if(point in grid) {}` 230 bool opBinaryRight(string op : "in")(Point pt) const { 231 return inBounds(pt); 232 } 233 } 234 235 /++ 236 Directions as a maskable bit flag. 237 238 History: Added May 3, 2020 239 +/ 240 enum DirFlag : ubyte { 241 N = 4, 242 S = 8, 243 W = 1, 244 E = 2 245 } 246 247 /++ 248 History: Added May 3, 2020 249 +/ 250 DirFlag dirFlag(Dir dir) { 251 assert(dir.x >= -1 && dir.x <= 1); 252 assert(dir.y >= -1 && dir.y <= 1); 253 254 255 /+ 256 (-1 + 3) / 2 = 2 / 2 = 1 257 (1 + 3) / 2 = 4 / 2 = 2 258 259 So the al-gore-rhythm is 260 (x + 3) / 2 261 which is aka >> 1 262 or 263 ((y + 3) / 2) << 2 264 which is aka >> 1 << 2 aka << 1 265 So: 266 1 = left 267 2 = right 268 4 = up 269 8 = down 270 +/ 271 272 ubyte dirFlag; 273 if(dir.x) dirFlag |= ((dir.x + 3) >> 1); 274 if(dir.y) dirFlag |= ((dir.y + 3) << 1); 275 return cast(DirFlag) dirFlag; 276 } 277 278 // this is public but like i don't want do document it since it can so easily fail the asserts. 279 DirFlag dirFlag(Point dir) { 280 return dirFlag(*cast(Dir*) &dir); 281 } 282 283 /++ 284 Generates a maze. 285 286 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. 287 288 History: Added May 3, 2020 289 +/ 290 Grid!ubyte generateMaze(int mazeWidth, int mazeHeight) { 291 import std.random; 292 293 Point[] cells; 294 cells ~= Point(uniform(0, mazeWidth), uniform(0, mazeHeight)); 295 296 auto grid = Grid!ubyte(mazeWidth, mazeHeight); 297 298 Point[4] directions = .directions; 299 300 while(cells.length) { 301 auto index = cells.length - 1; // could also be 0 or uniform or whatever too 302 Point p = cells[index]; 303 bool added; 304 foreach(dir; directions[].randomShuffle) { 305 auto n = p + dir; 306 if(n !in grid) 307 continue; 308 309 if(grid[n]) 310 continue; 311 312 grid[p] |= dirFlag(dir); 313 grid[n] |= dirFlag(dir * -1); 314 315 cells ~= n; 316 317 added = true; 318 break; 319 } 320 321 if(!added) { 322 foreach(i; index .. cells.length - 1) 323 cells[index] = cells[index + 1]; 324 cells = cells[0 .. $-1]; 325 } 326 } 327 328 return grid; 329 } 330 331 332 /++ 333 Implements the A* path finding algorithm on a grid. 334 335 Params: 336 start = starting point on the grid 337 goal = destination point on the grid 338 size = size of the grid 339 isPassable = used to determine if the tile at the given coordinates are passible 340 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. 341 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`. 342 Returns: 343 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. 344 345 So to get to the goal from the starting point, follow the returned array in $(B backwards). 346 347 The waypoints will not necessarily include every step but also may not only list turns, but if you follow 348 them you will get get to the destination. 349 350 Bugs: 351 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. 352 353 It doesn't consider wraparound possible so it might ask you to go all the way around the world unnecessarily. 354 355 History: 356 Added May 2, 2020. 357 +/ 358 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) { 359 360 Point[] reconstruct_path(scope Point[] cameFrom, Point current) { 361 Point[] totalPath; 362 totalPath ~= current; 363 364 auto cf = cameFrom[current.y * size.width + current.x]; 365 while(cf != Point(int.min, int.min)) { 366 current = cf; 367 cf = cameFrom[current.y * size.width + current.x]; 368 totalPath ~= current; 369 } 370 return totalPath; 371 } 372 373 // weighting thing..... 374 static int d_default(Point a, Point b) { 375 return 1; 376 } 377 378 if(d is null) 379 d = (Point a, Point b) => d_default(a, b); 380 381 if(h is null) 382 h = (Point a) { return abs(a.y - goal.x) + abs(a.y - goal.y); }; 383 384 Point[] openSet = [start]; 385 386 Point[] cameFrom = new Point[](size.area); 387 cameFrom[] = Point(int.min, int.min); 388 389 int[] gScore = new int[](size.area); 390 gScore[] = int.max; 391 gScore[start.y * size.width + start.x] = 0; 392 393 int[] fScore = new int[](size.area); 394 fScore[] = int.max; 395 fScore[start.y * size.width + start.x] = h(start); 396 397 while(openSet.length) { 398 Point current; 399 size_t currentIdx; 400 int currentFscore = int.max; 401 foreach(idx, pt; openSet) { 402 auto p = fScore[pt.y * size.width + pt.x]; 403 if(p <= currentFscore) { 404 currentFscore = p; 405 current = pt; 406 currentIdx = idx; 407 } 408 } 409 410 if(current == goal) { 411 /+ 412 import std.stdio; 413 foreach(y; 0 .. size.height) 414 writefln("%(%02d,%)", gScore[y * size.width .. y * size.width + size.width]); 415 +/ 416 return reconstruct_path(cameFrom, current); 417 } 418 419 openSet[currentIdx] = openSet[$-1]; 420 openSet = openSet[0 .. $-1]; 421 422 Point[4] neighborsBuffer; 423 int neighborsBufferLength = 0; 424 425 426 // FIXME: would be kinda cool to make this a more generic graph traversal like for subway routes too 427 428 if(current.x + 1 < size.width && isPassable(current + Point(1, 0))) 429 neighborsBuffer[neighborsBufferLength++] = current + Point(1, 0); 430 if(current.x && isPassable(current + Point(-1, 0))) 431 neighborsBuffer[neighborsBufferLength++] = current + Point(-1, 0); 432 if(current.y && isPassable(current + Point(0, -1))) 433 neighborsBuffer[neighborsBufferLength++] = current + Point(0, -1); 434 if(current.y + 1 < size.height && isPassable(current + Point(0, 1))) 435 neighborsBuffer[neighborsBufferLength++] = current + Point(0, 1); 436 437 foreach(neighbor; neighborsBuffer[0 .. neighborsBufferLength]) { 438 auto tentative_gScore = gScore[current.y * size.width + current.x] + d(current, neighbor); 439 if(tentative_gScore < gScore[neighbor.y * size.width + neighbor.x]) { 440 cameFrom[neighbor.y * size.width + neighbor.x] = current; 441 gScore[neighbor.y * size.width + neighbor.x] = tentative_gScore; 442 fScore[neighbor.y * size.width + neighbor.x] = tentative_gScore + h(neighbor); 443 // this linear thing might not be so smart after all 444 bool found = false; 445 foreach(o; openSet) 446 if(o == neighbor) { found = true; break; } 447 if(!found) 448 openSet ~= neighbor; 449 } 450 } 451 } 452 453 return null; 454 }