Flexible Room Layout algorithm

Published October 18, 2018
Advertisement

preimg.png.77b995a7bc8ffc6a71cfe5a16ecc6599.pngWhile making a roguelike game, procedural generation have to be quick and yet intriguing enough for the generated level to be fun to just pick up and play.

There are many ways to generate and laying out procedurally generated rooms. In The Binding Of Isaac, for example, you have many different types of regular room presets. 

The generator just picks a preset based on things like room placement and size. Because those rooms are always of fixed size, this is a nice compromise. By having handmade presets the generated level is somewhat believable (i.e. there are no gaps or obstacles below a room door or secret room and whatnot).

gWBi3.png  large.jpg  The-Binding-of-Isaac-Rebirth-2.jpg

Another example would be Nuclear Throne

The game takes a different approach to procedural generation by keeping it relatively simple. Because it's not room-based like The Binding Of Isaac, there are more things like caves and large open area.  The gameplay also plays into this, as the player needs to eliminate every enemy to spawn a portal to the next level.

orig_8b6b223c410185c7a632fd5e345397f2.png   z2POA7Q2QZGO48PDbp4E_screenshot_main_1000.jpg Nuclear-Throne.jpg

Because my game is somehow more inspired by The Binding of Isaac, the right way to procedurally generate rooms would be to use presets, and this is how I make special rooms.

However, there's a big difference between The Binding Of Isaac and my game: my regular rooms aren't always the same size. This means that rather than having presets of regular rooms as well as special rooms I need something more flexible and, most importantly, dynamic..

The anatomy of a Room

In my game, as I've said in a previous post, levels are big two-dimensional arrays from which the level geometry is generated. Every room of a level is made using a BSP tree. I won't go in details much on how rooms are generated, but in essence, we create a grid from which we trace a path between two rooms and sparsely attach bonus rooms along the way.

Because I already have rooms sizes and whatnot with that level generation, I could reuse the same idea for room layouts.

Within rooms, I've also set special anchor points from which props (or more precisely, prop formations, more on that later...) could be generated.

RoomAnchorPoints.png.cde9f70d504c4961c2038ca40fa60d35.png

Basic Layouts

The idea here is to have room layout presets. Within those, presets are an array of prop formations, and each of these formations is linked to a specific anchor point.

A formation has a two-dimensional boolean array that indicates whenever or not there should be a prop here.

simpleGrid.png.4110299325d4653877612c2a8cbf6620.png

Let's take, for example, a diamond array:

diamondArray.png.649e2f7d790010efd3d70a2079c1e9f2.png

The dimension of the array depends on its rooms' dimensions. Here's how it's done:

\( size = \left \lceil \frac{2(max(RoomSize_{x},RoomSize_{y}))) }{ 3 } \right \rceil\)

In order to change the array's content we actually use common image manipulation algorithms...

Bresenham's Line Algorithm

The first used algorithm is the Bresenham's Line Algorithm

Its purpose is to simply render a line describe by two bitmap points onto a raster image.

To put it simply, we get the deviation (delta, or "d" for short) in both X and Y of each point of the described line and compare both of them.

Depending on the biggest, we simply incremate the point on that axis and colour it in.

bresenhamLine.png.cf481327cfa1764feeb187bd17f07c77.png

Here's the implementation:


public void TraceLine(Vector2Int p0, Vector2Int p1)
{
  int dx = Mathf.Abs(p1.x - p0.x), sx = p0.x < p1.x ? 1 : -1;
  int dy = Mathf.Abs(p1.y - p0.y), sy = p0.y < p1.y ? 1 : -1;
  int err = (dx > dy ? dx : -dy) / 2, e2;

  while (true)
  {
    m_propArray[p0.x][p0.y] = true;
    if (p0.x == p1.x && p0.y == p1.y)
    {
      break;
    }
    e2 = err;
    if (e2 > -dx)
    {
      err -= dy; p0.x += sx;
    }
    if (e2 < dy)
    {
      err += dx; p0.y += sy;
    }
  }
}

Midpoint Circle Algorithm

