Creating the C++ world objects

With the character constructed, we can now start to build the level. We are going to be creating a block out for the lanes we will be using for the level. We can then use this block out to construct a mesh we can reference in code. Before we get into the level creation, we should ensure the functionality we implemented for the character works as intended. With the BountyDashMap open, navigate to the C++ classes folder of the content browser. Here, you will be able to see the BountyDashCharacter! Drag and drop the character into the game level onto the platform. Then, search for TargetPoint in the Modes panel. Drag and drop three of these target points into the game level, and you should be presented with the following:

Creating the C++ world objects

Now, press the Play button to enter the PIE mode (Play In Editor). The character should be automatically possessed and used for input! Also, ensure that when you press A or D the character moves to the next available target point!

Now that we have the base of the character implemented, we should start to build the level. We require three lanes for the player to run down and obstacles for the player to dodge. For now, we should focus on the lanes the player will be running on. Let's start by blocking out how the lanes will appear in the level. Drag a BSP box brush into the game world. You can find the Box brush in Modes panel under the BSP section under the name Box. Place the box at world location (0.0f, 0.0f, and -100.0f). This will place the box in the center of the world. Now, change the X property of the box under the Brush settings section of the Details panel to 10,000.

We require this lane to be long, so that later on we can hide the end using fog without obscuring the objects the player will need to dodge. Next, we need to click on and drag two more copies of this box. You can do this by holding ALT while moving an object via the transform widget. Position one box copy at world location (0.0f, -230.0f, and -100) and the next at (0.0f, 230, and -100). The last thing we need to do to finish blocking the level is place the Target points in the center of each lane. You should be presented with this when you are done!

Creating the C++ world objects

Converting BSP brushes to a static mesh

The next thing we need to do is convert the lane brushes we made into one mesh so that we can reference it within our code base. Select all of the boxes in the scene. You can do this by holding CTRL while selecting the box brushes in the editor. With all of the brushes selected, address the Details panel. Ensure that the transform of your selection is positioned in the middle of the three brushes. If it is not, you can either reselect the brushes in a different order or group the brushes by pressing CTRL-G while the boxes are selected. This is important as the position of the transform widget shows what the origin of the generated mesh will be. With the grouping or boxes selected address the Brush Settings section in the Details panel, there is a small white expansion arrow at the bottom of the section, click on this now. You will then be presented with a create static mesh button, press this now. Name this mesh Floor_Mesh_BountyDash and save it under Geometry/Meshes/ of the content folder.

Smoke and mirrors with C++ objects

We are going to be creating the illusion of movement within our level. You may have noticed that we have not included any facilities in our character to move forward in the game world. That is because our character will never advance past his X positon at 0. Instead, we are going to be moving the world toward and past him. This way we can create a very easy spawning and processing logic for the obstacles and game world without having to worry about continuously spawning objects the player can move past further and further down the x-axis.

We require some of the level assets to move through the world so we can establish the illusion of movement for the character. One of these moving objects will be the floor. This requires some logic that will reposition floor meshes as they reach a certain depth behind the character. We will be creating a swap chain of sorts that will work with three meshes. The meshes will be positioned in a contiguous line. As the meshes move underneath and behind the player, we need to move any mesh that is far enough behind the player's back, to the front of the swap chain. The effect is a never ending chain of floor meshes constantly flowing underneath the player. The following diagram may help to understand the concept:

Smoke and mirrors with C++ objects

Obstacles and coin pickups will follow a similar logic. However, they will simply be destroyed upon reaching the Kill point in the preceding diagram.

Modifying the BountyDashGameMode

Before we start to create code classes that will feature in our world, we are going to modify BountyDashGameMode that was generated when the project was created. The game mode is going to responsible for all of the game state variables and rules. Later on, we are going to be using the game mode to determine how the player respawns and when the game is lost.

BountyDashGameMode class definition

The game mode is going to be fairly simple. We are going to a add a few member variables that will hold the current state of the game such as game speed, game level, and the number of coins needed to increase the game speed. Navigate to BountyDashGameMode.h and add the following code:

UCLASS(minimalapi)
class ABountyDashGameMode : public AGameMode
{
GENERATED_BODY()

UPROPERTY()
float gameSpeed;

UPROPERTY()
int32 gameLevel;

As you can see, we have two private member variables gameSpeed and gameLevel. These are private as we wish no other object to be able to modify the contents of these values. You will also note that the class has been specified with minimalapi. This specifier effectively informs the engine that other code modules will not need information from this object outside of the class type. This means that you will be able to cast to this class type but functions cannot be called within other modules. This is specified as a way to optimize compile times as no module outside of this project API will require interactions with our game mode.

Next, we declare the public functions and members we will be using within our game mode. Add the following code to the ABountyDashGameMode class definition:

public:
ABountyDashGameMode();

    void CharScoreUp(unsigned int charScore);

    UFUNCTION()
    float GetInvGameSpeed();

    UFUNCTION()
    float GetGameSpeed();

    UFUNCTION()
    int32 GetGameLevel();

The CharScroreUp()function takes in the player's current score (held by the player) and changes game values based on that score. This means we are able to make the game more difficult, as the player scores more points. The next three functions are simply the accessor methods we can use to get the private data of this class in other objects.

Next, we need to declare our protected members we exposed to be EditAnywhere, so we may adjust these from the editor for testing purposes.

protected:

UPROPERTY(EditAnywhere, BlueprintReadOnly)
int32 numCoinsForSpeedIncrease;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
float gameSpeedIncrease;

};

The numCoinsForSpeedIncrease variable will determine how many coins it takes to increase the speed of the game and the gameSpeedIncrease value will determine how much faster the objects move when the numCoinsForSpeedIncrease value has been met.

BountyDashGameMode function definitions

Let's begin add some definitions to the BountyDashGameMode functions. They will be very simple at this point. Let's start by providing some default values for our member variables within the constructor and by assigning the class to be used for our default pawn. Add the definition for the ABountyDashGameMode constructor:

ABountyDashGameMode::ABountyDashGameMode()
{
    // set default pawn class to our ABountyDashCharacter
    DefaultPawnClass = ABountyDashCharacter::StaticClass();

    numCoinsForSpeedIncrease = 5;
    gameSpeed = 10.0f;
    gameSpeedIncrease = 5.0f;
    gameLevel = 1;
}

Here, we are setting the default pawn class by calling StaticClass() on ABountyDashCharacter. As we have just referenced the ABountyDashCharacter type, ensure that #include "BountyDashCharacter.h" is add to the BountyDashGameMode.cpp include list. The StaticClass() function is provided by default for all objects and returns the class type information of the object as a UClass*. We then establish some default values for member variables. The player will have to pick up five coins to increase level, the game speed is set to 10.0f (10m/s) and the game will speed up by 5.0f (5m/s) every time the coin quota is reached. Next, let's add a definition for the CharScoreUp() function:

