Home Game Development 3D Game Development Development Tricks with Unreal Engine 4

Development Tricks with Unreal Engine 4

0
10406
39 min read

In this article by Benjamin Carnall, the author of Unreal Engine 4 by Example, we will look at some development tricks with Unreal Engine 4.

(For more resources related to this topic, see here.)

Creating the C++ world objects

Learn Programming & Development with a Packt Subscription

With the character constructed, we can now start to build the level. We are going to create a block out for the lanes that we will be using for the level. We can then use this block to construct a mesh that we can reference in code. Before we get into the level creation, we should ensure the functionality that 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 will be presented with the following:

Now, press the Play button to enter the PIE (Play in Editor) mode. The character will 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 must focus on the lanes that 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 the Modes Panel under the BSP section, which is under the name called 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 10000.

We require this lane to be so long so that later on, we can hide the end using fog without obscuring the objects that the player will need to dodge. Next, we need to click 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, -100) and the next at (0.0f, 230, -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 will be presented with this when you are done:

Converting BSP brushes into a static mesh

The next thing we need to do is convert the lane brushes we made into one mesh, so 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 transformation of your selection is positioned in the middle of these three brushes. If it is not, you can either reselect the brushes in a different order, or you can 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 the Geometry/Meshes/ of the content folder.

Smoke and Mirrors with C++ objects

We are going to create 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. This 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 very easy spawning and processing logic for the obstacles and game world, without having to worry about continuously spawning objects that 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:

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 the BountyDashGameMode that was generated when the project was created. The game mode is going to be responsible for all of the game state variables and rules. Later on, we are going to use the game mode to determine how the player respawns 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 called 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 you will be able to cast 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 that 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 function called CharScroreUp()takes in the player’s current score (held by the player) and changes game values based on this 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 that we can use to get the private data of this class in other objects.

Next, we need to declare our protected members that we have 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 that is 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; we are doing this by calling StaticClass() on the ABountyDashCharacter. As we have just referenced, the ABountyDashCharacter type ensures that #include BountyDashCharacter.h is added to the BountyDashGameMode.cpp include list. The StaticClass() function is provided by default for all objects, and it 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 the 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(unsignedintcharScore)

{

    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 whether the character’s score is not currently 0, and we check 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 is 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 previously. 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 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 the 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 that 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 that 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. It gets the game mode from this context via the GetAuthGameMode() function, and then casts 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 this 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 USceneComponentobjects provide us with a world transform that we can apply translations to, so 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 BoxComponent instead of the meshes for collision is because we do not want the player to translate with the moving meshes. Due to surface friction simulation, having the character to 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 called 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. We will be performing the bulk of our calculations for this object here. We will be creating USceneComponents and UMeshComponents we are going to use to make up our moving floor. With dynamic programming in mind, we should establish the construction algorithm, so 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

The AFloor::AFloor() constructor

Start this by adding the following lines to the AFloor constructor called AFloor::AFloor(), which is 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 that we require for the mesh. For the myMesh finder, ensure that you parse the reference location of the static floor mesh that we created earlier. We also created 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 the segments of the code within a scope across multiple pages. If you are ever lost as to the current scope that we are working from, then look for this comment; <– Closing If(MyMesh.Succed()) or in the future a similarly named comment.

Firstly, we are initializing the NumRepeatingMesh value with 3. 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 function called GetBounds() on the mesh asset that we just retrieved. This returns the FBoxSphereBounds structure, which will provide you with all of the bounding information of a static mesh asset. We then use the X component of the member called 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 only represents the extent of the box from origin to one corner of the mesh. Meaning, if we wish the total bounds of the mesh, we must double any of the BoxExtent components.

Next, we calculate the initial scene positon 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 (XBounds * (numRepeatingMesh – 1) chain and then halve the resulting value, so we can get the distance the first SceneComponent that will be from 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 the X position 0).

We then use ScenePos to specify 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 that 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 that we are up too. We then convert this appended FString to FName and 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 that we calculated earlier as the Xcomponent of the new relative location. The relative location of this component will always be based off of the position of the root SceneComponent that we created earlier.

With the USceneCompoennt appropriately placed, we increment the ScenePos value by that of the XBounds value. This will ensure that subsequent USceneComponents created in this loop will be placed an entire mesh length away from the previous one, 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 and add 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 have performed a similar name creation process for the UMeshComponents as we did for the USceneComponents. The preceding construction process was quite simple. We attach the mesh to the scene component so the mesh will follow any translation that we apply to the parent USceneComponent. We then ensure that the mesh’s origin will be centered around the USceneComponent by setting its relative location to be (0.0f, 0.0f, 0.0f). We then ensure that meshes do not collide with anything in the game world; we do so with the SetCollisionProfileName() function.

If you remember, when we used this function earlier, we provided a profile name that we wish edthe 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 this 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 it later. We also close the loop scope that 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 a 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 it to move. 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 it will block any dynamic actor, 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 theTick() 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 that 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 use a C++ 11 range-based for the 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, it is USceneComponent*. For every scene component contained within FloorMeshScenes, we add 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 that we wrote earlier. As you can see, we have specified the template function to be of the ABountyDashGameMode type, 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 component’s 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 USceenComponents earlier, the meshes will also translate with the scene components. Lastly, ensure that you have added #include BountyDashGameMode.h to .cpp include the 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 that the ATargetPoints that we placed earlier are in the right positions above the lanes. With all this in place, you should be able to press play and observe the floor moving underneath the player indefinitely. You will see something similar to this:

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 used to 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 that 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 notice 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 have added a private member called KillPoint that will be used to determine when AObstacle should destroy itself. We have also added the accessor methods for this private member. You will notice that we have added the MyActorBeginOverlap and MyActorEndOverlap functions that we will provide appropriate delegates, so we can provide custom collision response for this object.

The definitions of these functions are not too complicated either. Ensure that you have included #include BountyDashGameMode.h in 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 that again, we set the mesh of this object to ignore the collision response to all the channels; this means that 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 the MyOnActorOverlap and MyOnActorEndOverlap functions to 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 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 that we created earlier to get the game mode to call GetInvGameSpeed(). AObstacle is also responsible for its own destruction; upon reaching a maximum bounds defined by killPoint, the AObstacle will destroy itself.

The last thing to we need to add is the function definitions for the OnOverlap functions the 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 that we just made, and name it RockObstacleBP. Within this blueprint, we need to make some adjustments. Select the collider component that we created, and expand the shape sections in the 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 menu, choose the SM_Rock mesh. Next, expand the transform section of the Mesh component details panel and match these values:

 

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

 

Spawning Actors from C++

Despite the obstacles being fairly easy to implement from a C++ standpoint, the complication usually comes from the spawning system that we will be using to create these objects in the game. We will leverage a similar system to the player’s movement by basing the spawn locations off of the 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 ObstacleSpawner to tick.

The first issue we are going to encounter is that our current target points give us a good indication of the Y positon for our spawns, but the X position is centered around the 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 that 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 the 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 that 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 have 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 beginning the 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 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, but the engine will also filter our search when we will be 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 have also included a scene object, so we can arbitrarily place AObstacleSpawner in the level somewhere, and we can place two private members that will hold the kill and the 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 so, ensure to include the list in ObstacleSpawner.cpp 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. Firstly, we are simply performing the same in the level object retrieval that 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 the point information. We will also set TimeSinceLastSpawn to SpawnTimer so that 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 understand in 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 off of the amount of time that has passed since we last spawned an object. Add the following code to AObstacleSpawner::Tick() underneath Super::Tick():

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 off of 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 have allowed our AObstacle class to be extended into blueprint, we have also exposed the object to a level of versatility that 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 to 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 it 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 that we require! Let’s begin detailing the spawn function; add 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 lookup 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 this number divided by the number of elements in the corresponding array. The result will be a random number that exists between zero 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 this, we create a transform that we will be providing to the 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 that the X value of the spawn transform matches the spawn point that we retrieved from the floor. We do this by setting the X component of the spawn transforms location to be the spawnPoint value that we received earlier, and w make sure that 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 the 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 get access to the sphere component of the obstacle so we may get the radius of this capsule and adjust the positon 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 that we obtained from SpawnActor() via GetComponentByClass(), which was mentioned previously. We pass the class type of USphereComponent via the static function called StaticClass() to the function. This will return a valid handle if newObs does indeed contain 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 that this handle is valid; if it is, we can then offset the actor that we just spawned on the Z axis by the unscaled radius of the sphere component. This will result in all the obstacles spawned be in line with the top of the floor!

Ensuring the obstacle spawner works

Okay, now is the 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:

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

Summary

This article gives an overview about various development tricks associated with Unreal Engine 4.

Resources for Article:


Further resources on this subject:


NO COMMENTS

LEAVE A REPLY

Please enter your comment!
Please enter your name here