Map Generation Logic

#1
Hey guys. So now that I've got the rooms imported into Unity I'm working on the random map generation which will include item placement and triggers for each room. I'm working on making that stuff modular. As for the room generation algorithm itself, I'm merely copying the code for CreateMap() into C# and then will pass it a list of rooms of each type. I'm starting to get a grasp on it, but was wondering if experienced modders could point out anything I should look out for.

I'm assuming that mapTemp is just where rooms should be on a grid? Is it an integer array that is also being treated as boolean, since it is assigned values such as true that are later changed to 2, 3, etc? And finally, if I pass in the x and y coordinates generated by the CreateMap() script, is there anything else i have to do to handle positioning or will that do it? Also how does this guarantee rooms do not overlap?

I'll probably stumble along the answers as I continue to convert and parse the code, but I'd rather not reinvent the wheel if people have a better understanding of these things. Thanks!

Re: Map Generation Logic

#2
I would advise against converting the algorithm as-is. What it does is really not that complicated, and there are much less convoluted (and less bug-prone) ways to generate a similar map. RogueBasin has some good articles and tutorials about procedural level/dungeon generation that might be useful.

But if you insist on using the same algorithm:
zornor90 wrote:I'm assuming that mapTemp is just where rooms should be on a grid? Is it an integer array that is also being treated as boolean, since it is assigned values such as true that are later changed to 2, 3, etc?
In B3D booleans are essentially integers, True being 1 and False 0. The (very descriptively named) MapTemp array determines which cells in the grid are rooms and which are not.

0 = no room
1 = a "dead end room" with only one adjacent room
2 = a room with two adjacent rooms (i.e. a hallway or a "corner room")
3 = a 3-way-room
4 = a 4-way room
zornor90 wrote:And finally, if I pass in the x and y coordinates generated by the CreateMap() script, is there anything else i have to do to handle positioning or will that do it?
If you mean the x and y indices in the MapTemp array, you should multiply them with some scaling factor or the rooms will be placed one unit apart from each other. In SCP-CB that factor is 8.0 (because I though 8x8 meters would be a good room size).
zornor90 wrote:Also how does this guarantee rooms do not overlap?
In the first few versions this was handled simply by constraining each room inside one of those 8x8 unit "cells". Now it's done by preventing the algorithm from placing any of the larger rooms next to each other (see the CheckRoomOverlap function).

Re: Map Generation Logic

#3
Oh man, thanks for taking the time to respond to this, Regalis! Yeah I have done a bit of procedural grid generation in the past so I guess I could make a new algorithm, or base it off of another that has been done before. I was hoping to just copy this part to save time and also to ensure that the rooms and doors line up correctly, and then in the future create a totally new generation algorithm once the game is ported. Is it safe to assume that any random dungeon generator will place them correctly inline with each other, as long as I map the rooms to the zones they belong in correctly (and of course apply the scalefactor)? (They are the rooms from rooms.rar available on this website, if it matters). I haven't gone all the way through the CreateMap function yet so forgive me if this is answered further in the code

Also the naming convention for the rooms makes a LOT more sense now, so thanks for clearing that up for me

Also just wanted to say that I greatly respect you for writing something this complex in Blitz3D, coming from somebody loathe to leave the comfortable world of C#, javascript, and java.

Re: Map Generation Logic

#4
CreateMap is a complete mess, don't try porting it. If you feel like changing up the game rather than making a 1:1 port, something you should consider is not having the map be set on a grid, but instead generate hallways of variable length and intersections completely procedurally, and put "special rooms" (containment chambers, checkpoints or otherwise important rooms) in gaps between each hallway. This would allow for more varied maps, and could potentially be easier to maintain in the long run.

But if you still want to go for a grid-based map system, here's some C++ code I wrote for SCPCBIrrlicht (modified to remove some stuff specific to that project), it should be slightly easier to follow:

Code: Select all

