📜 ⬆️ ⬇️

Procedural generation of floor plans


What does a major game developer do when he needs to concoct a lot of space for the game world? Hires a bunch of artists. What makes a lazy / poor / lonely game developer in the same situation? Writes a procedural generator that does all the dirty work for it.

There are many , many articles on procedural generation of floor plans. Here are five links to articles . Only the source to none of them.

In this article I will talk about how I implemented one simple generation method on Unity3d, which leads to good results and is easily modified. With pictures and sources.

If you want, you can read the original article , but the essence is approximately as follows:

Rooms grow one wall at a time to avoid walls and rooms crawling on top of each other. The wall is selected from the largest walls of the room, if there is the same - a random one is taken. The largest wall is taken in order to increase the area of ​​the rooms was maximum, and they were "plump".
')
Now about my implementation. I didn’t bother with the realistic layout, the coefficients and the size of the rooms - these are all decorations, I worked on the basic algorithm, everything else can be added quickly. The article also speaks of special checks that prevent the appearance of U-shaped rooms, I did not do them, I do not see anything wrong with such rooms. Also, I did not arrange corridors, doors and windows, this is a separate topic, and their placement may depend on the generation of the rest of the building.

The most difficult was to choose the principle of growth of rooms and the method of storing data about them and the building. One and the same result can be obtained with the help of cellular automata or even a simple run through the array. I tried several options, I always wanted to know about the dimensions of the room and the exact position of its walls, so I created a class in which only the corners of the room are stored, and the walls are automatically created by linking two adjacent corners.

Linking walls is essentially a search for a polygon shell from a set of points. The task is non-trivial and with a bunch of dirty tricks, if you're interested, then I advise you to look here . Fortunately, I decided to confine myself to right angles between adjacent walls and made a simple algorithm:

