SF Rogue Room Arrangement
This article continues covering the challenges I faced while creating SF Rogue.
Challenge 2
Now that we have our random rooms generated, we need to ensure that none of them overlap and where possible they should be placed adjacent to rooms they are close to, reducing the need for corridors.
We will continue using the same playground we created in the previous article, which can be found on GitHub.
Firstly we are going to add some Read-Only Computed Properties to the Room
class, so that we can retrieve information easily from the drawn node
.
var frame: CGRect {
self.node.frame
}
var height: CGFloat {
self.node.frame.height
}
var width: CGFloat {
self.node.frame.width
}
As we are going to be moving overlapping rooms we also need a function that will allow us to change the position of the rooms node
in the scene, add the following method after the properties you've just added...
public func moveTo(x: CGFloat = CGFloat.greatestFiniteMagnitude,
y: CGFloat = CGFloat.greatestFiniteMagnitude ) {
// Using 'fixed' default values means we only need to
// supply the values that need to change.
// If the default value is detected then the existing value is used
self.node.position = CGPoint(x: ( x == CGFloat.greatestFiniteMagnitude ? self.node.position.x : x ),
y: ( y == CGFloat.greatestFiniteMagnitude ? self.node.position.y : y ))
}
Now that we can move the room, we need to work out which rooms are actually overlapping and if they are, move them. The current Playground places all the rooms in close proximity of each other, so as we move one room out of the way of the current room there's a possibility that the new position could overlap another room.
We need a way to manage continuous overlapping and an indication when there are no furhter overlaps. Looping through each room, comparing it to rooms that are below it in the "stack" of rooms in the array and moving rooms that overlap, makes the process manageable. It will also have the effect of fanning out the rooms (I hope).
Add the following function to the Room
class...
public func removeOverlap(rooms: [Room]) -> Int {
// Used to highlight that intersections were found
var intersectionCount = 0
// Pulling out values from the frame, for simplicity
let thisFrame = self.frame
let thisRoomBottom = thisFrame.minY
let thisRoomTop = thisFrame.maxY
let thisRoomLeft = thisFrame.minX
let thisRoomRight = thisFrame.maxX
for otherRoom in rooms {
if thisFrame.intersects(otherRoom.frame) {
// Check the frames and if they intersect the otherRoom needs to move
intersectionCount += 1
// In order to keep the "randomness" of the rooms,
// we calculate new X and Y positions based on this rooms position.
// Using the nextBool function we can get varying layouts
// The height and width are halved because the anchor point is in the centre of the room
let newY = randomSource.nextBool()
? thisRoomBottom - (otherRoom.height / 2)
: thisRoomTop + (otherRoom.height / 2)
let newX = randomSource.nextBool()
? thisRoomLeft - (otherRoom.width / 2)
: thisRoomRight + (otherRoom.width / 2)
//Randomly pick which way to move the room
if randomSource.nextBool() {
otherRoom.moveTo(y: newY)
} else {
otherRoom.moveTo(x: newX)
}
if self.frame.intersects(otherRoom.frame) {
// they are still intersecting so change both values
otherRoom.moveTo(x: newX, y: newY)
}
}
}
return intersectionCount
}
This method give us the ability at a Room
level to ensure its not being overlapped by rooms that have been passed to it. All thats left is to call this from within the GameScene
.
The above code uses a new class variable called randomSource
to get some random true/false values. You'll need to add a private variable to the class...
private let randomSource = GKRandomSource.sharedRandom()
and an import of the GameplayKit
to the top of the class
import GameplayKit
Open the main Playground code and add the following loop to the end of the didMove
function...
var arrangedCount = 0
repeat {
arrangedCount = arrangeRooms()
} while arrangedCount > 0
This loop will repeat until we've been told there are no more overlapping rooms. Add the following function after the didMove
function...
func arrangeRooms() -> Int {
var overlappingCount = 0
// Loop through all the rooms, with the exception of the last one
for count in 0..<rooms.count-1 {
let thisRoom = rooms[count]
// Now remove the overlaps for this room and the rooms placed after it in the array
overlappingCount += thisRoom.removeOverlap(rooms: Array(rooms[count+1..<rooms.count]) )
}
return overlappingCount
}
The Playground is now ready to run, and should produce something like this in the LiveView...
What you'll probably notice is that although we've put the rooms next too each other in code, there appears to be gaps between them. This is being caused by the lineWidth
thats been set on the SKShapNode
in the Room
init
. Change the folloing line of code from...
self.node.lineWidth = 2
to...
self.node.lineWidth = 0
Now when running the Playground you should see something like this...
Its a bit tricky to see the rooms, so lets add a 'floor' shape to the room to give a border effect. Add the following code to the end of the init
function in the Room
class...
// Create a basic node using the size reduced by 2 tile sizes in each direction
let floor = SKShapeNode(rectOf: CGSize(width: width - (Constants.Constraints.tileSize * 2), height: height - (Constants.Constraints.tileSize * 2)))
floor.lineWidth = 0
floor.fillColor = .black
self.node.addChild(floor)
Run the code again and our border is back and the rooms are now adjacent to each other
Challenge 2 Complete!
Congratulations, we now have a number of random sized rooms, that don't overlap, being rendered in SpriteKit. The next challenge is creating doors between adjacent rooms.
The Playground for this article is available on GitHub
If you have any questions or comments you can reach me on Twitter.