Unreal Engine Chapter 3: Creating the Player Model, Minimap and Inventory
In this chapter we will start translating the JavaScript data in the model.js
file (in the state that it is at the end of Chapter 9) to Unreal C++ and introduce our automatic data-binding feature, exclusive to our Unreal Engine plugin.
Introducing the UType Binder
By far the most convenient feature we have (and will start demonstrating starting from this chapter onwards) available is the automatic data-binding. We have a whole section in our documentation that covers the feature in great detail, but for this guide all you need to know is that we’re going to basically expose class member data through Unreal’s reflection system and this is done by simply including the relevant header file that we have available.
Overview of the required Player model data
If you’ve followed the original guide, the JavaScript model
object should be quite familiar. We’re now going to translate all of its data from the Chapter 9 model.js
over to Unreal C++. For this purpose we obviously need to create another class through the Unreal Editor, let’s call it PlayerModel
and for simplicity’s sake (to spare us unnecessary noise), let’s have it’s parent class be UObject
. Since this should by now be a familiar process, we won’t add a screenshot this time.
Looking at the model.js
file, let’s quickly go over what kind of data we require:
- We want to include
CohtmlUTypeBinder.h
,CohtmlFStringBinder.h
andCohtmlTArrayBinder.h
- We want to specify our class constructor
- We want a
time
variable, which is a string that holds the current time, so we will declare it as anFString
- We want
currentHealth
andmaxHealth
variables, which will beint32
s - We want two methods returning
bool
-shouldShowHealthWarning
andshouldShowHealthDanger
- We want a
minimap
object, which will be aUSTRUCT
- It requires:
- An
id
variable, which will be of typeint32
- An
x
,y
andangle
variable, which can will of typefloat
- A
label
, which will be anFString
- An
- It requires:
- We want a
isPaused
bool
variable - We want a
activePauseMenu
FString
variable - We want
inventoryItems
, which is going to be aTArray
containingInventoryItem
objects (anotherUSTRUCT
)- The
InventoryItem
requires:- A
title
,image
anddescription
variable, all of which will beFString
- A
count
variable, which will beint32
- A
- The
- We want
selectedItem
, which will be anint32
variable - We want an
itemSelect
void
method - Additionally, we will also declare a
const
uint32
variable for the “inventory size”, as well as anFItemSelectDelegate
(this one will be explained later) - Lastly, for the C++ implementation, we also want an
itemToDisplay
variable of typeFInventoryItem
, which we will use to create a synchronization dependency so that we won’t have to update the currently selected inventory item every time a different one is selected.
Before we dive into the code, let’s get a few things explained - to successfully utilize the automatic binding of our data, we require the binder header files for certain types like FString
and TArray
, hence the additional include directives.
As mentioned previously in order for all of this to work, we utilize Unreal’s reflection system, which means that our class/struct needs to be have the Unreal respective UCLASS
/USTRUCT
macro used. But what’s more - every data we want exposed to JavaScript needs to be a UPROPERTY
. We can also expose methods using UFUNCTION
. Additional details can be once again be found in our JavaScript interactions section of our documentation.
Preparing for the model creation
Before we start implementing the Player model, let’s first do a quick setup in our StarterGuideHUD
class. We need to add a couple of things:
- We want to include the
CohtmlUTypeBinder.h
- We want to specify our class constructor
- We want to override our
BeginPlay
method - We want to add a
BindUI
method - We want to have a pointer to the
View
that we are going to use
Considering all of these, our StarterGuideHUD.h
needs to look like this in the end:
#pragma once
#include <CohtmlUTypeBinder.h>
#include "GameFramework/HUD.h"
#include "CohtmlGameHUD.h"
#include "StarterGuideHUD.generated.h"
UCLASS()
class COHERENTSAMPLE_API AStarterGuideHUD : public ACohtmlGameHUD
{
GENERATED_BODY()
public:
AStarterGuideHUD(const FObjectInitializer& PCIP);
virtual void BeginPlay() override;
void BindUI();
private:
cohtml::View* View;
};
Ok, now let’s go over to the StarterGuideHUD.cpp
side:
- Because it takes around 2-3 frames for the View to be ready to do data-binding and we don’t want people to implement waiting logic for this themselves, we have a convenient event available (an
Unreal Signature
), to which we can subscribe ourBindUI
method. - In the
BindUI
body, we want to retrieve the View and assign it to our pointer. By includingCohtmlGameHUD.h
, we will have access to a very convenient method calledGetCohtmlHUD
, which does exactly what its name implies. Not only that, but it also holds the aforementioned signature, to which we can subscribe ourBindUI
method.
With all of this explained, this is how all of this looks actually in code:
#include "StarterGuide/StarterGuideHUD.h"
#include "CohtmlGameHUD.h"
AStarterGuideHUD::AStarterGuideHUD(const FObjectInitializer& PCIP)
: Super(PCIP)
{
GetCohtmlHUD()->ReadyForBindings.AddDynamic(this, &AStarterGuideHUD::BindUI);
}
void AStarterGuideHUD::BeginPlay()
{
Super::BeginPlay();
}
void AStarterGuideHUD::BindUI()
{
View = GetCohtmlHUD()->GetView();
if (!View)
{
UE_LOG(LogTemp, Error, TEXT("Failed to retrieve View!"));
return;
}
}
And that’s it! We can now continue further.
Implementing the Player model
On to the actual code of the PlayerModel.h
now:
- This is the code for the includes and a delegate declaration (once again, will be explained later):
#pragma once
#include "CohtmlUTypeBinder.h"
#include <CohtmlFStringBinder.h>
#include <CohtmlTArrayBinder.h>
#include "PlayerModel.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FItemSelectDelegate);
- Next comes the minimap
USTRUCT
:
USTRUCT()
struct FSGMinimap
{
GENERATED_USTRUCT_BODY()
FSGMinimap()
: id(8)
, x(100.0f)
, y(100.0f)
, angle(90.0f)
, label("River Bank")
{
}
UPROPERTY()
int32 id;
UPROPERTY()
float x;
UPROPERTY()
float y;
UPROPERTY()
float angle;
UPROPERTY()
FString label;
};
- Next is the inventory item
USTRUCT
:
USTRUCT()
struct FInventoryItem
{
GENERATED_USTRUCT_BODY()
FInventoryItem()
: count(0)
{
}
FInventoryItem(FString Title, int32 Count, FString Image, FString Description)
: title(Title)
, count(Count)
, image(Image)
, description(Description)
{
}
UPROPERTY()
FString title;
UPROPERTY()
int32 count;
UPROPERTY()
FString image;
UPROPERTY()
FString description;
};
- And lastly the Player model
UCLASS
:
UCLASS()
class UPlayerModel : public UObject
{
GENERATED_BODY()
public:
UPlayerModel();
UPROPERTY()
FString time;
UPROPERTY()
int32 currentHealth;
UPROPERTY()
int32 maxHealth;
UFUNCTION()
bool shouldShowHealthWarning()
{
int currentHealthPercent = (currentHealth * 100) / maxHealth;
return currentHealthPercent > 25 && currentHealthPercent < 50;
}
UFUNCTION()
bool shouldShowHealthDanger()
{
int currentHealthPercent = (currentHealth * 100) / maxHealth;
return currentHealthPercent <= 25;
}
UPROPERTY()
FSGMinimap minimap;
UPROPERTY()
bool isPaused;
UPROPERTY()
FString activePauseMenu;
UPROPERTY()
TArray<FInventoryItem> inventoryItems;
UPROPERTY()
FInventoryItem itemToDisplay;
UPROPERTY()
int32 selectedItem;
UPROPERTY()
FItemSelectDelegate ItemSelectDelegate;
UFUNCTION()
void itemSelect(int index)
{
selectedItem = index;
itemToDisplay = inventoryItems[selectedItem];
ItemSelectDelegate.Broadcast();
}
private:
const uint32 INVENTORY_SIZE = 30;
};
On the PlayerModel.cpp
side, we just need to do our initializations as well as place the iventory items at the same indices, where they are located in the original model.js
:
#include "StarterGuide/PlayerModel.h"
UPlayerModel::UPlayerModel()
: currentHealth(100)
, maxHealth(100)
, isPaused(false)
, activePauseMenu("settings")
, selectedItem(0)
{
inventoryItems.SetNum(INVENTORY_SIZE);
// Adding items in the same slot as originally added in the JS version
inventoryItems[0] = FInventoryItem("Sharp Spear", 1, "spear", TEXT(
"A thrusting or throwing weapon with long shaft and sharp head or blade. Great for medium to long range combat"));
inventoryItems[6] = FInventoryItem("Horned Helmet", 1, "helmet", TEXT(
"Head covering made of a hard material to resist impact with two sharp horns on the side"));
inventoryItems[7] = FInventoryItem("Axe", 1, "axe", TEXT(
"Cutting tool that consists of a heavy edged head fixed to a handle with the edge parallel to the "
"handle and that is used especially for felling trees and chopping and splitting wood or your enemies."));
inventoryItems[8] = FInventoryItem("Longbow", 1, "bow", TEXT(
"Hand-drawn wooden bow held vertically and used especially by medieval English archers"));
inventoryItems[9] = FInventoryItem("Arrow", 5, "arrow", TEXT(
"Shot from a bow and usually having a slender shaft, a pointed head, and feathers at the butt"));
inventoryItems[23] = FInventoryItem("Beer", 2, "beer", TEXT(
"Carbonated, fermented alcoholic beverage that is usually made from "
"malted cereal grain (especially barley) and is flavored with hops"));
itemToDisplay = inventoryItems[selectedItem];
}
Registering the Player model
Now we need to add the PlayerModel
object to the StarterGuideHUD
and use the View
to invoke the creation of the model. We also need to add an UpdateItemSelect
method this time around, which will be hooked to the ItemSelectDelegate
that we added to the PlayerModel
class.
This is needed for when the inventory items get clicked and the currently-selected item has to be changed:
- First the
PlayerModel
’sitemSelect
method gets invoked from the frontend - We successfully update the
PlayerModel
’sselectedItem
variable with the newindex
that is provided - Lastly, we update the
itemToDisplay
One problem remains, however - for this change to be reflected in the frontend, we need to update the JavaScript model and synchronize. This is done by the View
, and only the HUD has access to the it.
And this is basically why we needed the delegate - because now the StarterGuideHUD
will be notified that a new inventory item was selected and then it can update the model accordingly, which will cause the synchronization dependency between the C++ model and the JavaScript observable model to happen and in turn - their properties to be synchronized.
This is one very powerful approach that can be applied in many different situations and allows for endless possibilities!
And now to wrap up with the actual code! In the StarterGuideHUD.h
:
class UPlayerModel;
UCLASS()
class COHERENTSAMPLE_API AStarterGuideHUD : public ACohtmlGameHUD
{
GENERATED_BODY()
public:
AStarterGuideHUD(const FObjectInitializer& PCIP);
virtual void BeginPlay() override;
void BindUI();
UFUNCTION()
void UpdateItemSelect();
UPROPERTY()
UPlayerModel* model;
private:
cohtml::View* View;
};
In the StarterGuideHUD.cpp
:
#include "StarterGuide/StarterGuideHUD.h"
#include "StarterGuide/PlayerModel.h"
#include "CohtmlGameHUD.h"
void AStarterGuideHUD::BeginPlay()
{
Super::BeginPlay();
model = NewObject<UPlayerModel>();
}
void AStarterGuideHUD::BindUI()
{
View = GetCohtmlHUD()->GetView();
if (!View)
{
UE_LOG(LogTemp, Error, TEXT("Failed to retrieve View!"));
return;
}
View->CreateModel("PlayerModel", model);
View->SynchronizeModels();
model->ItemSelectDelegate.AddDynamic(this, &AStarterGuideHUD::UpdateItemSelect);
UE_LOG(LogTemp, Log, TEXT("UI is bound!"));
}
void AStarterGuideHUD::UpdateItemSelect()
{
View->UpdateWholeModel(model);
View->SynchronizeModels();
}
In the next chapter we will go over how we will bind the Map model.