void
ABountyDashGameMode::CharScoreUp(unsigned int charScore)
{
    if (charScore != 0 &&
       charScore % numCoinsForSpeedIncrease == 0)
    {
        gameSpeed += gameSpeedIncrease;
        gameLevel++;
    }
}

This function is quite self-explanatory. The character's current score is passed into the function. We then check that the character's score is not currently 0 and that if the remainder of our character score is 0 after being divided by the number of coins needed for a speed increase, that is, if it divided equally thus the quota has been reached. We then increase the game speed by the gameSpeedIncrease value and then increment the level.

The last thing we need to add is the accessor methods described earlier. They do not require too much explanation short of the GetInvGameSpeed() function. This function will be used by objects that wish to be pushed down the x-axis at the game speed:

float
ABountyDashGameMode::GetInvGameSpeed()
{
    return -gameSpeed;
}

float
ABountyDashGameMode::GetGameSpeed()
{
    return gameSpeed;
}

int32 ABountyDashGameMode::GetGameLevel()
{
    return gameLevel;
}

Getting our game mode via Template functions


The ABountyDashGame mode now contains information and functionality that will be required by most of the BountyDash objects we create going forward. We need to create a light-weight method to retrieve our custom game mode ensuring that the type of information is preserved. We can do this by creating a template function that will take in a world context and return the correct game mode handle. Traditionally, we could just use a direct cast to ABountyDashGameMode; however, this would require including BountyDashGameMode.h in BountyDash.h. As not all of our objects will require knowledge of the game mode, this is wasteful. Navigate to the BoutyDash.h file now. You will be presented with the following:

#pragma once

#include "Engine.h"

What currently exists in the file is very simple, #pragma once has again been used to ensure the compiler only builds and includes the file once. Then Engine.h has been included, so every other object in BOUNTYDASH_API (they include BountyDash.h by default) has access to the functions within Engine.h. This is a good place to include utility functions you wish all objects to have access to. In this file, include the following lines of code:

template<typename T>
T* GetCustomGameMode(UWorld* worldContext)
{
    return Cast<T>(worldContext->GetAuthGameMode());
}

This code, simply put, is a template function that takes in a game world handle. Get the game mode from this context via the GetAuthGameMode() function, then cast this game mode to the template type provided to the function. We must cast to the template type as the GetAuthGameMode() simply returns a AGameMode handle. Now, with that in place, let's begin coding our never ending floor!

Coding the floor

The construction of the floor will be quite simple in essence, as we only need a few variables and a tick function to achieve the functionality we need. Use the class wizard to create a class named Floor that inherits from AActor. We will start by modifying the class definition found in Floor.h navigate to this file now.

Floor class definition

