I moved to a new URL! Check it out!

Game Jam Procedural Generation Part III

Game Jam Procedural Generation Part III
In the last episode of Game Jam Procedural Generation I talked a lot about generating the base of the platforming level in Starforger II, and carving out rooms into the ground of the level.

Image


So now we have a basic level that with a bunch of empty rooms below the ground. The next step is going to be connecting those rooms together. To do this I built a quick class called a TunnelSnake (tunnel snakes rule) to dig tunnels from any point on the map to any other point. Here's the full source of that class:
class TunnelSnake {
public int X;
public int Y;
public int Width = 1;
public int Height = 2;

public int EndX;
public int EndY;

int verticalSteps;
int verticalStepMax = 4;
int forceHorizontal = 0;
int forceHDirection = 1;

public TunnelSnake(int x, int y, int endX, int endY) {
X = x;
Y = y;
EndX = endX;
EndY = endY;
}

public void Dig(GridCollider grid) {
while (X != EndX || Y != EndY) {
grid.SetRect(X, Y, Width, Height, false);

if (Rand.Chance(50) || forceHorizontal > 0) {

if (forceHorizontal > 0) {
X += forceHDirection;
Height = 2;
X = (int)Util.Clamp(X, 2, grid.TileColumns - 2);
}
else {
X += Math.Sign(EndX - X);
}

forceHorizontal--;
if (forceHorizontal == 0) {
verticalSteps = 0;
}
}
else {
verticalSteps++;

Y += Math.Sign(EndY - Y);

if (verticalSteps == verticalStepMax) {
forceHorizontal = Rand.Int(3, 15);
forceHDirection = Rand.Sign;
}
}

if (Rand.Chance(50)) {
if (Rand.Chance(50)) {
Width += Rand.Sign;
Width = (int)Util.Clamp(Width, 1, 5);
}
if (Rand.Chance(50)) {
Height += Rand.Sign;
Height = (int)Util.Clamp(Height, 2, 6);
}
}
}
}
}

A TunnelSnake is created with a start position, and an end position. Then when the "dig" function is called on the grid in the level it will advance to its end position and remove tiles in the grid that it's passing through. The dig function has some measures in it to try and prevent totally impossible tunnels for the player to navigate. For example it will keep track of how many successive vertical moves it has made, and if it has made too many in a row it will be forced to move horizontally for awhile. This is an attempt to prevent the snake from digging tunnels that are purely vertical which would be incredibly difficult for the player to navigate.

Image


The dig will also choose a random size tunnel to dig. The smallest possible hole it can cut out is 1 x 2 tiles, but every time it digs it has a random chance to adjust its size in various ways, but it can never go smaller than 1 x 2.

So here is how the TunnelSnake is used after the rooms have been created.
// sort rooms by height I guess?
var sortedRooms = (from r in rooms
orderby r.Y descending
select r).ToList<Room>();

// Connect the rooms with digging tunnels
for(var i = 0; i < rooms.Count; i++) {
if (i < rooms.Count - 1) {
var room = sortedRooms[i];
var nextRoom = sortedRooms[i + 1];

var xstart = room.X + Rand.Int(room.Width - 1);
var ystart = room.Y + Rand.Int(room.Height - 1);

var xend = nextRoom.X + Rand.Int(nextRoom.Width - 1);
var yend = nextRoom.Y + Rand.Int(nextRoom.Height - 1);

var tunnel = new TunnelSnake(xstart, ystart, xend, yend);
tunnel.Dig(grid);
}
else {
//make this room exit
var room = sortedRooms[i];

var xstart = room.X + Rand.Int(room.Width - 1);
var ystart = room.Y + Rand.Int(room.Height - 1);

var tunnel = new TunnelSnake(xstart, ystart, xstart + Rand.Int(-5, 5), 0);

tunnel.Dig(grid);
}
}

I start here by sorting the list of rooms by their Y value. I want to iterate through the rooms starting with the lowest one first so that I can almost be sure that the tunnels will run from the bottom most room to the top most room and then out to the surface.

Image