Sort the corners of the room
public GridVector SortCorners() { //    var minX = corners[0].x; var maxX = corners[0].x; var minY = corners[0].y; var maxY = corners[0].y; foreach (var corner in corners) { if (corner.x < minX) minX = corner.x; if (corner.x > maxX) maxX = corner.x; if (corner.y < minY) minY = corner.y; if (corner.y > maxY) maxY = corner.y; } //    var oldC = new List<GridVector>(corners); var newC = new List<GridVector>(); bool parallelX = false; while (oldC.Count > 1) { //    if (newC.Count == 0) { if (ScanUp(ref oldC, ref newC, minX, minY, maxY)) continue; if (ScanRight(ref oldC, ref newC, minX, minY, maxX)) continue; if (ScanDown(ref oldC, ref newC, minX, minY, minY)) continue; if (!ScanLeft(ref oldC, ref newC, minX, minY, minX)) { Debug.Log("Error on start"); return null; } } //    else { var last = newC[newC.Count - 1]; if (parallelX) { if (ScanRight(ref oldC, ref newC, last.x, last.y, maxX)) { parallelX = false; continue; } if (ScanLeft(ref oldC, ref newC, last.x, last.y, minX)) { parallelX = false; continue; } } else { if (ScanUp(ref oldC, ref newC, last.x, last.y, maxY)) { parallelX = true; continue; } if (ScanDown(ref oldC, ref newC, last.x, last.y, minY)) { parallelX = true; continue; } } Debug.Log("Error -------------------------------------------------"); Debug.Log("Corners: " + corners.Count); Debug.Log("OldC: " + oldC.Count); Debug.Log("NewC: " + newC.Count); Debug.Log(last); color = Color.red; return last; } } //     newC.Add(oldC[0]); corners = newC; return null; } bool ScanLeft(ref List<GridVector> oldC, ref List<GridVector> newC, int startX, int startY, int minX) { for (var x = startX; x >= minX; x--) { var index = oldC.FindIndex(gv => gv.x == x && gv.y == startY); if (index > -1) { newC.Add(oldC[index]); oldC.RemoveAt(index); return true; } } return false; } bool ScanUp(ref List<GridVector> oldC, ref List<GridVector> newC, int startX, int startY, int maxY) { for (var y = startY; y <= maxY; y++) { var index = oldC.FindIndex(gv => gv.x == startX && gv.y == y); if (index > -1) { newC.Add(oldC[index]); oldC.RemoveAt(index); return true; } } return false; } bool ScanRight(ref List<GridVector> oldC, ref List<GridVector> newC, int startX, int startY, int maxX) { for (var x = startX; x <= maxX; x++) { var index = oldC.FindIndex(gv => gv.x == x && gv.y == startY); if (index > -1) { newC.Add(oldC[index]); oldC.RemoveAt(index); return true; } } return false; } bool ScanDown(ref List<GridVector> oldC, ref List<GridVector> newC, int startX, int startY, int minY) { for (var y = startY; y >= minY; y--) { var index = oldC.FindIndex(gv => gv.x == startX && gv.y == y); if (index > -1) { newC.Add(oldC[index]); oldC.RemoveAt(index); return true; } } return false; } 



In the end, we get a list of angles, sorted in a clockwise direction, from which you can easily draw walls. Thanks to the sorting, it becomes easy to recognize another important thing - the direction outward from the wall of the room. In order to rotate the end of the wall outward, you need to swap X and Y in places and invert the first coordinate to get the following: (-y, x).

Rooms are stored as points, points are sorted, walls are built automatically, outward direction is known - all this is enough to check the free space on the floor plan and grow rooms. Sometimes the truth has to add new walls when the old ones have already rested against something and cannot grow.

To check the availability of cells and search for possible new walls, I use a single function that collects a list of available for growth sections of the wall supplied to the entrance. Then I sort the found segments from the whole room into categories: solid walls, pieces at the edge of the wall, central segments. I choose the largest wall available and send it to the growth function of the room, but she understands, she already has such a wall, or we need to create a new one. When there is a wall, the coordinates of its ends are shifted outwards, which leads to the automatic lengthening of the adjacent walls.

Search wall segments
 List<RoomWall> FindSegments(RoomWall wall, Color freeColor, Color roomColor) { var moved = wall + wall.outwards.minimized; BresenhamLine(moved, new Color(Random.value * 0.7f + 0.1f, Random.value * 0.7f + 0.1f, Random.value * 0.7f + 0.1f), segmentsTexture); var x0 = moved.start.x; var y0 = moved.start.y; var x1 = moved.end.x; var y1 = moved.end.y; var segments = new List<RoomWall>(); GridVector start = null; GridVector end = null; bool steep = Math.Abs(y1 - y0) > Math.Abs(x1 - x0); if (steep) { Swap(ref x0, ref y0); Swap(ref x1, ref y1); } if (x0 > x1) { Swap(ref x0, ref x1); Swap(ref y0, ref y1); } for (int x = x0; x <= x1; x++) { for (int y = y0; y <= y1; y++) { int coordX = steep ? y : x; int coordY = steep ? x : y; Color color = texture.GetPixel(coordX, coordY); if (color != freeColor && color != roomColor) { if (end != null && start != null) { var segment = new RoomWall(start, end); segment -= wall.outwards.minimized; segments.Add(segment); start = null; end = null; } scanTexture.SetPixel(coordX, coordY, Color.red); } else { if (start == null) { start = new GridVector(coordX, coordY); } end = new GridVector(coordX, coordY); scanTexture.SetPixel(coordX, coordY, Color.green); } } } if (end != null && start != null) { var segment = new RoomWall(start, end); segment -= wall.outwards.minimized; segments.Add(segment); } return segments; } 


To display all the manipulations with the rooms, I use a texture, in pixels of which I bring up the positions of the walls. If you do not erase the old walls, you get filled areas, as in the gif-ke at the beginning of the article. I draw walls with the help of the Brezenham lines, which I wrote about here . In case of problems with the gluing of the walls, everything at once becomes clear. Instead of texture, you can use a two-dimensional array, or immediately operate a three-dimensional model.

Exterior walls can be generated very simply. We draw several black rectangles of arbitrary sizes, and on top of them we draw the same white ones and one pixel less on each side. It turns out a lot of different houses. You can also make them three-dimensional and cover the roof. Kanevsky's voice: However, this is a completely different story.

On the links below you can see the binary and source code of the finished project.

Unity Web Player | Windows | Linux | Mac | Sources on GitHub

Shift - Creates random exterior walls with random rooms.
Ctrl - Load a test texture with random rooms.
Enter - Remove all rooms and load a test texture.
Spacebar - Stop rooms growing
Alphabet Keyboard Unit - Enable Free Cell Search Visualization
Left mouse button on the free area - Add room
Esc - Exit

For Linux users: Make ProceduralExperiments.x86 executable using “chmod + x ProceduralExperiments.x86” and run.

Source: https://habr.com/ru/post/184818/


All Articles