**SF Rogue Corridor Generation**by Paul Ledger

# SF Rogue Corridor Generation

This article continues covering the challenges I faced while creating SF Rogue.

## Challenge 4

Now that we have our random rooms generated, that don't overlap and connecting to other rooms, we need to deal with "orphaned" rooms that could not connect due to location within the level.

### The Problem

Firstly it will help to explain the problem we are trying to solve in a bit more detail. As the rooms are created and moved to eliminate overlaps, some rooms will be left with no touching rooms...

The above image shows that there are 4 rooms in this level that are not connected, the first 3 do have touching rooms, but not enough space to put a door in, while the fourth room is truly on its own.

### The Solution

I'm sure there are probably a number of solutions to this problem, but the one that came to me was to create an imaginary rectangle thats the height of a corridor (3 tiles) and the width of the whole level. Starting at the bottom edge of the room find other rooms that intersect that rectangle and see if a connection can be made...

If no intersecting rooms where found (though obviously there is in this version), keep moving the rectangle up a tile until you've run out of height. If a room still hasn't been found repeat the process vertically.

### The Code

We will continue using the same playground we created in the previous article, which can be found on GitHub.

To get things started we need to add a few `Constants`

values, add the following...

```
static let corridorSize = Constraints.tileSize * 3 // The dimension of a corridor, 2 walls and a floor tile
static let floatTileSize = CGFloat(Constraints.tileSize) // A CGFloat version of the tile size, for convenience
```

Add the following computed properety to `Room`

so that we can easily identify the orphaned rooms...

```
public var noDoors: Bool {
return doors.count == 0
}
```

Now in the main `Playground`

we can include a function that will control the addition of corridors into the level.
Add the following...

```
func findCorridors() {
// Calculate the whole area the rooms are contained in
var levelArea = CGRect.zero
for room in rooms {
levelArea = room.frame.union(levelArea)
}
// Find the rooms that don't have an doors,
// obviously not all levels will contain rooms with no doors
let roomsWithoutDoors = rooms.filter( { $0.noDoors } )
for room in roomsWithoutDoors {
// Attempt to create a corridor and add it to the array of rooms
if let corridor = room.generateCorridor(levelArea, rooms) {
rooms.append(corridor)
}
}
}
```

Then add a call to `findCorridors()`

in the `didMove`

function, after the call to
`findConnections()`

and before the rendering of the rooms...

```
findConnections()
findCorridors()
for room in rooms {
```

Now the Playground will highlight errors as a `Room`

doesn't know how to `generateCorridor`

.
We will start correcting that now, its worth pointing out that a corridor is just a long narrow `Room`

.
Add the following function to the `Room`

```
public func generateCorridor(_ levelArea: CGRect, _ rooms: [Room]) -> Room? {
// The unique number of the room if it gets created
let roomNumber = rooms.count
// Try and create a horizontal corridor
if let corridor = generateHorizontalCorridor(levelArea, rooms, roomNumber) {
return corridor
}
// We've not created a horizontal one, so try and create a vertical one
if let corridor = generateVerticalCorridor(levelArea, rooms, roomNumber) {
return corridor
}
return nil
}
```

The `generateHorizontalCorridor`

function will control looking for possible rooms and if any are found attempt
to create the corridor...

```
func generateHorizontalCorridor(_ levelArea: CGRect, _ rooms: [Room], _ roomNumber: Int) -> Room? {
// Calculate the imaginary rectangle that extends the width of the level
// Place it at the bottom edge of the room
var horizontalExtension = CGRect( x: Int(levelArea.minX),
y: Int(frame.minY) ,
width: Int(levelArea.width),
height: Constants.Constraints.corridorSize)
repeat {
// Move the rectangle up a tile (first time this allows for missing the wall
horizontalExtension = horizontalExtension.offsetBy(dx: 0, dy: Constants.Constraints.floatTileSize)
// Find any rooms that intersect this area, that are to the left of this room
// and sort them so that the nearest one is the first in the results
let intersectingLeftRooms = rooms.filter(
{ $0.frame.intersects(horizontalExtension) &&
$0.leftEdge < leftEdge
}
).sorted { $0.leftEdge > $1.leftEdge }
// If we have a room, see if we can create the corridor, if it is return it
if let possibleRoom = intersectingLeftRooms.first,
let corridor = createHorizontalCorridor(horizontalExtension, possibleRoom, .left, roomNumber) {
return corridor
}
// Find any rooms that intersect this area, that are to the right of this room
// and sort them so that the nearest one is the first in the results
let intersectingRightRooms = rooms.filter(
{ $0.frame.intersects(horizontalExtension) &&
$0.rightEdge > rightEdge
}
).sorted { $0.rightEdge < $1.rightEdge }
// If we have a room, see if we can create the corridor, if it is return it
if let possibleRoom = intersectingRightRooms.first,
let corridor = createHorizontalCorridor(horizontalExtension, possibleRoom, .right, roomNumber) {
return corridor
}
// We've not created a corridor, if we have space to move the imaginary
// rectangle up then loop back round and try again
} while Int(horizontalExtension.maxY) < topEdge
return nil
}
```