When I iterate through the rooms all I'm doing is just choosing a start position from the current room and an end position in the next room on the list. After I have that I can let the TunnelSnake loose and have it dig a tunnel from point A to point B. This isn't the most elegant solution, but for a game jam game it creates passable tunnels. The final room is also a special case and the tunnel from that room goes to the very top of the map. This is an almost sure fire way to make sure that the rooms are accessible.
// dig random rooms to the surface
int roomsToSurface = Rand.Int(1, 4);
for (var i = 0; i < roomsToSurface; i++ ) {
var roomId = Rand.Int(0, rooms.Count);
var room = rooms[roomId];

var xstart = room.X + Rand.Int(room.Width - 1);
var ystart = room.Y + Rand.Int(room.Height - 1);

var tunnel = new TunnelSnake(xstart, ystart, xstart + Rand.Int(-5, 5), 0);
tunnel.Dig(grid);
}

One last move to make sure that rooms are accessible is that I grab random rooms from the list and have a TunnelSnake dig to the surface from that room as well. This just increases the odds that I wont have a totally broken level.

Image


Now that I have rooms and tunnels I can figure out a cool way to fill in some of the unused space. At this point my generation was working pretty well, but underground there was just a lot of big empty space and it was hard to navigate. The next step was to fill some of that empty space with platforms that the player can use to explore the rooms and tunnels, and areas above the ground as well.
// place random platforms in empty spaces
for (var yy = 10; yy < grid.TileRows; yy++) {
for (var xx = 0; xx < grid.TileColumns; xx++) {
var width = Rand.Int(3, 8);
var height = Rand.Int(1, 3);
var padding = Rand.Int(1, 4);
//look for empty rect?
if (CheckRect(xx - padding, yy - padding, width + padding * 2, height + padding * 2)) {
continue;
}
if (Rand.Chance(config.Platforms)) {
grid.SetRect(xx, yy, width, height, true);
}
}
}

I start by traversing the entire grid of the level from a Y position of 10. I choose a random width and height for the platform that I want to create. I also choose a padding value which is how much empty space should exist around the platform. Using this data I check to see if I can possibly place the desired platform in that location. If it is possible then I check against a random chance from the config object, and if I pass that final test then I generate the platform.

Image


I run this code for every single tile in the level, and it may seem pretty weird but it actually does generate some cool results most of the time. Sometimes levels have an insanely high platform chance, and the level is super cluttered, but it's fun to see that happen since not all planets have that feature.

Image


Next I actually run the same code again, but for the underground areas. Having platforms underground I found to be very important since the player might run into some trouble spots when just relying on the rooms and tunnels. I essentially run that same exact code again except I start at the Y position of the ground level, and my platform generation chance is hard set to 33%.

Now there are some platforms in the level along with the rooms and tunnels. Next up I add some random spots of "decay" to the level!
// select areas to decay
int decaySpots = config.DecaySpots;

for (var i = 0; i < decaySpots; i++ ) {
var xx = Rand.Int(16, grid.TileColumns - 16);
var yy = Rand.Int(16, grid.TileRows - 16);

int maxRadius = Rand.Int(3, 30);
for (var r = 0; r < maxRadius; r++) {
for (var a = 0; a < 360; a += 30) {
var decayx = xx + (int)Util.PolarX(a, r);
var decayy = yy + (int)Util.PolarY(a, r);

if (Rand.Chance(config.DecayChance)) {
grid.SetTile(decayx, decayy, false);
}
}
}
}

This looks a little crazy. Basically what is happening here is that I'm choosing a random spot on the grid to start. Then I move away from that spot by spiraling outwards. That's why I'm looping through an r value (for radius) and an a value (for angle.) I don't know why I chose to do this, I just figured it'd be cool.

While I'm iterating through tiles by spiraling outwards I check a random chance value from the config values passed in to the generator. If I get a success on that random chance I remove the tile from the grid. The result is kind of a freaky looking star shaped decay of missing tiles from areas in the map, or sometimes it's just a bunch of missing tiles scattered about. Anyway, it looks cool.

Image


Image


I actually then do the same exact thing after this except I add tiles instead of removing them. This is where the IslandSpots value comes in on the config object. This can create some bizarre looking platform formations in the air sometimes, but there's also a small chance that it can create impassible terrain, but I haven't seen it happen yet.

Image


Okay this post has gone on long enough! In the next and final part of this series I will talk about the last of the level generation including placing breakable blocks, enemies, a landing path for the ship, and finally finishing up the room that the treasure is in.
new comment!

Post your comment!

Name
Email
Comment