How to Make Rules for Procedural Generation?

Hello everyone.

I’m new into procedural generation and I am stuck at making rules to control locations objects are placed. The system I have created is based off of Ian Shadden’s design. Here is an example of my code and then a photo of what it does.

// Sets default values
ARoomGeneration::ARoomGeneration()
{	
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	ISM_Floor = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("ISM_Floor"));
	ISM_Floor->SetStaticMesh(SM_Floor);
	this->SetRootComponent(ISM_Floor);

	ISM_Wall = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("ISM_Wall"));
	ISM_Wall->SetStaticMesh(SM_Wall);
	ISM_Wall->AttachTo(RootComponent);

	ISM_Door = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("ISM_Door"));
	ISM_Door->SetStaticMesh(SM_Door);
	ISM_Door->AttachTo(RootComponent);

}

//Called when the object is placed, changed, or spawned in editor/scene
void ARoomGeneration::OnConstruction(const FTransform& Transform)
{	
	//Select Door Count for exit doors to be spawned
	DoorCount = FMath::RandRange(0, 3);

	GenerateFloor();
	GenerateWall();
	GenerateDoors();

	//Set Location of child instances to match the  floor's location
	ISM_Wall->SetRelativeLocation(FVector(0, 0, 0));
	ISM_Door->SetRelativeLocation(FVector(0, 0, 0));
}

// Called when the game starts or when spawned
void ARoomGeneration::BeginPlay()
{
	Super::BeginPlay();
}

// Called every frame
void ARoomGeneration::Tick( float DeltaTime )
{
	Super::Tick( DeltaTime );

}

//Generate a tile system to create a floor
void ARoomGeneration::GenerateFloor()
{
	ISM_Floor->ClearInstances();
	for (int32 n = 0; n < GridSizeX * GridSizeY; n++)
	{
		FTransform newTransform = FTransform(FVector((n / GridSizeY) * TileSize, (n % GridSizeY) * TileSize, 0));
		ISM_Floor->AddInstance(newTransform);
	}				
}

//Generate walls on the outer most edges of the floor
void ARoomGeneration::GenerateWall()
{
	ISM_Wall->ClearInstances();
	//Generate North Walls
	for(int32 n = 0; n < GridSizeX; n++)
	{
		FTransform newTransform = FTransform(FVector((n % GridSizeX) * TileSize, (n / GridSizeX) * TileSize, 300));
		ISM_Wall->AddInstance(newTransform);
	}

	//Generate West Walls
	for (int32 n = 1; n < GridSizeY; n++)
	{
		FTransform newTransform = FTransform(FVector((n / GridSizeY) * TileSize, n % GridSizeY * TileSize, 300));
		ISM_Wall->AddInstance(newTransform);
	}

	//Generate East Walls
	for (int32 n = 1; n < GridSizeY; n++)
	{
		FTransform newTransform = FTransform(FVector(GridSizeX * TileSize - TileSize, n % GridSizeY * TileSize, 300));
		ISM_Wall->AddInstance(newTransform);
	}

	//Generate South Walls
	for (int32 n = 1; n < GridSizeX - 1; n++)
	{
		FTransform newTransform = FTransform(FVector((n % GridSizeX) * TileSize, GridSizeY * TileSize - TileSize, 300));
		ISM_Wall->AddInstance(newTransform);
	}

}

//Replace single instanced wall components with Door Meshes
void ARoomGeneration::GenerateDoors()
{
	ISM_Door->ClearInstances();
	bHasEntryDoor = false;
	bHasExitDoor = false;

	//Create an entry door to give access to the room
	if(bHasEntryDoor == false)
	{
		int32 randInt = FMath::RandRange(0, ISM_Wall->GetInstanceCount());
		FTransform doorTransform;

		ISM_Wall->GetInstanceTransform(randInt, doorTransform);
		ISM_Door->AddInstance(doorTransform);
		ISM_Wall->RemoveInstance(randInt);
		bHasEntryDoor = true;
	}

	//Create exit doors if door count is greater than 0
	if(bHasEntryDoor == true && bHasExitDoor == false && DoorCount > 0)
	{
		for(int32 d = 0; d < DoorCount; ++d)
		{
			int32 randInt = FMath::RandRange(0, ISM_Wall->GetInstanceCount());
			FTransform doorTransform;

			ISM_Wall->GetInstanceTransform(randInt, doorTransform);
			ISM_Door->AddInstance(doorTransform);
			ISM_Wall->RemoveInstance(randInt);
		}

		bHasExitDoor = true;
	}
}

grey tiles are the floor, red tiles are walls, and green tiles are the doors

To be more specific about what I’m looking to learn here is a list of questions.

  • How do i keep doors from spawning in corners?
  • How could I make the walls spawn in different patterns, like in the shape of Tetris pieces?

I think if those questions are answered I will be able to figure out the rest. I’m not looking for people to create the code for me just an explanation or some examples of the logic used would be great.

Thanks in advance,
Russ.

I figured out the doors spawning in corners problem by adjusting the randInt that is used to select wall instances. Here is my working code.

//Replace single instanced wall components with Door Meshes
void ARoomGeneration::GenerateDoors()
{
	ISM_Door->ClearInstances();
	bHasEntryDoor = false;
	bHasExitDoors = false;

	bHasFirstExit = false;
	bHasSecondExit = false;
	bHasThirdExit = false;

	//Create an entry door to give access to the room
	//Entry Door will go on the West Wall
	if(bHasEntryDoor == false)
	{
		int32 randInt = FMath::RandRange(2, GridSizeX - 2);
		FTransform doorTransform;

		ISM_Wall->GetInstanceTransform(randInt, doorTransform);
		ISM_Door->AddInstance(doorTransform);
		ISM_Wall->RemoveInstance(randInt);
		bHasEntryDoor = true;
	}
	//Create exit doors if door count is greater than 0
	if(bHasEntryDoor == true && DoorCount > 0)
	{
		for(int32 d = 0; d < DoorCount; d++)
		{
			//Create Exit Door on South Wall
			if(bHasFirstExit == false && DoorCount > 0)
			{
				int32 randInt = FMath::RandRange(GridSizeX, GridSizeX + GridSizeY - 4);
				FTransform doorTransform;

				ISM_Wall->GetInstanceTransform(randInt, doorTransform);
				ISM_Door->AddInstance(doorTransform);
				ISM_Wall->RemoveInstance(randInt);
				bHasFirstExit = true;
			}
			//Create exit Door on North Wall
			else if (bHasFirstExit == true && bHasSecondExit == false && DoorCount > 1)
			{
				int32 randInt = FMath::RandRange(GridSizeX + GridSizeY -3, GridSizeX + GridSizeY * 2 - 6);
				FTransform doorTransform;

				ISM_Wall->GetInstanceTransform(randInt, doorTransform);
				ISM_Door->AddInstance(doorTransform);
				ISM_Wall->RemoveInstance(randInt);
				bHasSecondExit = true;
			}
			//Create Exit Door on East Wall
			else if (bHasFirstExit = true && bHasSecondExit == true && bHasThirdExit == false && DoorCount == 3)
			{
				int32 randInt = FMath::RandRange(GridSizeX + GridSizeY * 2 - 5, GridSizeX * 2 + GridSizeY * 2 - 8);
				FTransform doorTransform;

				ISM_Wall->GetInstanceTransform(randInt, doorTransform);
				ISM_Door->AddInstance(doorTransform);
				ISM_Wall->RemoveInstance(randInt);
				bHasThirdExit = true;
			}
			
		}
	}
}

I still don’t know how to get walls to spawn in patterns other than straight lines. If any one knows a way to do this please answer.

Drawing straight lines involves iterative changes in one dimension. Drawing other shapes involves changes in multiple dimensions, and often using combinations or a sequence of different rules.

Assuming you can convert your grid points into location vectors, I’ll give a couple examples to calculate the integer grid coordinates.

Right Triangle:

function BuildRightTriangle(int32 length)
{
    int32 x;
    int32 y;

    //draw left edge
    for (y = 0; y <= length; y++) {
        PlaceWall(0, y);

    }

    //draw bottom wall
    //(skip 0 because it was placed above)
    for (x = 1; x <= length; x++) {
        PlaceWall(x, 0);

    }

    //draw hypotenuse wall
    //(skip first and last because they were placed above)
    for (x = 1; x < length; x++) {
        PlaceWall(x, length - x);

    }

}

If you wanted a more robust algorithm you could adjust whether the vertical edge is on the left or right, and you can calculate the slope and multiply the x value in the hypotenuse by the slope to get an independent value; but the above should be enough to give you the concept.

Isosceles Triangle:

