In the previous chapter, we covered remote procedure calls (RPCs), which allow the server and the clients to execute remote functions on each other. We also covered enumerations and array index wrapping, which allow you to iterate an array in both directions and loop around when you go beyond its limits.
In this chapter, we’re going to look at the most common gameplay framework classes and see where their instances exist in a multiplayer environment. This is important to understand so that you know which instances can be accessed in a specific game instance. An example of this is that only the server should be able to access the game mode instance because you don’t want clients to be able to modify the rules of the game.
We’ll also cover the game state and player state classes, which, as their names imply, store information about the state of the game and each player, respectively. Finally, toward the end of this chapter, we’ll cover some new concepts in the game mode, as well as some useful built-in functionality.
In this chapter, we’re going to cover the following main topics:
By the end of this chapter, you’ll understand where the instances of the most important Gameplay Framework classes exist in multiplayer, as well as how the game state and player state store information that can be accessed by any client. You’ll also know how to make the most out of the Game Mode class and other useful built-in functionality.
This chapter has the following technical requirements:
The project for this chapter can be found in the Chapter18 folder of the code bundle for this book, which can be downloaded here: https://github.com/PacktPublishing/Elevating-Game-Experiences-with-Unreal-Engine-5-Second-Edition.
In the next section, we will learn how to access the gameplay framework instances in multiplayer.
Unreal Engine comes with a set of built-in classes (the Gameplay Framework) that provide the common functionality that most games require, such as a way to define the game rules (game mode), a way to control a character (the player controller and pawn/character class), and so on. When an instance of a gameplay framework class is created in a multiplayer environment, we need to know if it exists on the server, the clients, or the owning client. With that in mind, an instance of the gameplay framework class will always fall into one of the following categories:
Take a look at the following diagram, which shows each category and where the most common classes in the gameplay framework fall into:
Figure 18.1 – The most common gameplay framework classes divided into categories
Let’s look at each class in the preceding diagram in more detail:
To help you understand these concepts, we will use Dota 2 as an example:
In the next exercise, you will display the instance values of the most common gameplay framework classes.
In this exercise, we’re going to create a new C++ project that uses the Third Person template, and we’re going to add the following:
Follow these steps to complete this exercise:
virtual void Tick(float DeltaSeconds) override;
void AGFInstancesCharacter::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
}
const AGameModeBase* GameMode = GetWorld()->GetAuthGameMode();
const AGameStateBase* GameState = GetWorld()->GetGameState();
const APlayerController* PlayerController =
Cast<APlayerController>(GetController());
const AHUD* HUD = PlayerController != nullptr ? PlayerController->GetHUD() : nullptr;
In the preceding code snippet, we stored the instances for the game mode, game state, player controller, and HUD in separate variables so that we can check whether they are valid.
const FString GameModeString = GameMode != nullptr ?
TEXT("Valid") : TEXT("Invalid");
const FString GameStateString = GameState != nullptr ?
TEXT("Valid") : TEXT("Invalid");
const FString PlayerStateString = GetPlayerState() != nullptr ? TEXT("Valid") : TEXT("Invalid");
const FString PawnString = GetName();
const FString PlayerControllerString = PlayerController != nullptr ? TEXT("Valid") : TEXT("Invalid");
const FString HUDString = HUD != nullptr ? TEXT("Valid"):
TEXT("Invalid");
Here, we have created strings to store the name of the pawn and checked whether the other gameplay framework instances are valid.
const FString String = FString::Printf(TEXT("Game Mode = %s Game
State = %s PlayerState = %s Pawn = %s Player Controller =
%s HUD = %s"), *GameModeString, *GameStateString,
*PlayerStateString, *PawnString,
*PlayerControllerString,
*HUDString);
DrawDebugString(GetWorld(), GetActorLocation(), String, nullptr, FColor::White, 0.0f, true);
In the preceding code snippet, we have printed the strings that indicate the name of the pawn and whether the other gameplay framework instances are valid.
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "UMG" });
If you try to compile and get errors from adding the new module, then clean and recompile your project. If that doesn’t work, try restarting your IDE.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GF Instance Player Controller")
TSubclassOf<UUserWidget> MenuClass;
UPROPERTY()
UUserWidget* Menu;
virtual void BeginPlay() override;
#include "Blueprint/UserWidget.h"
void AGFInstancePlayerController::BeginPlay()
{
Super::BeginPlay();
}
if (IsLocalController() && MenuClass != nullptr)
{
Menu = CreateWidget<UUserWidget>(this, MenuClass);
if (Menu != nullptr)
{
Menu->AddToViewport(0);
}
}
Figure 18.2 – The Event Construct that displays the name of the WBP_Menu instance
Finally, you can test the project.
You should get the following output:
Figure 18.3 – Expected result on the Server and Client 1 windows
Now that you’ve completed this exercise, you’ll notice that each character displays its name, as well as if the instances for the game mode, game state, player state, player controller, and HUD are valid. It also displays the instance name of the WBP_Menu UMG widget in the top-left corner of the screen.
Now, let’s analyze the values that are displayed in the Server and Client 1 windows.
Note
The two figures for the Server and Client 1 window will have two text blocks that say Server Character and Client 1 Character. These were added to the original screenshot to help you understand which character is which.
Have a look at the following output of the Server window from the previous exercise:
Figure 18.4 – The Server window
In the preceding screenshot, you have the values for Server Character and Client 1 Character. The WBP_Menu UMG widget is displayed in the top-left corner and is only created for the player controller of Server Character since it’s the only player controller in this window that controls a character.
First, let’s analyze the values for Server Character.
This is the character that the listen server is controlling. The values that are displayed on this character are as follows:
Next, we are going to look at Client 1 Character in the same window.
This is the character that Client 1 is controlling. The values that are displayed on this character are as follows:
Have a look at the following output of the Client 1 window from the previous exercise:
Figure 18.5 – The Client 1 window
In the preceding screenshot, you have the values for Client 1 Character and Server Character. The WBP_Menu UMG widget is displayed in the top-left corner and is only created for the player controller of Client 1 Character since it’s the only player controller in this window that controls a character.
First, let’s analyze the values for Client 1 Character.
This is the character that Client 1 is controlling. The values that are displayed on this character are as follows:
Next, we are going to look at Server Character in the same window.
This is the character that the listen server controls. The values that are displayed on this character are as follows:
By completing this exercise, you should have a better understanding of where each instance of the gameplay framework class exists and where it doesn’t. In the next section, we’re going to cover the player state and game state classes, as well as some additional concepts regarding the game mode and useful built-in functionalities.
Using Game Mode, Player State, and Game State
So far, we’ve covered most of the important classes in the gameplay framework, including the game mode, player controller, and the pawn. In this section, we’re going to cover the player state, game state, and some additional concepts regarding the game mode, as well as some useful built-in functionalities.
We’ve already talked about the game mode and how it works, but there are a few concepts that are useful to know about. Let’s take a look.
To set the default class values, you can use a constructor like so:
ATestGameMode::ATestGameMode() { DefaultPawnClass = AMyCharacter::StaticClass(); PlayerControllerClass = AMyPlayerController::StaticClass(); PlayerStateClass = AMyPlayerState::StaticClass(); GameStateClass = AMyGameState::StaticClass(); }
The preceding code lets you specify which classes to use when spawning pawns, player controllers, player states, and game states when we are using this game mode.
If you want to access the game mode instance, you need to get it from the GetWorld function by using the following code:
AGameModeBase* GameMode = GetWorld()->GetAuthGameMode();
The preceding code allows you to access the current game mode instance, but you have to make sure that you are calling it on the server since this will be invalid on the clients due to security reasons.
So far, we’ve only been using the AGameModeBase class, which is the most basic game mode class in the framework. Although it’s more than enough for certain types of games, there will be cases where you will require a bit more functionality. An example of this would be if we wanted to do a lobby system, where the match only starts if all the players have marked that they are ready. This example wouldn’t be possible to do with just the built-in function of the AGameModeBase class. For these cases, it’s better to use the AGameMode class instead, which is a child class of AGameModeBase that adds support for match states. The way match states work is by using a state machine that can only be in one of the following states at a given time:
To help you understand these concepts better, we can use Dota 2 again as an example:
When the player dies and you want to respawn it, you typically have two options. The first option is to reuse the same pawn instance, manually reset its state back to the defaults, and teleport it to the respawn location. The second option is to destroy the current pawn instance and spawn a new one, which will already have its state reset. If you prefer the latter option, then the AGameModeBase::RestartPlayer function handles the logic of spawning a new pawn instance for a certain player controller for you and places it on a player start.
One important thing to take into consideration is that the function only spawns a new pawn instance if the player controller doesn’t already possess a pawn, so make sure to destroy the controlled pawn before calling RestartPlayer.
Take a look at the following example:
void ATestGameMode::OnDeath(APlayerController* VictimController) { if(VictimController == nullptr) { return; } APawn* Pawn = VictimController->GetPawn(); if(Pawn != nullptr) { Pawn->Destroy(); } RestartPlayer(VictimController); }
In the preceding code, we have the OnDeath function, which takes the player controller of the player that died, destroys its controlled pawn, and calls the RestartPlayer function to spawn a new instance. By default, the new pawn instance will spawn in the player start actor that was used when the player spawned for the first time. Alternatively, you can tell the game mode that you want to spawn on a random player start. To accomplish that, all you need to do is override the AGameModeBase::ShouldSpawnAtStartSpot function and force it to return false, like so:
bool ATestGameMode::ShouldSpawnAtStartSpot(AController* Player) { return false; }
The preceding code will make the game mode use a random player start instead of always using the first one that was used.
Note
For more information about the game mode, please visit https://docs.unrealengine.com/en-US/Gameplay/Framework/GameMode/#gamemodes and https://docs.unrealengine.com/en-US/API/Runtime/Engine/GameFramework/AGameMode/index.html.
The player state class stores the information that other clients need to know about a specific player (such as their current score, kills/deaths/assists, and so on) since they can’t access its player controller. The most widely used built-in functions are GetPlayerName(), GetScore and GetPingInMilliseconds(), which give you the name, score, and ping of the player, respectively.
A good example of how to use the player state is a scoreboard entry on a multiplayer shooter such as Call Of Duty, because every client needs to know the name, kills/deaths/assists, and ping for that player. The player state instance can be accessed in various ways, so let’s take a look at the most common ones:
This variable contains the player state associated with the controller and can only be accessed by the server and the owning client. The following example shows how to use the variable:
APlayerState* PlayerState = Controller->PlayerState;
This function returns the player state associated with the controller and can only be accessed by the server and the owning client. This function also has a template version so that you can cast it to your own custom player state class. The following example shows how to use the default and template versions of this function:
// Default version APlayerState* PlayerState = Controller->GetPlayerState(); // Template version ATestPlayerState* MyPlayerState = Controller->GetPlayerState<ATestPlayerState>();
This function returns the player state associated with the controller that is possessing the pawn and can be accessed by the server and the clients. This function also has a template version so that you can cast it to your own custom player state class. The following example shows how to use the default and template versions of this function:
// Default version APlayerState* PlayerState = Pawn->GetPlayerState(); // Template version ATestPlayerState* MyPlayerState = Pawn- >GetPlayerState<ATestPlayerState>();
This variable in the game state (covered in the next section) stores the player state instances for each player and can be accessed on the server and the clients. The following example shows how to use this variable:
TArray<APlayerState*> PlayerStates = GameState->PlayerArray;
To help you understand these concepts better, we will use Dota 2 again as an example. The player state would have at least the following variables:
Note
For more information about the player state, please visit https://docs.unrealengine.com/en-US/API/Runtime/Engine/GameFramework/APlayerState/index.html.
The game state class stores the information that other clients need to know about the game (such as the match’s elapsed time and the score required to win the game) since they can’t access the game mode. The most widely used variable is PlayerArray, which is an array that provides the player state of every connected client. A good example of how to use the game state is a scoreboard on a multiplayer shooter such as Call Of Duty because every client needs to know how many kills are required to win, as well as the names, kills/deaths/assists, and pings for every connected player.
The game state instance can be accessed in various ways. Let’s take a look.
This function returns the game state associated with the world and can be accessed on the server and the clients. This function also has a template version so that you can cast it to your own custom game state class. The following example shows how to use the default and template versions of this function:
// Default version AGameStateBase* GameState = GetWorld()->GetGameState(); // Template version AMyGameState* MyGameState = GetWorld()->GetGameState<AMyGameState>();
This variable contains the game state associated with the game mode and can only be accessed on the server. The following example shows how to use the variable:
AGameStateBase* GameState = GameMode->GameState;
This function returns the game state associated with the game mode and can only be accessed on the server. This function also has a template version so that you can cast it to your own custom game state class. The following example shows how to use the default and template versions of this function:
// Default version AGameStateBase* GameState = GameMode->GetGameState<AGameStateBase>(); // Template version AMyGameState* MyGameState = GameMode->GetGameState<AMyGameState>();
To help you understand these concepts better, we will use Dota 2 again as an example. The game state will have the following variables:
Note
For more information about the game state, please visit https://docs.unrealengine.com/en-US/API/Runtime/Engine/GameFramework/AGameState/index.html.
UE5 comes with a lot of functionality built in. Let’s look at some examples that are useful to know about when developing a game.
This function is called when the actor has stopped playing, which is the opposite of the BeginPlay function. This function has a parameter called EndPlayReason, which tells you why the actor stopped playing (if it was destroyed, if you stopped PIE, and so on). Take a look at the following example, which prints to the screen that the actor has stopped playing:
void ATestActor::EndPlay(const EEndPlayReason::Type EndPlayReason) { Super::EndPlay(EndPlayReason); const FString String = FString::Printf(TEXT("The actor %s has just stopped playing"), *GetName()); GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, String); }
This function is called when a player lands on a surface after being in the air. Take a look at the following example, which plays a sound when a player lands on a surface:
void ATestCharacter::Landed(const FHitResult& Hit) { Super::Landed(Hit); UGameplayStatics::PlaySound2D(GetWorld(), LandSound); }
This function will make the server load a new map and bring all of the connected clients along with it. This is different from using other methods that load maps, such as the UGameplayStatics::OpenLevel function, because it won’t bring the clients along; it will just load the map on the server and disconnect the clients.
Take a look at the following example, which gets the current map name and uses server travel to reload it and bring along the connected clients:
void ATestGameModeBase::RestartMap() { const FString URL = GetWorld()->GetName(); GetWorld()->ServerTravel(URL, false, false); }
The TArray data structure comes with the Sort function, which allows you to sort the values of an array by using a lambda function that returns whether the A value should be ordered first, followed by the B value. Take a look at the following example, which sorts an integer array from the smallest value to the highest:
void ATestActor::SortValues() { TArray<int32> SortTest; SortTest.Add(43); SortTest.Add(1); SortTest.Add(23); SortTest.Add(8); SortTest.Sort([](const int32& A, const int32& B) { return A < B; }); }
The preceding code will sort the SortTest array’s values of [43, 1, 23, 8] from smallest to highest – that is, [1, 8, 23, 43].
In Unreal Engine, there is a concept called Kill Z, which is a plane on a certain value in Z (set in the World Settings panel). If an actor goes below that Z value, it will call the FellOutOfWorld function, which, by default, destroys the actor. Take a look at the following example, which prints to the screen that the actor fell out of the world:
void AFPSCharacter::FellOutOfWorld(const UDamageType& DmgType) { Super::FellOutOfWorld(DmgType); const FString String = FString::Printf(TEXT("The actor %s has fell out of the world"), *GetName()); GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, String); }
This component rotates the owning actor along time with a certain rate on each axis, defined in the RotationRate variable. To use it, you need to include the following header:
#include "GameFramework/RotatingMovementComponent.h"
You must also declare the component variable:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Test Actor") URotatingMovementComponent* RotatingMovement;
Finally, you must initialize it in the actor constructor, like so:
RotatingMovement = CreateDefaultSubobject <URotatingMovementComponent>("Rotating Movement"); RotatingMovement->RotationRate = FRotator(0.0, 90.0f, 0);
In the preceding code, RotationRate is set to rotate 90 degrees per second on the Yaw axis.
In this exercise, we’re going to create a new C++ project that uses the Third Person template. The following will happen:
Follow these steps to complete the C++ part of this exercise:
Now, let’s create the new C++ classes we’re going to use.
Next, we’re going to work on the PickupsGameState class:
UPROPERTY(Replicated, BlueprintReadOnly)
int32 PickupsRemaining;
virtual void BeginPlay() override;
UFUNCTION(BlueprintCallable)
TArray<APlayerState*> GetPlayerStatesOrderedByScore() const;
void RemovePickup() { PickupsRemaining--; }
bool HasPickups() const { return PickupsRemaining > 0; }
#include "Pickup.h"
#include "Kismet/GameplayStatics.h"
#include "Net/UnrealNetwork.h"
#include "GameFramework/PlayerState.h"
void APickupsGameState::GetLifetimeReplicatedProps(TArray<
FLifetimeProperty >& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(APickupsGameState, PickupsRemaining);
}
void APickupsGameState::BeginPlay()
{
Super::BeginPlay();
TArray<AActor*> Pickups;
UGameplayStatics::GetAllActorsOfClass(this,
APickup::StaticClass(), Pickups);
PickupsRemaining = Pickups.Num();
}
TArray<APlayerState*> APickupsGameState::GetPlayerStatesOrderedByScore() const
{
TArray<APlayerState*> PlayerStates(PlayerArray);
PlayerStates.Sort([](const APlayerState& A, const
APlayerState&
B) { return A.GetScore() > B.GetScore(); });
return PlayerStates;
}
Next, let’s work on the PickupsPlayerState class. Follow these steps:
UPROPERTY(Replicated, BlueprintReadOnly)
int32 Pickups;
void AddPickup() { Pickups++; }
#include "Net/UnrealNetwork.h"
void APickupsPlayerState::GetLifetimeReplicatedProps(TArray<
FLifetimeProperty >& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(APickupsPlayerState, Pickups);
}
Next, let’s work on the PickupsPlayerController class.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pickup Player Controller")
TSubclassOf<class UUserWidget> ScoreboardMenuClass;
UPROPERTY()
class UUserWidget* ScoreboardMenu;
virtual void BeginPlay() override;
#include "Blueprint/UserWidget.h"
void ApickupsPlayerController::BeginPlay()
{
Super::BeginPlay();
if (IsLocalController() && ScoreboardMenuClass !=
nullptr)
{
ScoreboardMenu = CreateWidget<UUserWidget>(this,
ScoreboardMenuClass);
if (ScoreboardMenu != nullptr)
{
ScoreboardMenu->AddToViewport(0);
}
}
}
Now, let’s edit the PickupsGameMode class:
#include "GameFramework/GameMode.h"
class APickupsGameMode : public AGameMode
UPROPERTY()
class APickupsGameState* MyGameState;
virtual void BeginPlay() override;
virtual bool ShouldSpawnAtStartSpot(AController* Player)
override;
virtual void HandleMatchHasStarted() override;
virtual void HandleMatchHasEnded() override;
virtual bool ReadyToStartMatch_Implementation() override;
virtual bool ReadyToEndMatch_Implementation() override;
void RestartMap() const;
#include "Kismet/GameplayStatics.h"
#include "PickupsGameState.h"
void APickupsGameMode::BeginPlay()
{
Super::BeginPlay();
MyGameState = GetGameState<APickupsGameState>();
}
bool APickupsGameMode::ShouldSpawnAtStartSpot
(AController* Player)
{
return false;
}
void APickupsGameMode::HandleMatchHasStarted()
{
Super::HandleMatchHasStarted();
GEngine->AddOnScreenDebugMessage(-1, 2.0f,
FColor::Green, "The game has started!");
}
void APickupsGameMode::HandleMatchHasEnded()
{
Super::HandleMatchHasEnded();
GEngine->AddOnScreenDebugMessage(-1, 2.0f,
FColor::Red, "The game has ended!");
TArray<AActor*> Characters;
UGameplayStatics::GetAllActorsOfClass(this,
APickupsCharacter::StaticClass(), Characters);
for (AActor* Character : Characters)
{
Character->Destroy();
}
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this,
&APickupsGameMode::RestartMap, 5.0f);
}
bool APickupsGameMode::ReadyToStartMatch_Implementation()
{
return true;
}
bool APickupsGameMode::ReadyToEndMatch_Implementation()
{
return MyGameState != nullptr && !MyGameState
->HasPickups();
}
void APickupsGameMode::RestartMap() const
{
GetWorld()->ServerTravel(GetWorld()->GetName(),
false, false);
}
Now, let’s edit the PickupsCharacter class. Follow these steps:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =
"Pickups Character")
USoundBase* FallSound;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =
"Pickups Character")
USoundBase* LandSound;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void Landed(const FHitResult& Hit) override;
virtual void FellOutOfWorld(const UDamageType& DmgType) override;
void AddScore(const float Score) const;
void AddPickup() const;
UFUNCTION(Client, Unreliable)
void ClientPlaySound2D(USoundBase* Sound);
#include "PickupsPlayerState.h"
#include "GameFramework/GameMode.h"
#include "GameFramework/PlayerState.h"
#include "Kismet/GameplayStatics.h"
void APickupsCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
if (EndPlayReason == EEndPlayReason::Destroyed)
{
UGameplayStatics::PlaySound2D(GetWorld(),
FallSound);
}
}
void APickupsCharacter::Landed(const FHitResult& Hit)
{
Super::Landed(Hit);
UGameplayStatics::PlaySound2D(GetWorld(), LandSound);
}
void APickupsCharacter::FellOutOfWorld(const UDamageType&
DmgType)
{
AController* TempController = Controller;
AddScore(-10);
Destroy();
AGameMode* GameMode = GetWorld()
->GetAuthGameMode<AGameMode>();
if (GameMode != nullptr)
{
GameMode->RestartPlayer(TempController);
}
}
void APickupsCharacter::AddScore(const float Score) const
{
APlayerState* MyPlayerState = GetPlayerState();
if (MyPlayerState != nullptr)
{
const float CurrentScore = MyPlayerState->GetScore();
MyPlayerState->SetScore(CurrentScore + Score);
}
}
void APickupsCharacter::AddPickup() const
{
APickupsPlayerState* MyPlayerState =
GetPlayerState<APickupsPlayerState>();
if (MyPlayerState != nullptr)
{
MyPlayerState->AddPickup();
}
}
void APickupsCharacter::ClientPlaySound2D_Implementation(USoundBase* Sound)
{
UGameplayStatics::PlaySound2D(GetWorld(), Sound);
}
Now, let’s work on the Pickup class. Follow these steps:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =
"Pickup")
UStaticMeshComponent* Mesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =
"Pickup")
class URotatingMovementComponent* RotatingMovement;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =
"Pickup")
USoundBase* PickupSound;
APickup();
virtual void BeginPlay() override;
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor*
OtherActor, UPrimitiveComponent* OtherComp, int32
OtherBodyIndex, bool bFromSweep, const FHitResult&
Hit);
#include "PickupsCharacter.h"
#include "PickupsGameState.h"
#include "GameFramework/RotatingMovementComponent.h"
Mesh = CreateDefaultSubobject<UStaticMeshComponent>("Mesh");
Mesh->SetCollisionProfileName("OverlapAll");
RootComponent = Mesh;
RotatingMovement = CreateDefaultSubobject
<URotatingMovementComponent>("Rotating Movement");
RotatingMovement->RotationRate = FRotator(0.0, 90.0f, 0);
bReplicates = true;
PrimaryActorTick.bCanEverTick = false;
Mesh->OnComponentBeginOverlap.AddDynamic(this, &APickup::OnBeginOverlap);
void APickup::OnBeginOverlap(UPrimitiveComponent* OverlappedComp,
AActor* OtherActor, UPrimitiveComponent* OtherComp,
int32
OtherBodyIndex, bool bFromSweep, const FHitResult&
Hit)
{
APickupsCharacter* Character =
Cast<APickupsCharacter>(OtherActor);
if (Character == nullptr || !HasAuthority())
{
return;
}
APickupsGameState* GameState =
Cast<APickupsGameState>(GetWorld()
->GetGameState());
if (GameState != nullptr)
{
GameState->RemovePickup();
}
Character->ClientPlaySound2D(PickupSound);
Character->AddScore(10);
Character->AddPickup();
Destroy();
}
PublicDependencyModuleNames.AddRange(new string[] { "Core",
"CoreUObject", "Engine", "InputCore",
"HeadMountedDisplay",
"UMG" });
If you try to compile and get errors from adding the new module, then clean and recompile your project. If that doesn’t work, try restarting your IDE.
Once it’s loaded, we’re going to import some assets and create some blueprints that derive from the C++ classes we’ve just created.
First, let’s import the sound files:
Next, we will add the Play Sound anim notifies to some of the character’s animations.
Now, let’s set the sounds to use on the character blueprint:
Now, let’s create the blueprint for the pickup.
Note
To display the Engine content, you need to click on the dropdown for the static mesh, click on the cog icon next to the filter box, and make sure that the Show Engine Content flag is set to true.
Now, let’s create the scoreboard UMG widgets. Follow these steps:
Figure 18.6 – Creating the Player State variable
Figure 18.7 – Binding the player name function
Figure 18.8 – Binding the player score function
Figure 18.9 – Binding the pickups count function
Figure 18.10 – Determining whether the entry should be displayed in Bold or Regular
In the preceding code, we used a Select node, which can be created by dragging a wire from the return value and releasing it on an empty space, and then typed Select on the filter. From there, we picked the Select node from the list. Here, we are using the Select node to pick the name of the typeface we’re going to use, so it should return Regular if the player state’s pawn is not the same as the pawn that owns the widget and Bold if it is. We do this to highlight the player’s state entry in bold so that the player knows what their entry is.
Figure 18.11 – The Event Graph that sets the text for the name, score, and pickups count
In the preceding code, we set the font for Name, Score, and Pickups to use the Bold typeface to highlight which scoreboard entry is relative to the player of the current client. For the remainder of the players, use the Regular typeface. If you can’t find the Roboto font, then pick Show Engine Content from the dropdown options.
Figure 18.12 – The WBP_Scoreboard widget hierarchy
Figure 18.13 – Displaying the number of pickups remaining in the world
Figure 18.14 – The Add Scoreboard Header event
Figure 18.15 – The Add Scoreboard Entries event
Figure 18.16 – The Update Scoreboard event
Figure 18.17 – Event Construct
In the preceding code, we get the game state instance, update the scoreboard, and schedule a timer to automatically call the Update Scoreboard event every 0.5 seconds.
Now, let’s create the blueprint for the player controller. Follow these steps:
Next, let’s create the blueprint for the game mode.
Now, let’s modify the main level. Follow these steps:
Figure 18.18 – An example of a map configuration
You should get the following output:
Figure 18.19 – The listen Server and Client 1 picking up cubes in the world
By completing this exercise, you can play on each client. You’ll notice that the characters can collect pickups and gain 10 points just by overlapping with them. If a character falls from the level, they will respawn on a random player start and lose 10 points.
Once all the pickups have been collected, the game will end, and after 5 seconds, it will perform a server travel to reload the same level and bring all the clients with it. You will also see that the UI displays how many pickups are remaining in the level, as well as the scoreboard with information about the name, score, and pickups for each player.
In the next activity, you’re going to add a scoreboard, kill limit, the concept of death/respawning, and the ability for the characters to pick up weapons, ammo, armor, and health in our multiplayer FPS game.
In this activity, you’ll add the concept of death/respawning and the ability for a character to collect pickups to our multiplayer FPS game. We’ll also add a scoreboard and a kill limit to the game so that it has an end goal.
Follow these steps to complete this activity:
Expected output:
Figure 18.20 – The expected output of the activity
The result should be a project where each client’s character can use and switch between three different weapons. If a character kills another, it should register the kill and the death, as well as respawn the character that died at a random player start. You should have a scoreboard that displays the name, kill count, death count, and ping for each player. A character can fall from the level, which should only count as a death, and respawn at a random player start. The character should also be able to pick up the different pickups in the level to get ammo, armor, health, and weapons. The game should end when the kill limit has been reached by showing the scoreboard and server travel to the same level after 5 seconds.
Note
The solution to this activity can be found on GitHub here: https://github.com/PacktPublishing/Elevating-Game-Experiences-with-Unreal-Engine-5-Second-Edition/tree/main/Activity%20solutions.
In this chapter, you learned that the instances of the gameplay framework classes exist in some specific game instances, but not in others. You also learned about the purpose of the game state and player state classes, as well as new concepts for the game mode and some useful built-in functionalities.
At the end of this chapter, you made a basic but functional multiplayer shooter that can be used as a foundation to build upon. You added new weapons, ammo types, fire modes, pickups, and so on to make it more feature-complete and fun.
Having completed this book, you should now have a better understanding of how to use UE5 to make games come to life. We’ve covered a lot of topics in this book, ranging from the simple to more advanced. You started by learning how to create projects using the different templates and how to use Blueprints to create actors and components. Then, you learned how to create a fully functioning Third Person template from scratch by importing the required assets and setting up the Animation Blueprint, Blend Space, game mode, and character, as well as defining and handling the inputs.
Then, you moved on to your first project – a simple stealth game that uses game physics and collisions, projectile movement components, actor components, interfaces, blueprint function libraries, UMG, sounds, and particle effects. Following this, you learned how to create a simple side-scrolling game by using AI, Anim Montages, and Destructible Meshes. Finally, you learned how to create a first-person multiplayer shooter by using the Server-Client architecture, variable replication, and RPCs, as well as how the Player State, Game State, and Game Mode classes work.
By working on various projects that use different parts of Unreal Engine, you now have a strong understanding of how UE5 works. Although this is the end of this book, this is just the beginning of your journey into the world of game development using UE5.
3.137.211.189