When developing Susminer, a small 2d building/mining survival game, I needed to create a procedural world generator. And so I jumped head first by making a tile system, and then a basic bit of code to generate a small world, which gave me something like this.
So, then with this code working a bit I decided to make it expand out a bit horizontally. And then I also threw in some code to give it some extra variation on the bottom of the island.
Then, I made it start with nearly no height, and tried to make it generate in a near semi-circle design. But it’s far from perfect.
With some extra work, I got some more variety and tuned it to something a bit better. Procedural generation is really hard to get perfect, but “pretty good” is a fine metric for something like this.
Oh, and I also eventually added ore clusters which just replaces some blocks with an ore block.
Also, here’s the mess of the code I had for this at the end. Don’t worry, it got cleaned up later.
///C#, as it is in this final image
void GenerateMap()
{
Debug.Log("Generating Map");
Vector2 center = new Vector2(0, 0);
BlockData grass = (BlockData) blocks[1];
BlockData dirt = (BlockData)blocks[0];
BlockData stone = (BlockData)blocks[3];
BlockData ironOre = (BlockData)blocks[4];
int maxWidth = 80;
int maxHeight = 40;
int minWidth = 40;
int minHeight = 20;
int heightDiff = maxHeight - minHeight;
int widthDiff = maxWidth - minWidth;
int height = ((int)(Random.value * heightDiff) + minHeight);
int width = ((int)(Random.value * widthDiff) + minWidth);
int coreWidth = (int) (width / 3);
int coreHeight = (int)(height);
int baseHeight = 2;
int prevHeightVar = (int) (Random.value * 4);
int curHeight = baseHeight;
int maxPosYVar = 5;
int maxNegYVar = 5;
int posYVar = 1;
int negYVar = 1;
Debug.Log($"Height: {height}, Width: {width} ");
for (int x = (width/2) * -1; x < width/2; x++)
{
int calcX = x + width / 2;
int coreHeightDiff = coreHeight - curHeight;
int coreWidthDiff = coreWidth - calcX;
int coreWidthDiffLate = (coreWidth * 2);
float dist =0;
if (coreHeightDiff > 0 && coreWidthDiff > 0)
{
dist = (coreHeightDiff / coreWidthDiff);
Debug.Log($" coreWidthDiff: {coreWidthDiff} , coreHeightDiff: {coreHeightDiff} DIST: {dist}");
}
else
{
dist = ((0 - curHeight) / (width - calcX) * 2);
}
if (coreWidthDiff == 0)
{
Debug.Log($"Core Width Diff equals zero");
coreWidthDiff = 1;
}
if (calcX < coreWidth)
{
if (curHeight < coreHeight)
{
int temp = (int)(Random.value * (dist) );
curHeight += temp;
Debug.Log($"CurHeight added to by {temp}");
}
else
{
curHeight -= (int)(Random.value * 3);
}
} else if (calcX >= coreWidth && calcX < (coreWidth * 2))
{
curHeight = coreHeight;
Debug.Log("Is Centered");
}
else
{
Debug.Log($"CurHeight added to by {dist}");
curHeight += (int)(Random.value * (dist));
}
if (curHeight <= 0) { curHeight = 1; }
if (posYVar < maxPosYVar)
{
int coinflip = (int)(Random.value * 5);
if (coinflip > 0 && coinflip <= 2 )
{
posYVar += (int)(Random.value * 2);
}
else if (coinflip == 0)
{
posYVar -= (int)(Random.value * 2);
}
}
else
{
posYVar -= (int)(Random.value * 2);
}
if (posYVar < 0) { posYVar = 2; }
if (negYVar < maxNegYVar)
{
int coinflip = (int)(Random.value * 2);
if (coinflip > 0)
{
negYVar += (int)(Random.value * 4);
}
else
{
negYVar -= (int)(Random.value * 2);
}
}
else
{
negYVar -= (int)(Random.value * 4);
}
if (negYVar <= 0) { negYVar = 1; }
int startHeight = posYVar;
int endHeight = (curHeight + negYVar) * -1;
Debug.Log($"StartHeight: {startHeight} End Height: {endHeight}");
for (int y = startHeight; y > endHeight; y--)
{
if (y == startHeight)
{
map.Add(new BlockInstance(grass,(short) x, (short)y));
}
else if (y < startHeight && y >= startHeight - 2)
{
map.Add(new BlockInstance(dirt, (short)x, (short)y));
}
else
{
map.Add(new BlockInstance(stone, (short)x, (short)y));
}
}
}
//Adding Ore Clusters
int clusterSizeMin = 4;
int clusterSizeMax = 16;
int maxClusters = 12;
int minClusters = 4;
int clusters = minClusters + ((int) (Random.value * (maxClusters - minClusters)));
Debug.Log($"Clusters to be spawned: {clusters}");
for (int i = 0; i < clusters; i++)
{
int clusterSize = clusterSizeMin + ((int) (Random.value * (clusterSizeMax - clusterSizeMin)));
Vector2 [] cPos = new Vector2 [clusterSize];
int clusterX = 0;
int clusterY = 0;
cPos[0] = new Vector2(0, 0);
for (int ii = 1; ii < clusterSize; ii++)
{
int die = (int)(Random.value * 4);
if (die == 1) //drawing a square for remainder on one
{
int diff = clusterSize - i;
int count = 0;
if (diff % 2 == 0)
{
for (int x = 0; x < diff / 2; x++)
{
for (int y = 0; y < diff / 2; y++)
{
if (ii + count >= clusterSize) { break; }
cPos[ii + count] =
new Vector2(cPos[ii - 1].x +
x, cPos[ii-1].y + y);
count++;
}
}
break;
}
else
{
for (int x = 0; x <( diff / 2) -1; x++)
{
for (int y = 0; y < diff / 2; y++)
{
if (ii + count >= clusterSize) { break; }
cPos[ii + count] = new Vector2(cPos[ii - 1].x + x, cPos[ii - 1].y + y);
}
}
break;
}
}
else if (die ==2) // going down
{
cPos[ii] = new Vector2(cPos[ii].x, cPos[ii].y + 1);
}
else if (die == 3) // going down
{
cPos[ii] = new Vector2(cPos[ii].x + 1, cPos[ii].y);
}
}
BlockInstance start = (BlockInstance) map[ (int) (Random.value * map.Count) ];
while (start.blockData != stone)
{
start = (BlockInstance) map[(int)(Random.value * map.Count)];
}
short startX = start.x;
short startY = start.y;
for (short s = 0; s < clusterSize; s++)
{
BlockInstance block = findBlock(startX + (short) cPos[s].x, startY + (short)cPos[s].y);
block.blockData = ironOre;
}
}
}