void World::createMap(int zone) {
	int x,y,temp;
	int x2,y2;
	int width,height;

	struct tempRoom {
		RoomTypes type;
		/* angle can be:
		   * -1: do not spawn a room
		   * 0: means 0º rotation; facing east
		   * 1: means 90º rotation; facing north
		   * 2: means 180º rotation; facing west
		   * 3: means 270º rotation; facing south
		*/
		int angle;
	};

	tempRoom roomTemp[20][20];
	for (x=0;x<20;x++) {
		for (y=0;y<20;y++) {
			roomArray[x][y] = nullptr;
			roomTemp[x][y].type = RoomTypes::ROOM1;
			roomTemp[x][y].angle = -1;
		}
	}
	
    y = 19;
    x = rand()%6+8;
    int genDir = 1;
    int genDist = (rand()%2)+2;

    while (y>0) {
        switch (genDir) {
            case 0:
                for (int i=0;i<genDist;i++) {
                    if (x<19) {
                        x++;
                        roomTemp[x][y].angle = 0;
                    } else { break; }
                }
                genDir = 1;
            break;
            case 1:
                for (int i=0;i<genDist;i++) {
                    if (y>0) {
                        y--;
                        roomTemp[x][y].angle = 0;
                    } else { break; }
                }
                genDir = (rand()%2)*2;
                if (x>13) { genDir = 2; }
                if (x<7) { genDir = 0; }
            break;
            case 2:
                for (int i=0;i<genDist;i++) {
                    if (x>0) {
                        x--;
                        roomTemp[x][y].angle = 0;
                    } else { break; }
                }
                genDir = 1;
            break;
        }
        if (genDir!=1) {
            genDist = (rand()%7)+3;
        } else {
            genDist = (rand()%2)+2;
        }
        roomTemp[x][y].angle = 1;
    }

    for (x2=0;x2<19;x2++) {
        for (y2=0;y2<19;y2++) {
            if (roomTemp[x2][y2].angle==0) {
                if (y2<12) {
                    genDist = (rand()%5)+2;
                    if (roomTemp[x2+1][y2].angle==0 && roomTemp[x2-1][y2].angle==0) {
                        for (int i=0;i<genDist;i++) {
                            if (!(roomTemp[x2+1][y2+i+2].angle>=1 || roomTemp[x2-1][y2+i+2].angle>=1) && ((roomTemp[x2+1][y2+i+1].angle>=0) == (roomTemp[x2-1][y2+i+1].angle>=0)) && ((roomTemp[x2][y2+i+1].angle>=0) == (roomTemp[x2-1][y2+i+1].angle>=0))) {
                                roomTemp[x2][y2+i+1].angle = 1;
                            } else {
                                break;
                            }
                        }
                    }
                }
                if (x2<12) {
                    genDist = (rand()%5)+2;
                    if (roomTemp[x2][y2+1].angle==0 && roomTemp[x2][y2-1].angle==0) {
                        for (int i=0;i<genDist;i++) {
                            if (!(roomTemp[x2+i+2][y2+1].angle>=1 || roomTemp[x2+i+2][y2-1].angle>=1) && ((roomTemp[x2+i+1][y2+1].angle>=0) == (roomTemp[x2+i+1][y2-1].angle>=0)) && ((roomTemp[x2+i+1][y2].angle>=0) == (roomTemp[x2+i+1][y2-1].angle>=0))) {
                                roomTemp[x2+i+1][y2].angle = 1;
                            } else {
                                break;
                            }
                        }
                    }
                }

                if (y2>=8) {
                    genDist = (rand()%5)+2;
                    if (roomTemp[x2+1][y2].angle==0 && roomTemp[x2-1][y2].angle==0) {
                        for (int i=0;i<genDist;i++) {
                            if (!(roomTemp[x2+1][y2-i-2].angle>=1 || roomTemp[x2-1][y2-i-2].angle>=1) && ((roomTemp[x2+1][y2-i-1].angle>=0) == (roomTemp[x2-1][y2-i-1].angle>=0)) && ((roomTemp[x2][y2-i-1].angle>=0) == (roomTemp[x2-1][y2-i-1].angle>=0))) {
                                roomTemp[x2][y2-i-1].angle = 1;
                            } else {
                                break;
                            }
                        }
                    }
                }
                if (x2>=8) {
                    genDist = (rand()%5)+2;
                    if (roomTemp[x2][y2+1].angle==0 && roomTemp[x2][y2-1].angle==0) {
                        for (int i=0;i<genDist;i++) {
                            if (!(roomTemp[x2-i-2][y2+1].angle>=1 || roomTemp[x2-i-2][y2-1].angle>=1) && ((roomTemp[x2-i-1][y2+1].angle>=0) == (roomTemp[x2-i-1][y2-1].angle>=0)) && ((roomTemp[x2-i-1][y2].angle>=0) == (roomTemp[x2-i-1][y2-1].angle>=0))) {
                                roomTemp[x2-i-1][y2].angle = 1;
                            } else {
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

	int Room1amount;
	int Room2amount;
	int Room2camount;
	int Room3amount;
	int Room4amount;

	Room1amount=0;
	Room2amount=0;
	Room2camount=0;
	Room3amount=0;
	Room4amount=0;

	for (x=0;x<20;x++) {
		for (y=0;y<20;y++) {
			bool hasNorth,hasSouth,hasEast,hasWest;
			hasNorth = hasSouth = hasEast = hasWest = false;
			if (roomTemp[x][y].angle>=0) {
				if (x>0) {
					hasWest = (roomTemp[x-1][y].angle>-1);
				}
				if (x<19) {
					hasEast = (roomTemp[x+1][y].angle>-1);
				}
				if (y>0) {
					hasNorth = (roomTemp[x][y-1].angle>-1);
				}
				if (y<19) {
					hasSouth = (roomTemp[x][y+1].angle>-1);
				}
				if (hasNorth && hasSouth) {
					if (hasEast && hasWest) { //Room4
						roomTemp[x][y].type = RoomTypes::ROOM4;
						roomTemp[x][y].angle = rand()%4; //any angle will do
					} else if (hasEast && !hasWest) { //Room3, pointing east
						roomTemp[x][y].type = RoomTypes::ROOM3;
						roomTemp[x][y].angle = 3;
					} else if (!hasEast && hasWest) { //Room3, pointing west
						roomTemp[x][y].type = RoomTypes::ROOM3;
						roomTemp[x][y].angle = 1;
					} else { //vertical Room2
						roomTemp[x][y].type = RoomTypes::ROOM2;
						roomTemp[x][y].angle = (rand()%2)*2;
					}
				} else if (hasEast && hasWest) {
					if (hasNorth && !hasSouth) { //Room3, pointing north
						roomTemp[x][y].type = RoomTypes::ROOM3;
						roomTemp[x][y].angle = 0;
					} else if (!hasNorth && hasSouth) { //Room3, pointing south
						roomTemp[x][y].type = RoomTypes::ROOM3;
						roomTemp[x][y].angle = 2;
					} else { //horizontal Room2
						roomTemp[x][y].type = RoomTypes::ROOM2;
						roomTemp[x][y].angle = ((rand()%2)*2)+1;
					}
				} else if (hasNorth) {
					if (hasEast) { //Room2c, north-east
						roomTemp[x][y].type = RoomTypes::ROOM2C;
						roomTemp[x][y].angle = 0;
					} else if (hasWest) { //Room2c, north-west
						roomTemp[x][y].type = RoomTypes::ROOM2C;
						roomTemp[x][y].angle = 1;
					} else { //Room1, north
						roomTemp[x][y].type = RoomTypes::ROOM1;
						roomTemp[x][y].angle = 0;
					}
				} else if (hasSouth) {
					if (hasEast) { //Room2c, south-east
						roomTemp[x][y].type = RoomTypes::ROOM2C;
						roomTemp[x][y].angle = 3;
					} else if (hasWest) { //Room2c, south-west
						roomTemp[x][y].type = RoomTypes::ROOM2C;
						roomTemp[x][y].angle = 2;
					} else { //Room1, south
						roomTemp[x][y].type = RoomTypes::ROOM1;
						roomTemp[x][y].angle = 2;
					}
				} else if (hasEast) { //Room1, east
					roomTemp[x][y].type = RoomTypes::ROOM1;
					roomTemp[x][y].angle = 3;
				} else { //Room1, west
					roomTemp[x][y].type = RoomTypes::ROOM1;
					roomTemp[x][y].angle = 1;
				}
				switch (roomTemp[x][y].type) {
					case RoomTypes::ROOM1:
						Room1amount++;
					break;
					case RoomTypes::ROOM2:
						Room2amount++;
					break;
					case RoomTypes::ROOM2C:
						Room2camount++;
					break;
					case RoomTypes::ROOM3:
						Room3amount++;
					break;
					case RoomTypes::ROOM4:
						Room4amount++;
					break;
				}
			}
		}
	}
}
This code doesn't include the decision-making for each specific room, since that was a total disaster. I also didn't implement any intersection checking so you'll need to do that yourself later in the code, it shouldn't be a big issue.

Re: Map Generation Logic

#5
Awesome, thanks so much! I will probably move to a procedural system for version 2 when I upgrade the graphics, but for version 1 I'd like to stay with a grid based system to work with the old models.


Is there anything else I should keep in mind? I'm making the rooms into Unity prefab objects which handle their own events, collision, doors, interactivity, item placement, etc. (This should also allow users to mod the game's rooms with asset bundles, kind of like mods for kerbal space program. That's a distant goal though) I'm mostly wanting this script to handle placing the prefabs so that they line up correctly with hallways and everything.

Thanks

Re: Map Generation Logic

#6
juanjpro wrote:But if you still want to go for a grid-based map system, here's some C++ code I wrote for SCPCBIrrlicht (modified to remove some stuff specific to that project), it should be slightly easier to follow:
Dude your algorithm rocks, I now have a fully traversable map. It appears that each block connects nicely when sized to 20 * 20 units as the scale factor.
Image