├── LICENSE ├── README.md ├── dungeon.gif ├── genmap.pl └── qake_dungeon.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Magnus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dungeon Generator 2 | 3 | ## Description 4 | Dungeon generator that produces a 2D-array of different values that can be used to create a dungeon for 5 | a game. The script is written in Perl and generates a gif image of the dungeon. 6 | 7 | The values used for the algorithm in the 2D array is the following: 8 | * 1 = room 9 | * 2 = road 10 | * 3 = Flood fill, temporary used, not used in the resulting map (array). 11 | * 4 = (not used) 12 | * 5 = Player spawn (adds a position in the middle of a room) 13 | * 6 = Boss spawn (adds a position in a room as far away as possible from player) 14 | 15 | The image used in the script is just used to represent the actual dungeon. In a game, the 2D array would be used and the different values in the array would be parsed. 16 | 17 | The result of the script can look like this: 18 | [dungeon.gif] 19 | ![alt tag](https://github.com/Lallassu/DungeonGenerator/blob/master/dungeon.gif) 20 | 21 | The dungeon generator will be used in Qake voxel-engine that can be found here: 22 | https://www.assetstore.unity3d.com/#!/content/68150 23 | 24 | And the result implemented in Qake voxel engine using the output from the script (though ported to C#) can look like this: 25 | [qake_dungeon.png] 26 | ![alt tag](https://github.com/Lallassu/DungeonGenerator/blob/master/qake_dungeon.png) 27 | 28 | ## Run 29 | ``` 30 | perl genmap.pl && open dungeon.gif 31 | ``` 32 | 33 | ## Algorithm 34 | The algorithm is very basic but produces very good random dungeons. 35 | The process is as follows. 36 | * Generate rooms of random sizes and make sure that they don't overlap and also got some distance between them. 37 | * Add doors in the middle of each room. 38 | * From each door, try to draw a road of a certain size outwards until it hits either a road or another room. 39 | * Flood fill each room of the map. Store the biggest flood filled map and remove the rest. 40 | 41 | 42 | ## License 43 | MIT License. 44 | 45 | -------------------------------------------------------------------------------- /dungeon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lallassu/DungeonGenerator/5d68d8c9f0caeb87717372ec4719cb2710e9c9fe/dungeon.gif -------------------------------------------------------------------------------- /genmap.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2016 Magnus Persson (magnus@nergal.se) 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | use strict; 24 | use GD; 25 | use Data::Dumper; 26 | 27 | my $width = 512; 28 | my $height = 512; 29 | my $ok = 0; 30 | 31 | my $image; 32 | my $black; 33 | my $red; 34 | my $green; 35 | my $blue; 36 | my $white; 37 | my $player_spawn; 38 | my $portal_spawn; 39 | my @map; 40 | my @map2; 41 | my @doors; 42 | my @spawns; 43 | 44 | main(); 45 | 46 | sub main { 47 | InitImage(); 48 | InitMap(); 49 | CreateRooms(); 50 | CreateRoads(); 51 | 52 | my @list; 53 | my $max_count = 0; 54 | 55 | for(my $x = 0; $x < $width ; $x++) { 56 | for(my $y = 0; $y < $height ; $y++) { 57 | $map2[$x][$y] = $map[$x][$y]; 58 | } 59 | } 60 | my %world = (); 61 | while(!$ok) { 62 | my $start_y = 0; 63 | my $start_x = 0; 64 | for(my $x = 0; $x < $width; $x++) { 65 | for( my $y = 0; $y < $height; $y++) { 66 | if($map2[$x][$y] == 1 || $map2[$x][$y] == 2) { 67 | $start_x = $x; 68 | $start_y = $y; 69 | last; 70 | } 71 | } 72 | if($start_x != 0) { 73 | last; 74 | } 75 | } 76 | 77 | my $res = FloodFill($start_x,$start_y); 78 | if($res->{count} == 1) { 79 | $ok = 1; 80 | } else { 81 | $world{$res->{count}} = $res->{list}; 82 | } 83 | } 84 | my $largest = 0; 85 | foreach(keys %world) { 86 | if($_ > $largest) { 87 | $largest = $_; 88 | } 89 | } 90 | foreach(keys %world) { 91 | if($_ == $largest) { 92 | next; 93 | } 94 | foreach my $l (@{$world{$_}}) { 95 | $map[$l->[0]][$l->[1]] = 0; 96 | # Remove doors if included here. 97 | my $i = 0; 98 | foreach my $d (@doors) { 99 | $i++; 100 | if($d->[0] == $l->[0] && $d->[1] == $l->[1]) { 101 | splice(@doors, $i, 1); 102 | } 103 | } 104 | } 105 | } 106 | Spawn(); 107 | DrawMap(); 108 | } 109 | 110 | sub InitImage { 111 | $image = new GD::Image($width, $height); 112 | $black = $image->colorAllocate(50,50,50); 113 | $red = $image->colorAllocate(255,0,0); 114 | $green = $image->colorAllocate(0,255, 0); 115 | $blue = $image->colorAllocate(0,0, 255); 116 | $white = $image->colorAllocate(255,255,255); 117 | $player_spawn = $image->colorAllocate(0,255,0); 118 | $portal_spawn = $image->colorAllocate(0,255,255); 119 | } 120 | 121 | sub InitMap { 122 | for(my $x = 0; $x < $width; $x++) { 123 | for( my $y = 0; $y < $height; $y++) { 124 | $map[$x][$y] = 0; 125 | } 126 | } 127 | } 128 | 129 | sub CreateRooms { 130 | for(my $i = 0; $i < 100; $i++) { 131 | my $rx = 50+int(rand($width-100)); 132 | my $ry = 50+int(rand($height-100)); 133 | 134 | my $size_w = 50+int(rand(50)); 135 | my $size_h = 50+int(rand(50)); 136 | 137 | # Check if ok 138 | my $free = 1; 139 | for(my $x = $rx - int($size_w/1.1); $x < int($rx + $size_w/1.1); $x++) { 140 | for(my $y = int($ry - $size_h/1.1); $y < int($ry + $size_h/1.1); $y++) { 141 | if($x > 0 && $x < $width && $y > 0 && $y < $height) { 142 | if($map[$x][$y] == 1) { 143 | $free = 0; 144 | last; 145 | } 146 | } 147 | } 148 | if(!$free) { last; } 149 | } 150 | 151 | if($free) { 152 | for(my $x = $rx - int($size_w/2); $x < int($rx + $size_w/2); $x++) { 153 | for(my $y = int($ry - $size_h/2); $y < int($ry + $size_h/2); $y++) { 154 | $map[$x][$y] = 1; 155 | if($x == $rx && $y == $ry) { 156 | push(@spawns, [$x,$y]); 157 | } 158 | if($x == $rx && $y == int($ry-$size_h/2) ) { 159 | $map[$x][$y] = 2; 160 | push(@doors, [$x, $y]); 161 | } 162 | if($x == $rx && $y == int($ry+$size_h/2-1) ) { 163 | $map[$x][$y] = 2; 164 | push(@doors, [$x, $y]); 165 | } 166 | if($y == $ry && $x == $rx-int($size_w/2)) { 167 | $map[$x][$y] = 2; 168 | push(@doors, [$x, $y]); 169 | } 170 | if($y == $ry && $x == $rx+int($size_w/2-1)) { 171 | $map[$x][$y] = 2; 172 | push(@doors, [$x, $y]); 173 | } 174 | } 175 | } 176 | } 177 | } 178 | } 179 | 180 | sub CreateRoads { 181 | my $roadSize = 10; 182 | foreach my $p (@doors) { 183 | my $count = 1; 184 | my @list = []; 185 | my $roadFail = 0; 186 | while($map[$p->[0]+$count][$p->[1]] == 0 && $p->[0]+$count > 1 && $p->[0]+$count < $width - 1) { 187 | # Check if we can make a broad road. 188 | for(my $yy = $p->[1]-$roadSize/2; $yy < $p->[1]+$roadSize/2; $yy++) { 189 | if($yy > 1 && $yy < $height) { 190 | # Do not hit room our existing road. 191 | if($map[$p->[0]+$count][$yy] == 1 || $map[$p->[0]+$count][$yy] == 2) { 192 | $roadFail = 1; 193 | last; 194 | } 195 | } 196 | } 197 | if($roadFail) { 198 | last; 199 | } 200 | push(@list, [$p->[0]+$count, $p->[1]]); 201 | $count++; 202 | } 203 | if(!$roadFail) { 204 | for(my $yy = $p->[1]-$roadSize/2; $yy < $p->[1]+$roadSize/2; $yy++) { 205 | if($yy > 1 && $yy < $height) { 206 | # Do not hit room our existing road. 207 | if($map[$p->[0]+$count][$yy] == 0) { 208 | $roadFail = 1; 209 | last; 210 | } 211 | } 212 | } 213 | if(!$roadFail) { 214 | foreach(@list) { 215 | if(defined($_->[0])) { 216 | for(my $yy = $_->[1]-$roadSize/2; $yy < $_->[1]+$roadSize/2; $yy++) { 217 | $map[$_->[0]][$yy] = 2; 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | $count = 1; 225 | @list = []; 226 | $roadFail = 0; 227 | while($map[$p->[0]][$p->[1]+$count] == 0 && $p->[1]+$count > 1 && $p->[1]+$count < $height- 1) { 228 | for(my $xx = $p->[0]-$roadSize/2; $xx < $p->[0]+$roadSize/2; $xx++) { 229 | if($xx > 1 && $xx < $width) { 230 | if($map[$xx][$p->[1]+$count] == 1 || $map[$xx][$p->[1]+$count] == 2) { 231 | $roadFail = 1; 232 | last; 233 | } 234 | } 235 | } 236 | if($roadFail) { 237 | last; 238 | } 239 | push(@list, [$p->[0], $p->[1]+$count]); 240 | $count++; 241 | } 242 | if(!$roadFail) { 243 | for(my $xx = $p->[0]-$roadSize/2; $xx < $p->[0]+$roadSize/2; $xx++) { 244 | if($xx > 1 && $xx < $width) { 245 | if($map[$xx][$p->[1]+$count] == 0) { 246 | $roadFail = 1; 247 | last; 248 | } 249 | } 250 | } 251 | if(!$roadFail) { 252 | foreach(@list) { 253 | if(defined($_->[0])) { 254 | for(my $xx = $_->[0]-$roadSize/2; $xx < $_->[0]+$roadSize/2; $xx++) { 255 | $map[$xx][$_->[1]] = 2; 256 | } 257 | } 258 | } 259 | } 260 | } 261 | } 262 | foreach my $d (@doors) { 263 | $map[$d->[0]][$d->[1]] = 1; 264 | } 265 | } 266 | 267 | # Floodfill to see if the map is complete. 268 | sub FloodFill { 269 | my $start_x = shift; 270 | my $start_y = shift; 271 | 272 | my @stack; 273 | push(@stack, [$start_x,$start_y]); 274 | 275 | my $p; 276 | my $count = 0; 277 | my @list; 278 | while($#stack > -1) { 279 | $p = pop(@stack); 280 | push(@list, $p); 281 | $count++; 282 | if($map2[$p->[0]][$p->[1]] == 1 || $map2[$p->[0]][$p->[1]] == 2) { 283 | $map2[$p->[0]][$p->[1]] = 3; 284 | if($p->[0]+1 > 0 && $p->[0]+1 < $width) { 285 | push(@stack, [$p->[0]+1, $p->[1]]); 286 | } 287 | if($p->[0]-1 > 0 && $p->[0]-1 < $width) { 288 | push(@stack, [$p->[0]-1, $p->[1]]); 289 | } 290 | if($p->[1]-1 > 0 && $p->[1]-1 < $height) { 291 | push(@stack, [$p->[0], $p->[1]-1]); 292 | } 293 | if($p->[1]+1 > 0 && $p->[1]+1 < $height) { 294 | push(@stack, [$p->[0], $p->[1]+1]); 295 | } 296 | } 297 | } 298 | 299 | return {list => \@list, count => $count, status => 1}; 300 | } 301 | 302 | sub Spawn { 303 | my $player; 304 | my $portal; 305 | 306 | my $max_dist = 0; 307 | foreach my $p (@spawns) { 308 | foreach my $p2 (@spawns) { 309 | my $dist = sqrt((($p2->[0]-$p->[0])**2)+(($p2->[1]-$p->[1])**2)); 310 | if($dist > $max_dist) { 311 | $max_dist = $dist; 312 | $player = [$p->[0], $p->[1]]; 313 | $portal = [$p2->[0], $p2->[1]]; 314 | } 315 | } 316 | } 317 | $map[$player->[0]][$player->[1]] = 5; 318 | $map[$portal->[0]][$portal->[1]] = 6; 319 | } 320 | 321 | sub DrawMap { 322 | for(my $x = 0; $x < $width; $x++) { 323 | for( my $y = 0; $y < $height; $y++) { 324 | if($map[$x][$y] == 1) { 325 | $image->setPixel($x, $y, $red); 326 | } 327 | if($map[$x][$y] == 2) { 328 | $image->setPixel($x, $y, $green); 329 | } 330 | if($map[$x][$y] == 3) { 331 | $image->setPixel($x, $y, $white); 332 | } 333 | if($map[$x][$y] == 4) { 334 | $image->setPixel($x, $y, $green); 335 | } 336 | if($map[$x][$y] == 5) { 337 | $image->setPixel($x, $y, $player_spawn); 338 | } 339 | if($map[$x][$y] == 6) { 340 | $image->setPixel($x, $y, $portal_spawn); 341 | } 342 | } 343 | } 344 | 345 | binmode STDOUT; 346 | open(FILE, "+>dungeon.gif"); 347 | print FILE $image->gif; 348 | close(FILE); 349 | } 350 | 351 | -------------------------------------------------------------------------------- /qake_dungeon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lallassu/DungeonGenerator/5d68d8c9f0caeb87717372ec4719cb2710e9c9fe/qake_dungeon.png --------------------------------------------------------------------------------