The midpoint circle algorithm is an algorithm used to render a circle onto an image.

The idea is somehow similar to Bresenham's Line Algorithm, but rather than drawing a line we draw a circle.

To do this we also need, for simplicity sakes, to divide the circle into 8 pieces, called octants. We can do this because circles are always symmetric. (It's also a nice way to unroll loops)

midpoint.png.b4d352ebce1b5011a36d28b637dbc855.png

Here's the implementation:


private void TraceCircle(Vector2Int center, int r, AbstractPropFormation formation)
{
  int d = (5 - r * 4) / 4;
  int x = 0;
  int y = r;

  do
  {
    // ensure index is in range before setting (depends on your image implementation)
    // in this case we check if the pixel location is within the bounds of the image before setting the pixel
    if (IsValidPoint(center + new Vector2Int(x,y)) { formation.m_propArray[center.x + x][center.y + y] = true; }
    if (IsValidPoint(center + new Vector2Int(x,-y)) { formation.m_propArray[center.x + x][center.y - y] = true; }
    if (IsValidPoint(center + new Vector2Int(-x,y)) { formation.m_propArray[center.x - x][center.y + y] = true; }
    if (IsValidPoint(center + new Vector2Int(-x,-y)) { formation.m_propArray[center.x - x][center.y - y] = true; }
    if (IsValidPoint(center + new Vector2Int(y,x)) { formation.m_propArray[center.x + y][center.y + x] = true; }
    if (IsValidPoint(center + new Vector2Int(y,-x)) { formation.m_propArray[center.x + y][center.y - x] = true; }
    if (IsValidPoint(center + new Vector2Int(-y,x)) { formation.m_propArray[center.x - y][center.y + x] = true; }
    if (IsValidPoint(center + new Vector2Int(-y,-x)) { formation.m_propArray[center.x - y][center.y - x] = true; }
        
    if (d < 0)
    {
      d += 2 * x + 1;
    }
    else
    {
      d += 2 * (x - y) + 1;
      y--;
    }
        
    x++;
        
  } while (x <= y);
}

Flood Fill Algorithm

This is quite a classic, but it's still useful nevertheless.

The idea is to progressively fill a section of an image with a specific colour while 

The implementation is using a coordinate queue rather than recursion for optimization sakes.

We also try to fill the image using west-east orientation. Basically, we fill the westmost pixel first, eastmost second and finally go north-south.

fill.png.476095b0d53764a4b883cb4e89c32834.png

Here's the implementation:


public void Fill(Vector2Int point)
{
  Queue<Vector2Int> q = new Queue<Vector2Int>();
  q.Enqueue(point);
  while (q.Count > 0)
  {
    Vector2Int currentPoint = q.Dequeue();
    if (!m_propArray[currentPoint.x][currentPoint.y])
    {
      Vector2Int westPoint = currentPoint, eastPoint = new Vector2Int(currentPoint.x + 1, currentPoint.y);
      while ((westPoint.x >= 0) && !m_propArray[westPoint.x][westPoint.y])
      {
        m_propArray[westPoint.x][westPoint.y] = true;
        if ((westPoint.y > 0) && !m_propArray[westPoint.x][westPoint.y - 1])
        {
          q.Enqueue(new Vector2Int(westPoint.x, westPoint.y - 1));
        }
        if ((westPoint.y < m_propArray[westPoint.x].Length - 1) && !m_propArray[westPoint.x][westPoint.y + 1])
        {
          q.Enqueue(new Vector2Int(westPoint.x, westPoint.y + 1));
        }
        westPoint.x--;
      }
      while ((eastPoint.x <= m_propArray.Length - 1) && !m_propArray[eastPoint.x][eastPoint.y])
      {
        m_propArray[eastPoint.x][eastPoint.y] = true;
        if ((eastPoint.y > 0) && !m_propArray[eastPoint.x][eastPoint.y - 1])
        {
          q.Enqueue(new Vector2Int(eastPoint.x, eastPoint.y - 1));
        }
        if ((eastPoint.y < m_propArray[eastPoint.x].Length - 1) && !m_propArray[eastPoint.x][eastPoint.y + 1])
        {
          q.Enqueue(new Vector2Int(eastPoint.x, eastPoint.y + 1));
        }
        eastPoint.x++;
      }
    }
  }
}

Formation Shapes

Each formation also has a specific shape. These shapes simply define the content of the formation array. We can build these shapes using the previously mentioned algorithms. There are 9 different types of shapes as of now.

Vertical line

ShowVerticalLine.png.b9810ecc65ac25a1f03915017d5cd44d.png

A simple vertical line of a width of one

Horizontal line

ShowHorizontalLine.png.d2dfeb2b33efd585fbcc2d3010f4e4d4.png

A simple horizontal line of a width of one

Diamond

ShowDiamond.png.0da21b05f2bc5b5fc71a3b30943e426d.png

A rather nice diamond shape, especially pretty in corners

Circle

ShowCircle.png.c81621efb52433fd8033dd5a8abfcc5f.png

The circle is rendered using the Midpoint circle algorithm. Especially pretty in the center of rooms

Cross

ShowCross.png.9f2a1770f54feff3e4a2bb2bb078a186.png

A simple cross shape, i.e a vertical and horizontal line align at the center. 

X Shape

ShowX.png.ef4381a381593e774ff94d17be141a56.png

An "X" shaped cross, i.e two perpendicular diagonal lines align at the center.

Triangle

ShowTriangle.png.8ef5999303d89cba88d5032df3098239.png

An Isocele triangle.

Square

ShowSquare.png.8f1c40373b3975476a3257339328bdbd.png

A solid block. Every cell of the formation is essentially true.

Checkers

ShowCheckers.png.34dd738006128360698276b59f1e96eb.png

A nice variation of the square shape. Every other cell is false.

There might be more types of shapes as time goes by, but it's about it for now.

Placing props

Once the array is set, we simply need to place the actual props in the room.

Each formation is of an actual type, i.e. rocks, ferns, etc. 

(For simplicity sakes, let's say that every prop is a 1x1x1m cube. This would simplify future steps.)

In order to find their position, we simply align the array's center to the formations' specified anchor point.

ShowAlignedFormations.png.62a5f00f4133b24a4ad91b4a8251f743.png

For each prop formation, we then instantiate props for each true cells while checking whenever or not the prop would be outside its room.

ShowPropCreation.png.5cb144151b7908ba44fa31c60eacf5b0.png

Afterwards, we do a precise position check to make sure no props are either partially or fully outside a room.

ShowPropRemoval.png.b33a83143a5cc473234f6929814bfc96.png

Finally, we make sure every room connections aren't obstructed with props.

ShowConnectionClearing.png.2f176e20beb4779148c2ce94a2b05e81.png

And voilà, we have a nicely decorated room

ShowFinalRoom.png.92c03c9ddced50ce9065b0d423bcea02.png

In Game Screenshots

Here's a couple of screenshots of what it looks like in-game

image.thumb.png.07b465f833cc6280c198281d9a322cf7.pngimage.thumb.png.32e7a33888dc65e221cd0ef42bfa1104.pngimage.thumb.png.f43c8bcd77bc7e415544336de4bb42da.pngimage.thumb.png.167460b3d2bc2c8c60e9f4d0224cf5db.pngimage.thumb.png.f1f71417eef5da0058674c4604c5e8dd.pngimage.thumb.png.acb57e5168be86784192017974a0db17.pngimage.thumb.png.2620c33bd97bac8b55742451124c33e1.pngimage.thumb.png.2e7758e44d0c585795b01c8560587fca.pngimage.thumb.png.2630314e74fc70f03e55e654c4c239bf.png

 

4 likes 0 comments

Comments

jb-dev

UPDATE: After thinking about it for a while, the checkered pattern was kinda removed.

Instead, it's a property of each prop formation.

This means that we can now have rooms that have checkered circles and diamond among other shapes...

October 20, 2018 03:06 PM
jbadams

Sounds like a solid approach, good explanation! :)

October 21, 2018 11:15 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement