To The Core

My Role in this Project

In this project, I've taken on the role of procedural programmer. My main responsibilities were working on a random spawning system and the level generator. This is a third-year project built with a large team in one full academic year with the goal to release the game on Steam.

Summary of my work:

  • Made a procedural level generator.
  • Prototyped different ways of generating levels.
  • Made a random spawning system for the level designers to control the randomness in the world.
  • Close communication with the level designers for feedback, bug reports and feature requests.
  • Wrote documentation for the designers that explains how to work with the random spawning system.
  • Improved code based on feedback during code reviews and reviewed code of others.

Details

Type
School Project
Genre
Platformer / Roguelite
My Role
Procedural Programmer
Team Size
28
Duration
8 Months
Engine
Unreal Engine 4
Language(s)
C++, Unreal Blueprints

Game Description

To the Core is a third-person action-platforming speedrunner with a roguelite structure. Run, jump and dash your way through procedurally generated levels filled with obstacles and hazards. Fight through heaps of enemies to save your father and set record times. The game can be downloaded for free on Steam.

Trailer

Code Snippets

Generating the Level

The designers create level chunks in Unreal Engine and add them to a data table. They fill in the data for each chunk, such as where the entrance and exits are located. The level is split up in two sections, but is capable of generating more.
void ALevelGenerator::GenerateLevel()
{
	bool succes = AddChunksFromDataTable();

	if (!succes) return;

	// Generate the sections
	endLocation = startLocation;
	for (int i = 1; i <= sectionsAmount; i++)
	{
		// Add all the chunks that are on hold to the list if they should appear in this section
		if (i > 1)
			for (int j = 0; j < holdChunks.Num(); j++)
				if (holdChunks[j].section == i)
					chunks[holdChunks[j].type].Add(holdChunks[j]);

		// Generate the next section
		GenerateSection(endLocation, i);

		// Remove all the chunks that are only allowed to appear in the current section
		if (i < sectionsAmount)
			for (auto& pair : chunks)
				for (int j = 0; j < pair.Value.Num(); j++)
					if (pair.Value[j].section == i)
						pair.Value.RemoveAt(j);
	}

	// Spawn the final tower as the last chunk
	SpawnLevelChunk(GetChunkData(EChunkType::FinalTower), endLocation + FVector(chunkSize.X * -0.5f, chunkSize.Y, 0), 0);

	// Empty all arrays
	chunks.Empty();
	usedChunks.Empty();
	holdChunks.Empty();
}

Generating a Single Section

Generating a section of chunks is split up in 4 functions. Creating the grid, generating a path on the grid, generate the boundaries around the path on the grid, generate the out of bounds environment pieces. This process will only store the necessary data in the grid. The actual level chunks are spawned based on the generated grid.
// Setup the grid for where the chunks can be placed
InitGrid();

// Generate a path of level chunks
GeneratePath();

// Generate the boundaries around the level chunks
GenerateBoundaries();

// Generate the external environment chunks surrounding the boundaries
GenerateExternalChunks();

Generating a Path

The GeneratePath function creates a path in the grid and stores where the entrance and exit is located for each individual chunk.
for (int i = 0; i < chunksAmount; i++)
{
    // Booleans that determine whether it should move vertical or horizontal. Also depends on rng.
    bool cannotMoveVertical = vertical <= 0 && i <= chunksAmount - 2;
    bool canMoveHorizontal = horizontal > 0 && i <= chunksAmount - 2;
    bool isFirstChunkOfSection = i == 0;

    if (cannotMoveVertical || (canMoveHorizontal && !isFirstChunkOfSection && RandBool()))
    {
        int xDir = RandBool() ? 1 : -1;
        int xLoc = currentPoint.X;

        // Try both left and right
        for (int j = 0; j < 2; j++)
        {
            if (GridCellIsEmpty(currentPoint + FIntPoint(xDir, 0)) && 
                !IsOutsideTraversalSpace(currentPoint.X + xDir, currentPoint.Y) && 
                HasEnoughSpaceForBoundaries(currentPoint.X + xDir, currentPoint.Y))
            {
                // Set the entrance and exit of the current and next point
                grid[currentPoint.Y][currentPoint.X].to = GetCardinal(xDir, 0);
                grid[currentPoint.Y][currentPoint.X + xDir].from = GetCardinal(-xDir, 0);

                currentPoint.X += xDir;
                horizontal--;
                break;
            }
            // Invert the direction if it can't move to the opposite direction
            xDir *= -1;
        }

        // If it made a horizontal path succesfuly, continue the for loop, else it will move verticaly
        if (xLoc != currentPoint.X)
            continue;
    }

    // If horizontal failed, move vertical
    grid[currentPoint.Y][currentPoint.X].to = GetCardinal(0, 1);
    grid[currentPoint.Y + 1][currentPoint.X].from = GetCardinal(0, -1);

    currentPoint.Y++;
    vertical--;
}

Links

Steam Page