In part 2 of this tutorial series we created the obstacle and level classes and we created blueprints from those classes.
In this part of the tutorial we are going to spawn level parts in the game using a method called procedural level generation.
Level Spawner Class
In the C++ Classes -> SideRunner, Right Click -> New C++ Class. Make sure the class inherits the Actor class, name the class LevelSpawner and click Create Class button.
Open LevelSpawner.h file in Visual Studio, and add the following line of code above the class declaration:
class ABaseLevel;
We are forward declaring the BaseLevel class because we are going to add references to level parts that we created in the previous part of this tutorial series.
At the bottom of the class add the following lines:
public:
UFUNCTION()
void SpawnLevel(bool IsFirst);
UFUNCTION()
void OnOverlapBegin(UPrimitiveComponent* OverlappedComp,
AActor* OtherActor, UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
The UFUNCTION keyword above the function is a C++ function that is recognized by the Unreal Engine 4 (UE4) reflection system e.g. it’s a keyword we use to denote that this function is a C++ function that we will use in the class.
The name of the function is self explanatory, we are going to use the function to spawn level parts in the game.
OnOverlapBegin is a function that we will use to detect collision between the LevelSpawner and other actors.
Moving forward, below the lines where we declared the functions above, add the following lines:
protected:
UPROPERTY(EditAnywhere)
TSubclassOf Level1;
UPROPERTY(EditAnywhere)
TSubclassOf Level2;
UPROPERTY(EditAnywhere)
TSubclassOf Level3;
UPROPERTY(EditAnywhere)
TSubclassOf Level4;
UPROPERTY(EditAnywhere)
TSubclassOf Level5;
UPROPERTY(EditAnywhere)
TSubclassOf Level6;
UPROPERTY(EditAnywhere)
TSubclassOf Level7;
UPROPERTY(EditAnywhere)
TSubclassOf Level8;
UPROPERTY(EditAnywhere)
TSubclassOf Level9;
UPROPERTY(EditAnywhere)
TSubclassOf Level10;
TArray LevelList;
public:
int RandomLevel;
FVector SpawnLocation = FVector();
FRotator SpawnRotation = FRotator();
FActorSpawnParameters SpawnInfo = FActorSpawnParameters();
We are going to use RandomLevel variable to randomize the level part that we will spawn in the game.
The SpawnLocation and SpawnRotation variables are going to serve as the location and rotation for the new level part we will spawn, and SpawnInfo is required when we spawn a new actor, so we need to pass it as a parameter but we will not do anything with it.
Before we proceed to code the functionality in the LevelSpawner.cpp file, I am going to leave the finished LevelSpawner.h file below as a reference:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "LevelSpawner.generated.h"
class ABaseLevel;
UCLASS()
class SIDERUNNER_API ALevelSpawner : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ALevelSpawner();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
public:
UFUNCTION()
void SpawnLevel(bool IsFirst);
UFUNCTION()
void OnOverlapBegin(UPrimitiveComponent* OverlappedComp,
AActor* OtherActor, UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
protected:
APawn* Player;
UPROPERTY(EditAnywhere)
TSubclassOf Level1;
UPROPERTY(EditAnywhere)
TSubclassOf Level2;
UPROPERTY(EditAnywhere)
TSubclassOf Level3;
UPROPERTY(EditAnywhere)
TSubclassOf Level4;
UPROPERTY(EditAnywhere)
TSubclassOf Level5;
UPROPERTY(EditAnywhere)
TSubclassOf Level6;
UPROPERTY(EditAnywhere)
TSubclassOf Level7;
UPROPERTY(EditAnywhere)
TSubclassOf Level8;
UPROPERTY(EditAnywhere)
TSubclassOf Level9;
UPROPERTY(EditAnywhere)
TSubclassOf Level10;
TArray LevelList;
public:
int RandomLevel;
FVector SpawnLocation = FVector();
FRotator SpawnRotation = FRotator();
FActorSpawnParameters SpawnInfo = FActorSpawnParameters();
};
The first thing we are going to add in the LevelSpawner.cpp file are the includes of all classes we will use in that file. Below the first #include in the file add the following lines:
#include "BaseLevel.h"
#include "Engine.h"
#include "Components/BoxComponent.h"
All the magic in regards to spawning level parts is going to happen inside the SpawnLevel function. Here are the first lines we will add in that function:
void ALevelSpawner::SpawnLevel(bool IsFirst)
{
SpawnLocation = FVector(0.0f, 1000.0f, 0.0f);
SpawnRotation = FRotator(0, 90, 0);
if (!IsFirst)
{
ABaseLevel* LastLevel = LevelList.Last();
SpawnLocation = LastLevel->GetSpawnLocation()->GetComponentTransform().GetTranslation();
}
RandomLevel = FMath::RandRange(1, 10);
ABaseLevel* NewLevel = nullptr;
}
First we set the values for the SpawnLocation and SpawnRotation. If you are wondering why did I set 1000 as a parameter for the Y axis in the SpawnLocation vector, this is because we set the scale of the level part at 10, which means the level part will be 1000 units or cm long in the game, and in order to position every level part next to each other, we need to move it by 1000 units.
Same goes for the Y value of the SpawnRotation. How the camera is positioned for our game, we need to rotate the level part by 90 degrees on the Y axis so that we can see it properly and be able to play the game.
Next we use the IsFirst parameter and test if the level part that we are spawning is not the first level part. We use the exclamation mark(!) in front of the IsFirst variable which means if the value is false, the exclamation mark will make it the opposite and that is true, and if the value is true, the exclamation mark will make it the opposite which is false.
So essentially we are testing if the level part that is being spawned is not the first level part. The reason for this is because every level part we spawn we will put it in the LevelList array, this way we can keep track of the level parts that are currently in the game and we can always access the last level part in order to get its location.
We need the location of the last level part in order to spawn the next part after it:
We can get the last level part from the array by using the Last function of the array. After that, we can access the location of the last part by using the GetSpawnLocation function, then calling GetComponentTransform to get the transform component of the last level part, and lastly calling GetTranslation function we get the location of the last level part.
Next we use RandRange function from FMath to generate a random number between 1 and 10. This is because we have 10 level parts and based on the number that is returned by the RandRange function we will spawn that level part in the game.
The NewLevel variable is there so that we can store a reference to the new level part that we spawn in the level. If we don’t get a reference to the new level part we will not be able to pass it to the LevelList array and save it.
We set the initial value of NewLevel to be nullptr or null pointer, and when we create a new level part we will store it in NewLevel variable.
Moving forward, after we create the NewLevel variable, add the following lines of code:
if (RandomLevel == 1)
{
NewLevel = GetWorld()->SpawnActor(Level1,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 2)
{
NewLevel = GetWorld()->SpawnActor(Level2,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 3)
{
NewLevel = GetWorld()->SpawnActor(Level3,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 4)
{
NewLevel = GetWorld()->SpawnActor(Level4,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 5)
{
NewLevel = GetWorld()->SpawnActor(Level5,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 6)
{
NewLevel = GetWorld()->SpawnActor(Level6,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 7)
{
NewLevel = GetWorld()->SpawnActor(Level7,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 8)
{
NewLevel = GetWorld()->SpawnActor(Level8,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 9)
{
NewLevel = GetWorld()->SpawnActor(Level9,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 10)
{
NewLevel = GetWorld()->SpawnActor(Level10,
SpawnLocation, SpawnRotation, SpawnInfo);
}
if (NewLevel)
{
if (NewLevel->GetTrigger())
{
NewLevel->GetTrigger()->OnComponentBeginOverlap.
AddDynamic(this, &ALevelSpawner::OnOverlapBegin);
}
}
First we test if we have a NewLevel variable, or to be more precise, we are testing if NewLevel is not equal to nullprt, which means we can write:
if (NewLevel)
{
}
Or we can write:
if (NewLevel != nullptr)
{
}
Both of these lines of code are testing for the same thing. After that we are testing if we have the trigger from the NewLevel variable by calling its GetTrigger function.
This is the same thing as with the NewLevel variable, because essentially we are testing if the trigger of NewLevel variable that is returned by GetTrigger function is not equal to nullptr.
If both of these if statements are true, then we get the trigger of NewLevel and we use its OnComponentBeginOverlap function to add a function listener which is our OnOverlapBegin function that will be informed when an actor collides with the trigger of NewLevel variable.
This this trigger is the one we declared in BaseLevel.h file:
// VARIABLE DECLARED IN BaseLevel.h
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "My Triggers")
UBoxComponent* Trigger;
LevelList.Add(NewLevel);
if (LevelList.Num() > 5)
{
LevelList.RemoveAt(0);
}
First we add the NewLevel to the array by using its Add function. After that we test if number of elements that are inside the LevelList array are greater than 5, we do this using the Num function.
If that is true, then we will remove the first element in that array using the RemoveAt function and passing 0 as the parameter because RemoveAt function will remove the element that is on the index we specify as the parameter, and we set that index to 0.
This is the final version of the SpawnLevel function:
void ALevelSpawner::SpawnLevel(bool IsFirst)
{
SpawnLocation = FVector(0.0f, 1000.0f, 0.0f);
SpawnRotation = FRotator(0, 90, 0);
if (!IsFirst)
{
ABaseLevel* LastLevel = LevelList.Last();
SpawnLocation = LastLevel->GetSpawnLocation()->GetComponentTransform().GetTranslation();
}
RandomLevel = FMath::RandRange(1, 10);
ABaseLevel* NewLevel = nullptr;
if (RandomLevel == 1)
{
NewLevel = GetWorld()->SpawnActor(Level1,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 2)
{
NewLevel = GetWorld()->SpawnActor(Level2,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 3)
{
NewLevel = GetWorld()->SpawnActor(Level3,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 4)
{
NewLevel = GetWorld()->SpawnActor(Level4,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 5)
{
NewLevel = GetWorld()->SpawnActor(Level5,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 6)
{
NewLevel = GetWorld()->SpawnActor(Level6,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 7)
{
NewLevel = GetWorld()->SpawnActor(Level7,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 8)
{
NewLevel = GetWorld()->SpawnActor(Level8,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 9)
{
NewLevel = GetWorld()->SpawnActor(Level9,
SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 10)
{
NewLevel = GetWorld()->SpawnActor(Level10,
SpawnLocation, SpawnRotation, SpawnInfo);
}
if (NewLevel)
{
if (NewLevel->GetTrigger())
{
NewLevel->GetTrigger()->OnComponentBeginOverlap.
AddDynamic(this, &ALevelSpawner::OnOverlapBegin);
}
}
LevelList.Add(NewLevel);
if (LevelList.Num() > 5)
{
LevelList.RemoveAt(0);
}
}
void ALevelSpawner::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
SpawnLevel(false);
}
void ALevelSpawner::BeginPlay()
{
Super::BeginPlay();
SpawnLevel(true);
SpawnLevel(false);
SpawnLevel(false);
SpawnLevel(false);
}
You will notice that I am passing true as the parameter for SpawnLevel function the first time I am calling it. This is because we need to spawn the initial level part which is the first level part in the game, after that we pass false to SpawnLevel function when we create other level parts.
Make sure that you compile the class from Visual Studio or Unreal Engine editor before we proceed.
Level Spawner Blueprint
Now that we are finished with our LevelSpawner class, we can create a blueprint out of it.
Inside the Content -> Blueprints folder, Right Click -> Blueprint Class. Make sure that you inherit LevelSpawner class:
Name the blueprint BP_LevelSpawner and open it in the editor. In the Components tab click on the BP_LevelSpawner(self) top parent, and in the Details tab locate the empty fields where we need to attach the level parts:
We are able to attach these level parts because when we declared them, we added the EditAnywhere paramter in the UPROPERTY for every level part that we declared.
The EditAnywhere parameter will allow us to edit the variables on blueprint instances of that class:
UPROPERTY(EditAnywhere)
TSubclassOf Level1;
Compile and Save the changes we made to BP_LevelSpawner. Now that we attached all level parts in the appropriate fields, we can drag the BP_LevelSpawner blueprint in the level and test our game:
As soon as we run the game we saw that the level parts are created.
You probably noticed two issues, one is that when we touch the spike nothing happens, this is because we didn’t code that part of the functionality yet, so that is normal.
The second issue is that our levels stopped spawning the further we went into the game. The problem is that we jumped over the Trigger Box component that is a part of every level. The player actor needs to pass through the Trigger Box and then the code to spawn a new level part will be executed.
To fix this, we can go inside BP_Level1 blueprint, select the Trigger Box in the Components tab, and change the Z Location and Z Scale in the Transform property:
This will make the Trigger Box component larger, and it will move its position upwards, so now the player actor will not be able to jump over it and we will not have the issue where we don’t spawn new level parts as we did before.
Where To Go From Here
In this tutorial we created the LevelSpawner class and we coded the level spawning functionality, so now when we play the game, we have an infinite level where we can move.
In the next part titled Detecting Collision Between Player And Obstacles And Wrapping Up Our Game we will detect when the player actor collides with the obstacles and restart the game when that happens, and with that we will finish the game.