function BuildIsosceleseTriangle(int32 height)
{
    int32 x;
    int32 y;
    int32 width = height * 2;

    //draw bottom
    for (x = 0; x <= width; x++) {
        PlaceWall(x, 0);

    }

    //draw left edge
    for (y = 1; y <= height; y++) {
        x = y;
        PlaceWall(x, y);

    }

    //draw right edge
    for (y = height - 1; y > 0; y--) {
        x++;
        PlaceWall(x, y);

    }

}

Again, you can do more to accommodate different slopes.

Now for something a bit more complicated.

Flat-Topped Hexagon:

function BuildHex(int32 length)
{
    int32 x = 0;
    int32 y = 1 + (2 * length);
    int32 i;

    //build left sides
    for (i = 0; i <= length; i++) {
        PlaceWall(x, length - i);
        PlaceWall(x, length + 1 + i);
        x++;

    }
    
    //build middle sides (top and bottom)
    for (i = 0; i <= length; i++) {
        PlaceWall(x, 0);
        PlaceWall(x, y);
        x++;

    }

    //build right sides
    for (i = length; i >= 0; i--) {
        PlaceWall(x, length - i);
        PlaceWall(x, y);
        x++;
        y--;

    }

}

This hexagon is squished, but again, it’s just to give you the idea.

Here’s how you can do a T-shaped Tetris piece:

function BuildTetrisT()
{
    for (int32 y = 0; y < 3; y++) {
        PlaceWall(0, y);

    }
    PlaceWall(1, 1);

}

Hopefully this will give you the general idea and you can start to figure out whatever shapes you want.

Thank you for the post but it doesn’t quite give me what I’m looking for. Let me elaborate on what I’m trying to accomplish. I generate the west, south, north, and east walls in that order. What I want to know exactly is how do i make the walls generate with a non-diagonal bend. For instance, instead of the west wall spawning in a straight line, it generates 4 tiles from 0,0 to 0,4, then makes tiles from 0,4 to 4,4, and finally finishes the wall from 4,4 to 4,10.

Again i’m not looking for a handout but an explanation or simple example of the math and logic for this.

You’ve already got the logic for what you want to do, you just need to build the for-loops to do exactly what you describe:

“instead of the west wall spawning in a straight line, it generates 4 tiles from 0,0 to 0,4, then makes tiles from 0,4 to 4,4, and finally finishes the wall from 4,4 to 4,10.”

//generates 4 tiles from 0,0 to 0,4
for (y=0; y < 4; y++) {
    BuildWall(0, y);

}

//makes tiles from 0,4 to 4,4
for (x=0; x < 4; x++) {
    BuildWall(x, 4);

}

//finish the wall from 4,4 to 4,10
for (y = 4; y < 11; y++) {
    BuildWall(4, y);

}

You can convert this into a more robust method like this:

void BuildWallWithBend(int32 x, int32 y, int32 bendAt, int32 bendEnd, int32 wallEnd) {

    //build the top portion of the wall
    for (y; y < bendAt; y++) {
        BuildWall(x, y);

    }

    //makes horizontal wall
    for (x; x < bendEnd; x++) {
        BuildWall(x, bendAt);

    }

    //finish the wall
    for (y = bendAt; y <= wallEnd; y++) {
        BuildWall(bendEnd, y);

    }

}

x and y are the grid coordinates where you want your wall to start; bendAt is the y coordinate where you want the wall to turn horizontal, bendEnd is the x coordinate where you want the wall to turn down again, and wallEnd is the y coordinate end of your wall.

Alternatively, you can build a couple methods that build horizontal and vertical segments, then feed them start and end coordinates and call them in sequence.

i.e.

void BuildHorizontalWall (int32 x, int32 y, int32 length) {
    for (int32 i = 0; i < length; i++) {
        BuildWall(x, y);
        x++;

    }

}

void BuildVerticalWall (int32 x, int32 y, int32 length) {
    for (int32 i = 0; i < length; i++) {
        BuildWall(x, y);
        y++;

    }

}

void BuildBendyWall (int32 x, int32 y, int32 bendAt, int32 bendEnd, int32 wallEnd) {
    BuildVerticalWall(x, y, bendAt);
    y = bendAt;
    BuildHorizontalWall(x, y, bendEnd);
    x = bendEnd;
    BuildVerticalWall(x, y, wallEnd + 1);

}

Thank you. I understand now.