The class definition for the floor is very basic. All we need is a Tick() function and some accessor methods, so we may provide some information about the floor to other objects. I have also removed the BeginPlay function provided by default by the class wizard as it is not needed. The following is what you will need to write for the AFloor class definition in its entirety. Replace what is present in Floor.h with this now (keeping the #include list intact):

UCLASS()
class BOUNTYDASH_API AFloor : public AActor
{
GENERATED_BODY()
	
public:	
    // Sets default values for this actor's properties
    AFloor();
    
    // Called every frame
    virtual void Tick( float DeltaSeconds ) override;

    float GetKillPoint();
    float GetSpawnPoint();

protected:
    UPROPERTY(EditAnywhere)
    TArray<USceneComponent*> FloorMeshScenes;

    UPROPERTY(EditAnywhere)
    TArray<UStaticMeshComponent*> FloorMeshes;

    UPROPERTY(EditAnywhere)
    UBoxComponent* CollisionBox;

    int32 NumRepeatingMesh;
    float KillPoint;
    float SpawnPoint;
};

We have three UPROPERTY declared members. The first two being TArrays that will hold handles to the USceneComponent and UMeshComponent objects that will make up the floor. We require the TArray of scene components, as the USceneComponent objects provide us with a world transform that we can apply translations to so that we may update the position of the generated floor mesh pieces. The last UPROPERTY is a collision box that will be used for the actual player collisions to prevent the player from falling through the moving floor. The reason we are using a BoxComponent instead of the meshes for collision is that we do not want the player to translate with the moving meshes. Due to surface friction simulation, having the character collide with any of the moving meshes will cause the player to move with the mesh.

The last three members are protected and do not require any UPROPRTY specification. We are simply going to use the two float values, KillPoint and SpawnPoint, to save output calculations from the constructor so we may use them in the Tick() function. The integer value NumRepeatingMesh will be used to determine how many meshes we will have in the chain.

Floor function definitions

As always, we will start with the constructor of the floor. It is here that we will be performing the bulk of our calculations for this object. We will be creating USceneComponents and UMeshComponents that we are going to use to make up our moving floor. With dynamic programming in mind, we should establish the construction algorithm so that we can create any number of meshes in the moving line. Also as we will be getting the speed of the floors movement form the game mode, ensure that #include "BountyDashGameMode.h" is included in Floor.cpp.

AFloor::AFloor() constructor

Start by adding the following lines to the AFloor constructor AFloor::AFloor() found in Floor.cpp:

RootComponent =CreateDefaultSubobject<USceneComponent>(TEXT("Root"));

ConstructorHelpers::FObjectFinder<UStaticMesh>myMesh(TEXT(
"/Game/Barrel_Hopper/Geometry/Floor_Mesh_BountyDash.Floor_Mesh_BountyDash"));

ConstructorHelpers::FObjectFinder<UMaterial>myMaterial(TEXT(
"/Game/StarterContent/Materials/M_Concrete_Tiles.M_Concrete_Tiles"));

To start with, we are simply using FObjectFinders to find the assets we require for the mesh. For the myMesh finder, ensure you parse the reference location of the static floor mesh we created earlier. We also create a scene component to be used as the root component for the floor object. Next, we are going to be checking the success of the mesh acquisition, and then establishing some variables for the mesh placement logic:

if (myMesh.Succeeded())
{
    NumRepeatingMesh = 3;

    FBoxSphereBounds myBounds = myMesh.Object->GetBounds();
    float XBounds = myBounds.BoxExtent.X * 2;
    float ScenePos = ((XBounds * (NumRepeatingMesh - 1)) / 2.0f) * -1;

    KillPoint = ScenePos - (XBounds * 0.5f);
    SpawnPoint = (ScenePos * -1) + (XBounds * 0.5f);

Note that we have just opened an if statement without closing the scope; from time to time, I will split segments of code within a scope across multiple pages. If you are ever lost as to the current scope, we are working from look for this comment; <-- Closing If(MyMesh.Succed()) or in the future, a similarly named comment.

Firstly, we are initializing the NumRepeatingMesh value with three. We are using a variable here instead of a hard coded value so that we may update the number of meshes in the chain without having to refactor the remaining code base.

We then get the bounds of the mesh object using the GetBounds()function on the mesh asset we just retrieved. This returns a FBoxSphereBounds struct, which will provide you with all of the bounding information of a static mesh asset. We then use the X component of the member BoxExtent to initialize Xbounds. BoxExtent is a vector that holds the extent of the bounding box of this mesh. We save the X component of this vector, so we can use it for mesh chain placement logic. We have doubled this value as the BoxExtent vector that represents the extent of the box from origin to one corner of the mesh. Meaning if we wish for the total bounds of the mesh, we must double any of the BoxExtent components.

Next, we calculate the initial scene position of the first USceneCompoennt we will be attaching a mesh to and storing in the ScenePos array. We can determine this position by getting the total length of all of the meshes in the chain (XBounds * (numRepeatingMesh – 1), then halve the resulting value so we can get the distance the first SceneComponent will be from the origin along the x-axis. We also multiply this value by -1 to make it negative as we wish to start our mesh chain behind the character (at X position 0).

We then use ScenePos to specify our killPoint, which represents the point in space at which floor mesh pieces should get to before swapping back to the start of the chain. For the purposes the swap chain, whenever a scene component is half a mesh piece length behind the position of the first scene component in the chain, it should be moved to the other side of the chain. With all of our variables in place, we can now iterate through the number of meshes we desire (3) and create the appropriate components. Add the following code to the scope of the if statement we just opened:

for (int i = 0; i < NumRepeatingMesh; ++i)
{
// Initialize Scene
FString SceneName = "Scene" + FString::FromInt(i);
FName SceneID = FName(*SceneName);
USceneComponent* thisScene = CreateDefaultSubobject<USceneComponent>(SceneID);
check(thisScene);

thisScene->AttachTo(RootComponent);
thisScene->SetRelativeLocation(FVector(ScenePos, 0.0f, 0.0f));
ScenePos += XBounds;

floorMeshScenes.Add(thisScene);

Firstly, we are creating a name for the scene component by appending Scene with the iteration value we are up too. We then convert this appended FString to an FName; then provide this to the CreateDefaultSubobject template function. With the resultant USceneComponent handle, we call AttachTo()to bind it to the root component. Then, we set the RelativeLocation of USceneComponent. Here, we are parsing in the ScenePos value we calculated earlier as the X component of the new relative location. The relative location of this component will always be based off the position of the root SceneComponent we created earlier.

With USceneCompoennt appropriately placed, we then increment the ScenePos value by that of the XBounds value. This will ensure that subsequent USceneComponents created in this loop will be placed in an entire mesh length away from the previous, forming a contiguous chain of meshes attached to scene components. Lastly, we add this new USceneComponent to floorMeshScenes, so we may later perform translations on the components. Next, we will construct the mesh components by adding the following code to the loop:

// Initialize Mesh
FString MeshName = "Mesh" + FString::FromInt(i);
UStaticMeshComponent* thisMesh = CreateDefaultSubobject<UStaticMeshComponent>(FName(*MeshName));
check(thisMesh);

thisMesh->AttachTo(FloorMeshScenes[i]);
thisMesh->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
thisMesh->SetCollisionProfileName(TEXT("OverlapAllDynamic"));

if (myMaterial.Succeeded())
{
	thisMesh->SetStaticMesh(myMesh.Object);
	thisMesh->SetMaterial(0, myMaterial.Object);
}

FloorMeshes.Add(thisMesh);
} // <--Closing For(int i = 0; i < numReapeatingMesh; ++i)

As you can see, we performed a similar name creation process for UMeshComponents as we did for USceneComponents. The construction process following is quite simple. We attach the mesh to the scene component so the mesh will follow any translations we apply to the parent USceneComponent. We then ensure that the mesh's origin will be centered around USceneComponent by setting the Mesh's relative location to be (0.0f, 0.0f, and 0.0f). We then ensure that the meshes do not collide with anything in the game world, we do that with the SetCollisionProfileName()function.

If you remember when we used this function earlier, you provide a profile name you wish the object to use the collision properties from. In our case, we wish this mesh to overlap all dynamic objects; thus, we parse OverlapAllDynamic. Without this line of code, the character may collide with the moving floor meshes, and that will drag the player along at the same speed thus breaking the illusion of motion we are trying to create.

Lastly, we assign the static mesh object and material we obtained earlier with the FObjectFinders. We ensure that we add this new mesh object to the FloorMeshes array in case we need them later. We also close the loop scope we created earlier.

The next thing we are going to do is create the collision box that will be used for character collisions. With the box set to collide with everything and the meshes set to overlap everything, we will be able to collide on the stationary box while the meshes whip past under our feet. The following code will create the box collider:

collisionBox =CreateDefaultSubobject<UBoxComponent>(TEXT("CollsionBox"));check(collisionBox);

collisionBox->AttachTo(RootComponent);
collisionBox->SetBoxExtent(FVector(spawnPoint, myBounds.BoxExtent.Y, myBounds.BoxExtent.Z)); 
collisionBox->SetCollisionProfileName(TEXT("BlockAllDynamic"));

} // <-- Closing if(myMesh.Succeeded())

As you can see, we initialize UBoxComponent as we always initialize components. We then attach the box to the root component as we do not wish to move it. We also set the box extent to be that of the length of the entire swap chain by setting the spawnPoint value as the X bounds of the collider. We set the collision profile to BlockAllDynamic. This means that it will block any dynamic actors such as our Character! Note that we have also closed the scope of the if statement opened earlier. With the constructor definition finished, we might as well define the accessor methods for spawnPoint and killPoint before we move onto the Tick() function:

float AFloor::GetKillPoint()
{
    return KillPoint;
}

float AFloor::GetSpawnPoint()
{
    return SpawnPoint;
}

AFloor::Tick()

Now, it is time to write the function that will move the meshes and ensure they move back to the start of the chain when they reach KillPoint. Add the following code to the Tick() function found in Floor.cpp:

for (auto Scene : FloorMeshScenes)
{
Scene->AddLocalOffset(FVector(GetCustomGameMode
<ABountyDashGameMode>(GetWorld())->GetInvGameSpeed(), 0.0f, 0.0f));

if (Scene->GetComponentTransform().GetLocation().X <= KillPoint)
{
    Scene->SetRelativeLocation(FVector(SpawnPoint, 0.0f, 0.0f));
}
}

Here, we are using a C++ 11 range for loop. Meaning that for each element inside of FloorMeshScenes, it will populate the scene handle of type auto with a pointer to whatever type is contained by FloorMeshScenes, in this case USceneComponent*. For every scene component contained within FloorMeshScenes, we are adding a local offset to each frame. The amount we offset each frame is dependent on the current game speed.

We are getting the game speed from the game mode via the template function we wrote earlier. As you can see, we specified the template function to be of type ABountyDashGameMode, thus we will have access to the bounty dash game mode functionality. We have done this so that the floor will move faster under the player's feet as the speed of the game increases. The next thing we do is check the X value of the Scene components location. If this value is equal to or less than the value stored in KillPoint, we reposition the scene component back to the spawn point. As we attached the meshes to these USceenComponents earlier, the meshes will also translate with the scene components. Lastly, ensure that you have added #include nBountyDashGameMode.h" to the .cppts include list.

Placing the Floor in the level!

We are done making the floor! Compile the code and return to the level editor. We can now place this new floor object in the level! Delete the static mesh that would have replaced our earlier box brushes and drag and drop the Floor object into the scene. The floor object can be found under the C++ classes folder of the content browser. Select the Floor in the level and ensure that its location is set too (0.0f, 0.0f, and -100.f). This will place the floor just below the player's feet around origin. Also ensure the ATargetPoints we placed earlier are in the right positions above the lanes. With this all in place, you should be able to press play and observe the floor moving underneath the player indefinitely. You should see something similar to this:

Placing the Floor in the level!

You will notice that as you move between the lanes by pressing A and D the player maintains the X Position of the target points but nicely travels to the center of each lane.

Creating the obstacles

The next step for this project is to create the obstacles that will come flying at the player. These obstacles are going to be very simple and contain only a few members and functions. These obstacles are to only serve as a blockade for the player and all of the collision with the obstacles will be handled by the player itself. Use the class wizard to create a new class named Obstacle and inherit this object from AActor. Once the class has been generated, modify the class definition found in Obstacle.h, so it appears as follows:

UCLASS(BlueprintType)
class BOUNTYDASH_API AObstacle: public AActor
{
GENERATED_BODY()
    
    float KillPoint;

public:
    // Sets default values for this actor's properties
    AObstacle ();

    // Called when the game starts or when spawned
    virtual void BeginPlay() override;
    
    // Called every frame
    virtual void Tick( float DeltaSeconds ) override;
    
void SetKillPoint(float point);
float GetKillPoint();

protected:
    UFUNCITON()
    virtual void MyOnActorOverlap(AActor* otherActor);

    UFUNCTION()
    virtual void MyOnActorEndOverlap(AActor* otherActor);
    
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
    USphereComponent* Collider;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    UStaticMeshComponent* Mesh;
};

You will note that the class has been declared with the BlueprintType specifier! This object is simple enough to justify extension into blueprint as there is no new learning to be found within this simple object, and we can use blueprint for convenience. For this class, we added a private member KillPoint that will be used to determine when the AObstacle should destroy itself. We also added the accessor methods for this private member. You will notice that we added the MyActorBeginOverlap and MyActorEndOverlap functions that we will be providing to the appropriate delegates, so we can provide custom collision response for this object. We also declared these functions as virtual; this is so we can override these collision functions in child classes of AObstacle.

The definitions of these functions are not too complicated either. Ensure that you have included #include "BountyDashGameMode.h" in the Obstacle.cpp. Then, we can begin filling out our function definitions. The following is code what we will use for the constructor:

AObstacle::AObstacle()
{
PrimaryActorTick.bCanEverTick = true;

Collider = CreateDefaultSubobject<USphereComponent>(TEXT("Collider"));
check(Collider);

RootComponent = Collider;
Collider ->SetCollisionProfileName("OverlapAllDynamic");

Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
check(Mesh);
Mesh ->AttachTo(Collider);
Mesh ->SetCollisionResponseToAllChannels(ECR_Ignore);
KillPoint = -20000.0f;

OnActorBeginOverlap.AddDynamic(this, &AObstacle::MyOnActorOverlap);
OnActorBeginOverlap.AddDynamic(this, &AObstacle::MyOnActorEndOverlap);
}

The only thing of note within this constructor is again we set the mesh of this object to ignore collision response to all channels meaning. The mesh will not affect collision in any way. We have also initialized kill point with a default value of -20000.0f. Following that we are binding the custom MyOnActorOverlap and MyOnActorEndOverlap function to the appropriate delegates.

The Tick() function of this object is responsible for translating the obstacle during play. Add the following code to the Tick function of AObstacle:

void AObstacle::Tick( float DeltaTime )
{
    Super::Tick( DeltaTime );
float gameSpeed = GetCustomGameMode<ABountyDashGameMode>(GetWorld())->
GetInvGameSpeed();

    AddActorLocalOffset(FVector(gameSpeed, 0.0f, 0.0f));

    if (GetActorLocation().X < KillPoint)
    {
        Destroy();
    }
}

As you can see, the tick function will add an offset to the AObstacle each frame along the x-axis via the AddActorLocalOffset function. The value of the offset is determined by the game speed set in the game mode. Again, we are using the template function we created earlier to get the game mode to call GetInvGameSpeed(). The AObstacle is also responsible for its own destruction, upon reaching a maximum bounds defined by killPoint the AObstacle will destroy itself.

The last thing we need to add is the function definitions for the OnOverlap functions and KillPoint accessors:

void AObstacle::MyOnActorOverlap(AActor* otherActor)
{
    
}
void AObstacle::MyOnActorEndOverlap(AActor* otherActor)
{

}

void AObstacle::SetKillPoint(float point)
{
	killPoint = point;
}

float AObstacle::GetKillPoint()
{
    return killPoint;
}

Now, let's abstract this class into blueprint. Compile the code and go back to the game editor. Within the content folder, create a new blueprint object that inherits form the Obstacle class we just made, name it RockObstacleBP. Within this blueprint, we need to make some adjustments. Select the collider component we created and expand the shape sections in Details panel. Change the Sphere radius property to 100.0f. Next, select the mesh component and expand the Static Mesh section; from the provided drop down, choose the SM_Rock mesh. Next, expand the transform section of the Mesh component details panel and match these values:

Creating the obstacles

You should end up with an object that looks similar to this:

Creating the obstacles

Spawning actors from C++!

Despite the Obstacles being fairly easy to implement from a C++ standpoint, the complication comes from the spawning system we will be using to create these objects in game. We will leverage a similar system to the player's movement by basing the spawn locations off of ATargetPoints that are already in the scene. We can then randomly select a spawn target when we require a new object to spawn. Open the class wizard now, and create a class that inherits from Actor and call it ObstacleSpawner. We inherit from AActor as even though this object does not have a physical presence in the scene, we still require the ObstacleSpawner to tick.

The first issue we are going to encounter is our current target points give us a good indication of the Y positon for our spawns but the X position is centered around origin. This is undesirable for the obstacle spawn point as we would like to spawn these objects a fair distance away from the player so we can do two things. One, obscure the popping of spawning the objects via fog and two, present the player with enough obstacle information so they may dodge them at high speeds. This means we are going to require some information from our floor object, we can use the KillPoint and SpawnPoint members of the floor to determine the spawn location and kill location of the Obstacles.

Obstacle Spawner class definition

This will be another fairly simple object. It will require a BeginPlay function, so we may find the floor and all the target points we require for spawning. We also require a Tick function so that we may process spawning logic on a per frame basis. Thankfully, both of these are provided by default by the class wizard. We created a protected SpawnObstacle() function, so we may group that functionality together. We are also going to require a few UPRORERTY declared members that can be edited from the level editor. We need a list of obstacle types to spawn; we can then randomly select one of the types each time we spawn an obstacle. We also require the spawn targets (though we will be populating these upon begin play). Finally, we will need a spawn time that we can set for the interval between obstacles spawning. To accommodate for all of this, navigate to ObstacleSpawner.h now and modify the class definition to match the following:

UCLASS()
class BOUNTYDASH_API AObstacleSpawner : public AActor
{
    GENERATED_BODY()
    
public:	
    // Sets default values for this actor's properties
    AObstacleSpawner();

    // Called when the game starts or when spawned
    virtual void BeginPlay() override;
    
    // Called every frame
    virtual void Tick( float DeltaSeconds ) override;


protected:

    void SpawnObstacle();

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<TSubclassof<class AObstacle*>> ObstaclesToSpawn;

    UPROPERTY()
    TArray<class ATargetPoint*>SpawnTargets;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float SpawnTimer;
    
    UPROPERTY()
    USceneComponent* scene;
private:
    float KillPoint;
    float SpawnPoint;
   float TimeSinceLastSpawn;
};

I have again used TArrays for our containers of obstacle objects and spawn targets. As you can see the obstacle list is of type TSubclassof<class AObstacle>>. This means that the objects in this TArray will be class types that inherit from AObscatle. This is very useful as not only will we be able to use these array elements for spawn information; the engine will also filter our search when adding object types to this array from the editor! With these class types, we will be able to spawn objects that inherit from AObject (including blueprints!) when required. We also included a scene object, so we can arbitrarily place AObstacleSpawner in the level somewhere and two private members that will hold the kill and spawn point of the objects. The last element is a float timer that will be used to gauge how much time has passed since the last obstacle spawn.

Obstacle Spawner function definitions

Okay, now we can create the body of the AObstacleSpawner object. Before we do, ensure the include list in ObstacleSpawner.cpp is as follows:

#include "BountyDash.h"
#include "BountyDashGameMode.h"
#include "Engine/TargetPoint.h"
#include "Floor.h"
#include "Obstacle.h"
#include "ObstacleSpawner.h"

Following this, we have a very simple constructor that establishes the root scene component:

// Sets default values
AObstacleSpawner::AObstacleSpawner()
{
// 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;

Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
check(Scene);
RootComponent = scene;

SpawnTimer = 1.5f;
}

Following the constructor, we have BeginPlay(). Inside this function, we are going to do a few things. First, we are simply performing the same in level object retrieval we executed in ABountyDashCarhacter to get the location of ATargetPoints. However, this object also requires information from the floor object in the level. We are also going to get the Floor object the same way we did with the ATargetPoints by utilizing TActorIterators. We will then get the required kill and spawn point information. We will also set TimeSinceLastSpawn to SpawnTimer, so we begin spawning objects instantaneously:

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

for(TActorIterator<ATargetPoint> TargetIter(GetWorld()); TargetIter; 
    ++TargetIter)
    {
        SpawnTargets.Add(*TargetIter);
    }

for (TActorIterator<AFloor> FloorIter(GetWorld()); FloorIter;  
       ++FloorIter)
    {
        if (FloorIter->GetWorld() == GetWorld())
        {
            KillPoint = FloorIter->GetKillPoint();
            SpawnPoint = FloorIter->GetSpawnPoint();
        }
    }
    TimeSinceLastSpawn = SpawnTimer;
}

The next function we will detail is Tick(), which is responsible for the bulk of the AObstacleSpawner functionality. Within this function, we need to check if we require a new object to be spawned based on the amount of time that has passed since we last spawned an object. Add the following code to AObstacleSpawner::Tick() underneath Super::Tick() now:

TimeSinceLastSpawn += DeltaTime;

float trueSpawnTime = spawnTime / (float)GetCustomGameMode <ABountyDashGameMode>(GetWorld())->GetGameLevel();

if (TimeSinceLastSpawn > trueSpawnTime)
{
    timeSinceLastSpawn = 0.0f;
    SpawnObstacle ();
}

Here, we are accumulating the delta time in TimeSinceLastSpawn, so we may gauge how much real time has passed since the last obstacle was spawned. We then calculate the trueSpawnTime of the AObstacleSpawner. This is based on a base SpawnTime, which is divided by the current game level retrieved from the game mode via the GetCustomGamMode() template function. This means that as the game level increases and the obstacles begin to move faster, the obstacle spawner will also spawn objects at a faster rate. If the accumulated timeSinceLastSpawn is greater than the calculated trueSpawnTime we need to call SpawnObject() and reset the timeSinceLastSpawn timer to 0.0f.

Getting information from components in C++

Now, we need to write the spawn function. This spawn function is going to have to retrieve some information from the components of the object that is being spawned. As we allowed our AObstacle class to be extended into blueprint, we also exposed the object to a level of versatility we must compensate for in the codebase. With the ability to customize the mesh and bounds of the Sphere Collider that makes up any given obstacle, we must be sure we spawn the obstacle in the right place regardless of size!

To do this, we are going to need to obtain information form the components contained within the spawned AObstacle class. This can be done via GetComponentByClass(). It will take the UClass* of the component you wish to retrieve and will return a handle to the component if it has been found. We can then cast this handle to the appropriate type and retrieve the information we require! Let's begin detailing the spawn function by adding the following code to ObstacleSpawner.cpp:

void AObstacleSpawner::SpawnObstacle()
{
if (SpawnTargets.Num() > 0 && ObstaclesToSpawn.Num() > 0)
{
    short Spawner = FMath::Rand() % SpawnTargets.Num();
    short Obstical = FMath::Rand() % ObstaclesToSpawn.Num();
    float CapsuleOffset = 0.0f;

Here, we ensure that both of the arrays have been populated with at least one valid member. We then generate the random look up integers that we will use to access the SpawnTargets and obstacleToSpawn arrays. This means that every time we spawn an object, both the lane spawned in and the type of the object will be randomized. We do this by generating a random value with FMath::Rand() and then we find the remainder of that number divided by the number of elements in the corresponding array. The result will be a random number that exists between 0 and the number of objects in either array minus one, which is perfect for our needs. Continue by adding the following code:

FActorSpawnParameters SpawnInfo;

FTransform myTrans = SpawnTargets[Spawner]->GetTransform();
myTrans.SetLocation(FVector(SpawnPoint, myTrans.GetLocation().Y, myTrans.GetLocation().Z));

Here, we are using a struct called FActorSpawnParameters. The default values of this struct are fine for our purposes. We will soon be parsing this struct to a function in our world context. After that, we create a transform that we will be providing to world context as well. The transform of the spawner will suffice apart from the X Component of the location. We need to adjust this so the X value of the spawn transform matches the spawn point we retrieved from the floor. We do this by setting the X component of the spawn transforms location to be the spawnPoint value we received earlier, and the other components of the location vector to be the Y and Z components of the current location.

The next thing we must do is actually spawn the object! We are going to utilize a template function called SpawnActor() that can be called form the UWorld* handle returned by GetWorld(). This function will spawn an object of a specified type in the game world at a specified location. The type of the object is determined by passing a UClass* handle that holds the object type we wish to spawn. The transform and spawn parameters of the object are also determined by the corresponding input parameters of SpawnActor(). The template type of the function will dictate the type of object that is spawned and the handle that is returned from the function. In our case, we require AObstacle to be spawned. Add the following code to the SpawnObstacle function:

AObstacle* newObs = GetWorld()-> SpawnActor<AObstacle>(obstacleToSpawn[Obstical, myTrans, SpawnInfo);

if(newObs)
{
newObs->SetKillPoint(KillPoint);

As you can see, we are using SpawnActor() with a template type of AObstacle. We use the random look up integer we generated before to retrieve the class type from the obstacleToSpawn array. We also provide the transform and spawn parameters we created earlier to SpawnActor(). If the new AObstacle was created successfully, we then save the return of this function into an AObstacle handle that we will use to set the kill point of the obstacle via SetKillPoint().

We must now adjust the height of this object. The object will more than likely spawn in the ground in its current state. We need to obtain access to the sphere component of the obstacle so that we may get the radius of this capsule and adjust the position of the obstacle, so it sits above the ground. We can use the capsule as a reliable resource as it is the root component of the obstacle, thus we can move the obstacle entirely out of the ground if we assume the base of the sphere will line up with the base of the mesh. Add the following code to the SpawnObstacle() function:

USphereComponent* obsSphere = Cast<USphereComponent>
(newObs->GetComponentByClass(USphereComponent::StaticClass()));

if (obsSphere)
{
newObs->AddActorLocalOffset(FVector(0.0f, 0.0f, obsSphere-> GetUnscaledSphereRadius()));
}
}//<-- Closing if(newObs)
}//<-- Closing if(SpawnTargets.Num() > 0 && obstacleToSpawn.Num() > 0)

Here, we are getting the sphere component out of the newObs handle we obtained from SpawnActor() via GetComponentByClass(), which was earlier mentioned. We pass the class type of a USphereComponent via the static function StaticClass() to the function. This will return a valid handle if the newObs contains USphereComponent (which we know it does). We then cast the result of this function to USphereComponent* and save it in the obsSphere handle. We ensure this handle is valid; if it is, we can then offset the actor we just spawned on the z-axis by the unscaled radius of the sphere component. This will result in all obstacles spawned be in line with the top of the floor!

Ensuring the Obstacle Spawner works

Okay, now it is time to bring the obstacle spawner into the scene. Be sure to compile the code then navigate to the C++ classes folder of the content browser. From here, drag and drop ObstacleSpawner into the scene. Select the new ObstacleSpawner via the World Outlier and address the Details panel. You will see the exposed members under the ObstacleSpawner section like so:

Ensuring the Obstacle Spawner works

Now, add the RockObstacleBP we made earlier to the ObstacleToSpawn array. Press the small white plus next to the property in Details panel; this will add an element to the TArray that you will then be able to customize. Select the dropdown that currently says None. Within this drop down search for the RockObstacleBP and select it. If you wish to create and add more obstacle types to this array feel free. We do not need to add any members to the Spawn Targets property as that will happen automatically. Now, press Play and behold a legion of moving rocks!

Ensuring the Obstacle Spawner works

Minting the coin object

We are nearly ready to start playing BountyDash! First, we need to create a coin system. Coins will function in a very similar way to obstacles, but they require other specific functionality that warrants their own class. What we can do, however, is create the coin as a child class of AObstacle! This means that we do not have to write duplicate functionality! Use the class wizard to create another class called Coin; however, this time be sure to make the parent class AObstacle.

Coin class definition

Once the class generation is complete, navigate to the Coin.h file. You will notice that we are given no default functionality and we must specify it all. Still much like the AObstacle, our class definition is going to be very minimal. Most of the functionality we require for the coin has already been included in the AObstacle base class. All we need to do here is override the Tick() and MyOnActorOverlap() functions, so we can add additional coin functionality.

The following is the class definition for the ACoin object:

UCLASS()
class BOUNTYDASH_API ACoin : public AObstacle
{
    GENERATED_BODY()

    ACoin();

    // Called every frame
    virtual void Tick(float DeltaSeconds) override;

    UFUNCTION()
    virtual void MyOnActorOverlap(AActor* otherActor) override;
};

Simple right? As you can see, we declared a ACoin()constructor and we are overriding the virtual Tick function. You will notice that we have also included the UFUNCITON macro above the MyOnActorOverlap function that we will be passing to the overlap delegate for this actor so that the delegate association succeeds.

Coin function definitons

Let's now define how the coin is going to work navigate to coin.cpp now. Start by adding the following to the include list if they are not already present:

#include "BountyDash.h"
#include "BountyDashCharacter.h"
#include "BountyDashGameMode.h"
#include "Coin.h"

Now, we are going to define our coin's constructor. AObstacle base class already constructs the mesh and collider objects, so we do not have to do that here. It also provides the MyOnActorBeginOverlap function to the OnActorBeginOverlap delegate. Because the MyOnActorBeginOverlap function was specified as virtual and we have overridden this function in ACoin, we do not need to re-bind the functions. We can define our constructor with no functionality as the following:

ACoin::ACoin()
{
    
}

Following that, we can define the Tick() function. This function is going to be used to rotate the mesh component of the object so it spins while moving toward the player. Add the following to Tick()within Coin.cpp:

void ACoin::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
    Mesh->AddLocalRotation(FRotator(5.0f, 0.0f, 0.0f));
}

Here, you can see we are calling Super::Tick(), so the functionality we implemented in AObstacle is executed. We then add a local rotation to the mesh component every frame via the AddLocalRotation function. We yaw the coin roughly at 5 degrees per frame.

Ok, now we can define our overridden MyOnActorOverlap functionality. We will be detailing the collisions between objects in more detail later in the chapter. For now, we will write code that will handle when a coin is spawned on top of an obstacle object. This may happen from time to time as they will be sharing spawning locations but not necessarily have exclusive spawn times. We need to detect if the coin is currently colliding with an obstacle. If so, we need to adjust the height of the coin object, we can do that with the following code:

void ACoin::MyOnActorOverlap(AActor* otherActor)
{
    if (otherActor->GetClass()->IsChildOf(AObstacle::StaticClass()))
    {
USphereComponent* thisSphere = Cast<USphereComponent>(GetComponentByClass(USphereComponent::StaticClass()));

USphereComponent* otherSpehre = Cast<USphereComponent>(otherActor-> GetComponentByClass(USphereComponent::StaticClass()));

        if (otherSpehre)
        {
AddActorLocalOffset(FVector(0.0f, 0.0f, (otherSpehre-> GetUnscaledSphereRadius() * 2.0f) + Collider-> GetUnscaledSphereRadius()));
        }
    }
} 

The first thing we do is use the class type information of the AActor object to see if the offending actor is either a AObstacle or a child of AObstacle. We do this with the IsChildOf()function. This function can be called on any actor to determine if it is a child of a specific class type or of that type itself. For the input parameter, we parse the class type of AObstacle via StaticClass(). If this check succeeds, we need to get information from the sphere component of the other actor now proven to be of type AObstacle. We do this as we did previously using GetComponentByClass() and parsing the class information of the type of component we require. We then check that this function returned a valid handle. If so, we will add a local offset to the coin actor. We will only be adding an offset on the z-axis. We will offset by the otherSphere radius * 2 plus the radius of the coin sphere so that the coin will be placed on top of the obstacle mesh.

Making the coin Blueprint!

Let's create the blueprint that is going to extend from the coin object. Create a new Blueprint class somewhere in the content browser and inherit the blueprint from the Coin class we just made. Call this class CoinBP. We are going to assemble this blueprint in the same way we did RockObstacleBP. For the collider component, set the Sphere Radius property of the Shape section in the details panel to 60.0f. Then, set the Static Mesh property of the Static Mesh section of the mesh component to SM_AssetPlatform. After that, add a member to the Override Materials list in the Rendering section of Details panel. Set this member to be the M_Metal_Gold material. Then, change the transform settings of this component so they match the following:

Making the coin Blueprint!

You will then be presented with a blueprint object that looks like this!

Making the coin Blueprint!

Making it rain coins, creating the coin spawner

We now need to create a system that will spawn the coins. This time, we cannot use the AObstacleSpawner we created previously as a base class. This is because the spawning logic for the coin spawner is much more involved. For the coins in BountyDash, we need to construct the system so the coins will spawn in sets of a random number. Also, within each set we need to ensure that the coins spawn far enough apart. The result will be periodically spawning streams of coins the player can pick up. Open the class wizard again and create a class that inherits from Actor; call it CoinSpawner. This class is going to be very minimal with regards to methods, but it will have quite a few properties.

Coin Spawner class Definition

We will be keeping most of the default functionality provided by the class wizard. The only function we will be removing is the Tick() method, as we are going to utilize the FTimerManager object to drive our spawning functionality. More on this later. Ensure that you remove the Tick function declaration and add the following publically declared member variables:

public:
    UPROPERTY(EditAnywhere)
    TSubclassOf<ACoin> CoinObject;

    UPROPERTY()
    TArray<class ATargetPoint*> SpawnTransforms;

    UPROPERTY()
    USceneComponent* Root;

    UPROPERTY(EditAnywhere)
    int32 MaxSetCoins;

    UPROPERTY(EditAnywhere)
    int32 MinSetCoins;

    UPROPERTY(EditAnywhere)
    float CoinSetTimeInterval;

    UPROPERTY(EditAnywhere)
    float CoinTimeInterval;

    UPROPERTY(EditAnywhere)
    float MovementTimeInterval;

Here, we have a TSubclassOf<class ACoin> handle called CoinObject. This has been declared as EditAnywhere, as we wish to edit this value from the editor much like the obstacle TArray of AObstacleSpawner. It will act as the class object that we use when spawning the coins. We included another TArray of ATargetpoints to be used for our coin spawn locations. Following that is another USceneComponent that will be used to arbitrarily position the spawner in the world. Underneath that, we have all of the properties that we are going to use to dictate the spawning logic for the coins. You will notice that they have all been declared with the UPROPERTY macro and specified, so we may edit the values from the editor. MaxSetCoins and MinSetCoins will be used to determine a random number of coins to spawn within a set between those two maxims. Next, we have all of the float timers that will dictate how long it takes between each major spawner action. We have one for each set of coins, each individual coin within a set, and a movement interval.

Following that, we have our protected members that will be used for internal logic and thus do not need to be exposed:

protected:
    void SpawnCoin();
    void SpawnCoinSet();
    void MoveSpawner();

    int32 NumCoinsToSpawn;

    float KillPoint;
    float SpawnPoint;
    int32 TargetLoc;

    FTimerHandle CoinSetTimerHandle;
    FTimerHandle CoinTimerHandle;
    FTimerHandle SpawnMoveTimerHandle;

We declared several spawning functions. These functions are going to be used to invoke the various spawning methods we require for our coins. We then declare a series of variables. The first NumCoinsToSpawn is an integer value that will hold the number of coins we need to spawn for any given set. We also have the two float values KillPoint and SpawnPoint that will be used for spawning logics. Following this, we have an int value that will be used to determine within which lane we are spawning coins.

Lastly, we have a new object type we have yet to interact with the FTimerHandle object. Unlike our obstacle spawner, we are not going to be using traditional float timers that are incremented within a Tick() function. We are instead going to be leveraging the TimerManager object that is present in our GameWorld (UWorld). Through the time manager, we are able to create timer objects that will invoke a provided function after a given time period. The three timer handles here will be used to initialize the various timers we require for this coin spawner.

Coin Spawner function definitions

With our member variables set up, we can now define how the coin spawner will function. As always, ensure the include list at the top of the coinspawner.cpp matches these:

#include "BountyDash.h"
#include "Engine/TargetPoint.h"
#include "Coin.h"
#include "Floor.h"
#include "CoinSpawner.h"

The constructor for the coin spawner is very simple and does not require much of an explanation. The constructor simply initializes USceneComponent and assigns it as the root and establishes some default values for the coin spawn variables. Ensure that the ACoinSpawner constructor found in the CoinSpawner.cpp matches the following:

// Sets default values
ACoinSpawner::ACoinSpawner()
{
// 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;

    Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    RootComponent = Root;

MaxSetCoins = 5;
    MinSetCoins = 3;
    CoinSetTimeInterval = 4.0f;
    CoinTimeInterval = 0.5f;
    MovementTimeInterval = 1.0f;
}

We then need to define our BeginPlay() function. This function will be responsible for obtaining the information we require from our level as well as initialize the timers we spoke of earlier. Add the following definition for ACoinSpawner::BeginPlay() now:

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

for (TActorIterator<ATargetPoint> TargetIter(GetWorld());
     TargetIter; ++TargetIter)
    {
        SpawnTransforms.Add(*TargetIter);
    }

    for (TActorIterator<AFloor> FloorIter(GetWorld()); FloorIter; 
            ++FloorIter)
    {
        if (FloorIter->GetWorld() == GetWorld())
        {
            KillPoint = FloorIter->GetKillPoint();
            SpawnPoint = FloorIter->GetSpawnPoint();
        }
    }

    // Create Timers
    FTimerManager& worldTimeManager = GetWorld()->
      GetTimerManager();

worldTimeManager.SetTimer(CoinSetTimerHandle, this, &ACoinSpawner::SpawnCoinSet, CoinSetTimeInterval, false);

worldTimeManager.SetTimer(SpawnMoveTimerHandle, this, &ACoinSpawner::MoveSpawner, MovementTimeInterval, true);
}

As you can see, the first section of BeginPlay() is identical to that of the AObstacleSpawner. Following this, however, we are creating two of the timers we are using for the spawning logic. We first get a handle to the timer manager for this game world via GetWorld()->GetTimeManager(). With this FTimeManager handle, we are able to set two of the timers we require. We do this via SetTimer(). This method takes in an FTimerHandle to populate (we provide the handles we declared in the ACoinSpawner class definition), an object to call the provided function on, a handle to the function we wish executed upon timer completion, a timer rate (how long it will take for the timer to execute), and if we wish the timer to loop.

Here, we are setting two timers. The first is a coin set timer; this timer will invoke our SpawnCoinSet function after a provided time (4.0f by default). We specified that this method is to not loop as we will be resetting this timer ourselves. The second timer we are setting is the Move timer; this timer will invoke MoveSpawner() every second by default. This method will periodically shift the lane in which the coins spawn.

Next, we must define the various methods that will be invoked by our timers. Let's begin with the SpawnCoinSet function. Add the following code to CoinSpawner.cpp now:

void ACoinSpawner::SpawnCoinSet()
{
    NumCoinsToSpawn = FMath::RandRange(MinSetCoins, MaxSetCoins);
    
    FTimerManager& worldTimeManager = GetWorld()->
GetTimerManager();

    // Swap active timers
worldTimeManager.ClearTimer(CoinSetTimerHandle);

worldTimeManager.SetTimer(CoinTimerHandle, this, &ACoinSpawner::SpawnCoin, CoinTimeInterval, true);
}

The first thing we do is generate the number of coins we will be spawning in this set by calling FMath::RandRange and providing the two maxims we declared earlier. As the SpawnCoinSet() method was invoked via a timer we now need to clear the coin set timer and active the per coin timer. This is so we can guarantee our spawn coin set timer will only reactivate once all individual coins for a given set have been spawned. We can do this by calling ClearTimer() on the FTimerManager handle, which will remove the timer from execution. We parse the handle of the timer we wish to clear to the manager (in this case CoinSetTimerHandle).

Following this, we set another timer via the SetTimer method we used earlier. This time we are setting the individual spawn coin timer. We have set this timer to loop as we wish the timer continue to spawn coins until all coins have been spawned. Following this, we can define the MoveSpawner function. This function is very simple and simply changes the TargetLoc integer we declared earlier. This integer will be used for a look up into the SpawnTransforms array. Add the following function definition to the CoinSpawner.cpp:

void ACoinSpawner::MoveSpawner()
{
    TargetLoc = FMath::Rand() % SpawnTransforms.Num();
}
We are nearly done with the coin spawner. The last thing we need to do is define the SpawnCoin() function. This function is very similar to AObstacleSpawn::SpawnObstacle(). It appears as follows:
void ACoinSpawner::SpawnCoin()
{
    FActorSpawnParameters spawnParams;

    FTransform coinTransform = SpawnTransforms[TargetLoc]->
GetTransform();

coinTransform.SetLocation(FVector(SpawnPoint, coinTransform.GetLocation().Y, coinTransform.GetLocation().Z));

ACoin* spawnedCoin = GetWorld()->SpawnActor<ACoin>(CoinObject, coinTransform, spawnParams);

    if (spawnedCoin)
    {
USphereComponent* coinSphere = Cast<USphereComponent>(spawnedCoin->
GetComponentByClass(USphereComponent::StaticClass()));

        if (coinSphere)
        {
        float offset = coinSphere->
GetUnscaledSphereRadius();

spawnedCoin->AddActorLocalOffset(FVector(0.0f, 0.0f, offset));
        }

        NumCoinsToSpawn--;
    }

    if (NumCoinsToSpawn <= 0)
    {
        FTimerManager& worldTimeManager = GetWorld()->
GetTimerManager();
    
worldTimeManager.SetTimer(CoinSetTimerHandle, this, &ACoinSpawner::SpawnCoinSet, CoinSetTimeInterval, false);

        worldTimeManager.ClearTimer(CoinTimerHandle);
    }
}

The first section of this function is exactly the same as the spawn method used in the AObstacleSpawner. The most noticeable difference is the inclusion of the timer swap logic and the end of the function. Each time a coin has spawned, we decremented the NumCoinsToSpawn variable. Once this value has reached 0 or lower, we swap our timers again. This swap is very similar to the one we performed in the SpawnCoinSet() method, yet this time we are clearing the individual coin timer and resetting the coin set timer.

Testing what we have so far!

Ok now that we have our floor, our coins, our obstacles, and our character set up, let's add the coin spawner to the level to complete the world object set! Do this by navigating back to the C++ classes folder of Content browser and dragging an ACoinSpawner object into the world. Select the new CoinSpawner in World Outlier and address the details panel. Within the details panel, set the Coin Object property under the Coin Spawner section to be CoinBP. With that in place, press Play and you will see rocks and coins hurtling at the player at 10 m/s!

Testing what we have so far!
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.134.108.197