Creating the corridor is a case of checking that the intersection can fit at least one floor tile, calculating the room dimensions,
creating the `Room`

and adding a door at each end. This means we need an additional `init`

for the `Room`

class that takes
dimensions rather than cols/rows, it will also give us the chance to draw a different colour for corridor floors...

```
public init(number: Int, rect: CGRect) {
self.number = number
self.cols = Int(rect.width) / Constants.Constraints.tileSize
self.rows = Int(rect.height) / Constants.Constraints.tileSize
self.node = SKShapeNode(rectOf: rect.size)
self.node.lineWidth = 0
self.node.name = "room\(self.number)"
self.node.position = CGPoint(x: rect.midX, y: rect.midY)
// Create a basic node using the size reduced by 2 tile sizes in each direction
let floor = SKShapeNode(rectOf: CGSize(width: Int(rect.width) - (Constants.Constraints.tileSize * 2),
height: Int(rect.height) - (Constants.Constraints.tileSize * 2)))
floor.lineWidth = 0
floor.fillColor = .yellow
self.node.addChild(floor)
}
```

Now that we have the new constructor, we can add the function to generate the corridor, we already have functions that will add the doors...

```
func createHorizontalCorridor(_ horizontalExtension: CGRect, _ possibleRoom: Room, _ direction: Constants.DoorWall, _ roomNumber: Int) -> Room? {
// Calculate the intersection with the room and check that the area can actually fit a corridor
let intersection = possibleRoom.frame.intersection(horizontalExtension)
if Int(intersection.height) == Constants.Constraints.corridorSize, Int(intersection.width) >= Constants.Constraints.tileSize {
// Calculate the dimensions based on the direction the corridor is going
let corridorRect = direction == .left
? CGRect(x: possibleRoom.rightEdge,
y: Int(horizontalExtension.minY),
width: abs(leftEdge - possibleRoom.rightEdge),
height: Constants.Constraints.corridorSize)
: CGRect(x: rightEdge,
y: Int(horizontalExtension.minY),
width: abs(possibleRoom.leftEdge - rightEdge),
height: Constants.Constraints.corridorSize)
// Create the room
let corridorRoom = Room(number: roomNumber, rect: corridorRect)
// Add the doors
corridorRoom.createConnectingDoor(toRoom: self, basedOnWall: direction == .left ? .right : .left)
corridorRoom.createConnectingDoor(toRoom: possibleRoom, basedOnWall: direction == .left ? .left : .right)
return corridorRoom
}
return nil
}
```

The code for generating vertical corridors is very similar, just looking in a different direction. Rather than repeat this here,
take a look at the completed `Playground`

on GitHub. Theres
no additional code to render the corridors as they are just rooms and will get processed along with the other rooms.

## Challenge 4 Completed

Orphaned rooms are now connected with corridors! All of the challenges have now been completed, (well sort of read the Closing Comments below).

### Closing Comments

Whilst the code in these articles explain the processes I went through, it doesn't show all the code I used to generate the levels in my game. As you run this code you will notice that there are times where "groups" of rooms are orphaned and will not be connected using the code supplied. There are few additional hoops that my app jumps through to ensure all rooms can be reached and I don't want to give away all of my code. But the challenges that I've left for you to solve are really not that complicated, if you get stuck feel free to drop me some questions.

There are also other improvements that could be made

- Corridors with bends in them
- Random walls that are checked first, so that there is more of a mix of vertical and horizontal corridors
- Picking the corridor direction based on proximity rather than direction

Thanks for reading.

The Playground for this article is available on GitHub

If you have any questions or comments you can reach me on Twitter.