Overview
This chapter will resume work on the dodgeball-based game that we started making in the previous chapters. We will continue with the dodgeball game by learning about a helpful set of utilities that you can implement in UE4 in order to improve the quality of your project's structure and reuse logic from it in other projects. We will specifically be talking about Blueprint Function Libraries, Actor Components, and Interfaces. By the end of this chapter, you will be able to use these utilities and other tools in your projects.
In the previous chapter, we learned about the remaining collision-related concepts in UE4, such as collision events, object types, physics simulation, and collision components. We learned how to have objects collide against one another, changing their responses to different collision channels, as well as how to create our own collision presets and spawn actors and use timers.
In this chapter, we will go into several UE4 utilities that will allow you to easily move logic from one project to another and to keep your project well structured and organized, which will make life much easier for you in the long run and also make it easier for other people in your team to understand your work and modify it in the future. Game development is a tremendously hard task and is rarely done individually, but rather in teams, so it's important to take these things into account when building your projects.
We will mainly be talking about Blueprint Function Libraries, Actor Components, and Interfaces.
Blueprint Function Libraries will be used to move some generic functions in our project from a specific actor to a Blueprint Function Library so that it can be used in other parts of our project's logic.
Actor Components will be used to move part of some actor classes' source code into an Actor Component, so that we can easily use that logic in other projects. This will keep our project loosely coupled. Loose coupling is a software engineering concept that refers to having your project structured in such a way that you can easily remove and add things as you need. The reason you should strive for loose coupling is if you want to reuse parts of one of your projects for another project as a game developer, loose coupling will allow you to do that much more easily.
A practical example of how you could apply loose coupling is if you had a player character class that was able to fly and also had an inventory with several usable items. Instead of implementing the code responsible for both those things in that player character class, you would implement the logic for each of those in separate Actor Components, that you then add to the class. This will not only make it easier to add and remove things that this class will do, by simply adding and removing the Actor Components responsible for those things, but also allow you to reuse those Actor Components in other projects where you have a character that has an inventory or is able to fly. This is one of the main purposes of Actor Components.
Interfaces, much like Actor Components, make our project better structured and organized.
Let's get started with these concepts by talking about Blueprint Function Libraries.
In UE4, there's a class called BlueprintFunctionLibary, which is meant to contain a collection of static functions that don't really belong to any specific actor and can be used in multiple parts of your project.
For instance, some of the objects that we used previously, such as the GameplayStatics object and Kismet libraries such as KismetMathLibrary and KismetSystemLibrary, are Blueprint Function Libraries. These contain functions that can be used in any part of your project.
There is at least one function in our project created by us that can be moved to a Blueprint Function library: the CanSeeActor function defined in the EnemyCharacter class.
Let's then, in the first exercise of this chapter, create our own Blueprint Function library, so that we can then move the CanSeeActor function from the EnemyCharacter class to the Blueprint Function library class.
In this exercise, we will be moving the CanSeeActor function that we created for the EnemyCharacter class to a Blueprint Function Library.
The following steps will help you complete this exercise:
The new CanSeeActor function will be static, will return a bool, and will receive the following parameters:
public:
// Can we see the given actor
static bool CanSeeActor(
const UWorld* World,
FVector Location,
const AActor* TargetActor,
TArray<const AActor*> IgnoreActors = TArray<const AActor*>());
// Where the Line Trace starts and ends
FVector Start = Location;
FCollisionQueryParams QueryParams;
// Ignore the actors specified
QueryParams.AddIgnoredActors(IgnoreActors);
// Execute the Line Trace
World->LineTraceSingleByChannel(Hit, Start, End, Channel, QueryParams);
// Show the Line Trace inside the game
DrawDebugLine(World, Start, End, FColor::Red);
#include "Engine/World.h"
#include "DrawDebugHelpers.h"
#include "CollisionQueryParams.h"
// Remove this line
#include "DrawDebugHelpers.h"
#include "DodgeballFunctionLibrary.h"
const TArray<const AActor*> IgnoreActors = {this, TargetActor};
Note
Introducing this last code snippet might give you an IntelliSense error in Visual Studio. You can safely ignore it, as your code should compile with no issues regardless.
if (UDodgeballFunctionLibrary::CanSeeActor(
GetWorld(),
SightSource->GetComponentLocation(),
TargetActor,
IgnoreActors))
Now that you've made all those changes, compile your code, open your project, and verify that the EnemyCharacter class still looks at the player as it walks around, as long as it's in the enemy character's sight, as shown in the following screenshot:
And that concludes our exercise. We've put our CanSeeActor function inside of a Blueprint Function Library and can now reuse it for other actors that require the same type of functionality.
The next step in our project is going to be learning more about Actor Components and how we can use them to our advantage. Let's take a look at those.
As we've seen in the first chapters of this book, Actors are the main way to create logic in UE4. However, we've also seen that Actors can contain several Actor Components.
Actor Components are objects that can be added to an Actor and can have multiple types of functionality, such as being responsible for a character's inventory or making a character fly. Actor Components must always belong to and live inside an Actor, which is referred to as their Owner.
There are several different types of existing Actor Components. Some of these are listed here:
This leaves us with two main ways to add logic to our Actors: directly in the Actor class or through Actor Components. In order to follow good software development practices, namely loose coupling (mentioned previously), you should strive to use Actor Components instead of placing logic directly inside an Actor whenever possible. Let's take a look at a practical example in order to understand the usefulness of Actor Components.
Let's say you're making a game where you have the player character and enemy characters, both of which have health, and where the player character must fight enemies, who can also fight back. If you had to implement the health logic, which includes gaining health, losing health, and tracking the character's health, you'd have two options:
There are a few reasons why the first option is not a good option, but the main one is this: If you wanted to add another piece of logic to both character classes (for example, stamina, which would limit the strength and frequency of the characters' attacks), doing so using the same approach of a base class wouldn't be a viable option. Given that in UE4, C++ classes can only inherit from one class and there's no such thing as multiple inheritance, that would be very hard to manage. It would also only get more complicated and unmanageable the more logic you decided to add to your project.
With that said, when adding logic to your project can be encapsulated in a separate component, allowing you to achieve Loose Coupling, you should always do so.
Let's now create a new Actor Component, which will be responsible for keeping track of an actor's health, as well as gaining and losing that health. We will do this in the next exercise.
In this exercise, we will be creating a new actor component responsible for gaining, losing, and keeping track of an actor's health (its Owner).
In order for the player to lose, we'll have to make the player character lose health and then end the game when it runs out of health. We'll want to put this logic inside an actor component so that we can easily add all this health-related logic to other actors if we need to.
The following steps will help you complete the exercise:
// The Owner's initial and current amount health points
UPROPERTY(EditDefaultsOnly, Category = Health)
float Health = 100.f;
// Take health points from its Owner
void LoseHealth(float Amount);
Now, in the class's source file, let's start by notifying it that it should never use the Tick event so that its performance can be slightly improved.
PrimaryComponentTick.bCanEverTick = false;
void UHealthComponent::LoseHealth(float Amount)
{
Health -= Amount;
}
if (Health <= 0.f)
{
}
Health = 0.f;
UKismetSystemLibrary::QuitGame(this,
nullptr,
EQuitPreference::Quit,
true);
#include "Kismet/KismetSystemLibrary.h"
With this logic done, whenever any actor that has HealthComponent runs out of health, the game will end. This isn't exactly the behavior we want in our Dodgeball game. However, we'll change it when we talk about Interfaces later in this chapter.
In the next exercise, we'll be making the necessary modifications to some classes in our project in order to accommodate our newly created HealthComponent.
In this exercise, we will be modifying our DodgeballProjectile class in order for it to damage the player's character when it comes into contact with it, and the DodgeballCharacter class, in order for it to have a Health Component.
Open the DodgeballProjectile class's files in Visual Studio and make the following modifications:
// The damage the dodgeball will deal to the player's character
UPROPERTY(EditAnywhere, Category = Damage)
float Damage = 34.f;
In the class's source file, we'll have to make some modifications to the OnHit function.
#include "HealthComponent.h"
ADodgeballCharacter* Player = Cast<ADodgeballCharacter>(OtherActor);
if (Player != nullptr)
{
}
UHealthComponent* HealthComponent = Player->
FindComponentByClass<UHealthComponent>();
Note
The FindComponent(s) ByClass function, included in the Actor class, will return a reference(s) to the actor component(s) of a specific class that the actor contains. If the function returns nullptr, that means the actor doesn't have an Actor Component of that class.
You may also find the GetComponents function inside the Actor class useful, which will return a list of all the Actor Components inside that actor.
if (HealthComponent != nullptr)
{
HealthComponent->LoseHealth(Damage);
}
Destroy();
Before we finish this exercise, we'll need to make some modifications to our DodgeballCharacter class.
Open the class's files in Visual Studio and follow these steps.
class UHealthComponent* HealthComponent;
#include "HealthComponent.h"
HealthComponent = CreateDefaultSubobject<UHealthComponent>(TEXT("Health Component"));
After you've made all these changes, compile your code, and open the editor. When you play the game, if you let your player character get hit by a dodgeball 3 times, you'll notice that the game abruptly stops, as intended:
Once the game is stopped, it will look like the following screenshot:
And that completes this exercise. You now know how to create your own Actor Components and how to access an actor's Actor Components. This is a very important step toward making your game projects more understandable and better structured, so good job.
Now that we've learned about Actor Components, we will be learning about another way to make your projects better structured and organized – Interfaces – in the next section.
There's a chance that you might already know about Interfaces, given that other programming languages, such as Java, for instance, already have them. If you do, they work pretty similarly in UE4, but if you don't, let's see how they work, taking the example of the Health Component we created.
As you've seen in the previous exercise, when the Health property of the Health Component reaches 0, that component will simply end the game. However, we don't want that to happen every time an actor's health points run out: some actors might simply be destroyed, some might notify another actor that they have run out of health points, and so on. We want each actor to be able to determine what happens to them when they run out of health points. But how can we handle this?
Ideally, we would simply call a specific function that belongs to the Owner of the Health Component, which would then choose how to handle the fact that the Owner has run out of health points, but in which class should you implement that function, given that our Owner can be of any class, as long as it inherits from the Actor class? As we discussed at the beginning of the chapter, having a class responsible just for this would quickly become unmanageable. Luckily for us, Interfaces solve this problem.
Interfaces are classes that contain a collection of functions that an object must have if it implements that Interface. It essentially works as a contract that the object signs, saying that it will implement all the functions present on that Interface. You can then simply check whether an object implements a specific Interface and call the object's implementation of the function defined in the Interface.
In our specific case, we'll want to have an Interface that has a function that will be called when an object runs out of health points so that our Health Component can simply check whether its Owner implements that Interface and then call that function from the Interface. This will make it easy for us to specify how each actor behaves when running out of health points: some actors might simply be destroyed, others might trigger an in-game event, and others might simply end the game (which is the case with our player character).
However, before we create our first Interface, we should first talk a bit about Blueprint Native Events.
When using the UFUNCTION macro in C++, you can turn a function into a Blueprint Native Event by simply adding the BlueprintNativeEvent tag to that macro.
So what is a Blueprint Native Event? It's an event that is declared in C++ that can have a default behavior, which is also defined in C++, but that can be overridden in Blueprint. You declare a Blueprint Native Event called MyEvent by declaring a MyEvent function using the UFUNCTION macro with the BlueprintNativeEvent tag, followed by the virtual MyEvent_Implementation function:
UFUNCTION(BlueprintNativeEvent)
void MyEvent();
virtual void MyEvent_Implementation();
The reason why you have to declare these two functions is that the first one is the Blueprint signature, which allows you to override the event in Blueprint, while the second one is the C++ signature, which allows you to override the event in C++.
The C++ signature is simply the name of the event followed by _Implementation, and it should always be a virtual function. Given that you declared this event in C++, in order to implement its default behavior, you have to implement the MyEvent_Implementation function, and not the MyEvent function (that one should remain untouched). In order to call a Blueprint Native Event, you can simply call the normal function, without the _Implementation suffix; in this case, MyEvent().
We will take a look at how to use Blueprint Native Events in practice in the next exercise, where we'll create a new Interface.
In this exercise, we will be creating the Interface responsible for handling how an object behaves when it runs out of health points.
In order to do this, follow these steps:
UFUNCTION(BlueprintNativeEvent, Category = Health)
void OnDeath();
virtual void OnDeath_Implementation() = 0;
Note that the OnDeath_Implementation function declaration needs its own implementation. However, there is no need for the Interface to implement that function because it would simply be empty. In order to notify the compiler that this function has no implementation in this class, we add = 0 to the end of its declaration.
// Add this include
#include "HealthInterface.h"
#include "DodgeballCharacter.generated.h"
class ADodgeballCharacter : public ACharacter, public IHealthInterface
virtual void OnDeath_Implementation() override;
void ADodgeballCharacter::OnDeath_Implementation()
{
UKismetSystemLibrary::QuitGame(this,
nullptr,
EQuitPreference::Quit,
true);
}
#include "Kismet/KismetSystemLibrary.h"
// Replace this line
#include "Kismet/KismetSystemLibrary.h"
// With this line
#include "HealthInterface.h"
// Remove this
UKismetSystemLibrary::QuitGame(this,
nullptr,
EQuitPreference::Quit,
true);
if (GetOwner()->Implements<UHealthInterface>())
{
}
#include "GameFramework/Actor.h"
If this if statement is true, that means that our Owner implements HealthInterface. In this case, we'll want to call its implementation of the OnDeath function.
if (GetOwner()->Implements<UHealthInterface>())
{
IHealthInterface::Execute_OnDeath(GetOwner());
}
Note
If your interface's function receives parameters, you can send them in the function call after the first parameter mentioned in the last step. For instance, if our OnDeath function received an int property as a parameter, you would call it like this: IHealthInterface::Execute_OnDeath(GetOwner(), 5).
The first time you try to compile your code after adding a new function to an Interface and then calling Execute_ version, you may get an Intellisense error. You can safely ignore this error.
After you've made all these changes, compile your code, and open the editor. When you play the game, try letting the character get hit by 3 dodgeballs:
If the game ends after that, then that means that all our changes worked and the game's logic remains the same:
And with that, we conclude this exercise. You now know how to use Interfaces. The benefit to the change that we just made is that we can now have other actors that lose health and can specify what happens when they run out of health points, using this Health Interface.
The next and last step of this chapter will be an activity where we'll move all of the logic related to the LookAtActor function to its own Actor Component and use it to replace the SightSource component we created.
In this activity, we'll be moving all of the logic related to the LookAtActor function, inside the EnemyCharacter class, to its own Actor Component (similarly to how we moved the CanSeeActor function to a Blueprint Function Library). This way, if we want an actor (that isn't an EnemyCharacter) to look at another actor, we will simply be able to add this component to it.
The following steps will help you complete this activity:
Head to the class's files, which are opened in Visual Studio.
Note
While the LookAtActor function of EnemyCharacter received the AActor* TargetActor parameter, this Actor Component will have its TargetActor as a class property, which is why we won't need to receive it as a parameter.
Now, go to the class's source file and take the following steps:
We'll also need to add an include to the Actor class since we'll be accessing several functions belonging to that class.
Now make some further modifications to our EnemyCharacter class:
Now, there's only one last step we have to do before this activity is completed.
Expected output:
And with that, we conclude our activity. You have now applied your knowledge of refactoring part of an actor's logic into an Actor Component so that you can reuse it in other parts of your project, or even in other projects of your own.
Note
The solution to this activity can be found at: https://packt.live/338jEBx.
You now know about several utilities that will help you to keep your projects more organized and allow better reuse of the things that you make.
You have learned how to: create a Blueprint Function Library; create your own Actor Components and use them to refactor the existing logic in your project; and create Interfaces and call functions from an object that implements a specific Interface. Altogether, these new topics will allow you to refactor and reuse all the code that you write in a project in that same project, or in another project.
In the next chapter, we'll be taking a look at UMG, UE4's system for creating user Interfaces, and learning how to create our own user Interfaces.
3.143.228.40