This is a continuation of the previous chapter where we designed a login screen.
In this chapter we will look to not only create a character with a name, but customize its appearance and save it to database so we can load it.
The overview of this post is covered in the YouTube below. Have a look through the small demo at the beginning to see if its for you.
Preparing the data for backend in C++
First of all, how will we save character appearance data? We will simple use a map of strings. This way you can customize it to whatever requirements you need.
For example, you can have something like this:
{
head: "style1",
hair: "style1",
torso: "style3",
legs: "style6"
}
the above is made up, but the point is to keep the data model generic and you can modify both the keys and values to your requirements.
In chapter 8, we created all the relevant C++ code, we will make a small extension to achieve this.
First of all, let’s modify our CharacterBase
class in MyUserAccountWidget.h
and add the AppearanceInfo
to it. All we need to add is:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AccountClient")
TMap<FString, FString> AppearanceInfo;
After adding this, the struct will look like this:
USTRUCT(BlueprintType)
struct FAccountCharacterBase
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AccountClient")
FString Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AccountClient")
int32 Xp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AccountClient")
FString AccountName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AccountClient")
TMap<FString, FString> AppearanceInfo;
};
Next, we modify our character creation function, let’s change the input parameter to reference this new map. It will now look like this:
// Create user characters
UFUNCTION(BlueprintCallable, Category = "AccountClient")
void CreateNewCharacter(FString AccessToken, FString name, TMap<FString, FString> appearanceInfo);
With that created, let’s modify the function call code:
void UMyUserAccountWidget::CreateNewCharacter(FString AccessToken, FString name, TMap<FString, FString> appearanceInfo)
{
CreateNewCharacterApiPath = "http://localhost:8081/player/create-character";
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
Request->OnProcessRequestComplete().BindUObject(this, &UMyUserAccountWidget::OnCreateNewCharacterResponse);
TSharedPtr<FJsonObject> params = MakeShareable(new FJsonObject);
// Set appearance info
TSharedPtr<FJsonObject> appearanceParams = MakeShareable(new FJsonObject);
for (auto& Elem : appearanceInfo)
{
appearanceParams->SetStringField(Elem.Key, Elem.Value);
}
params->SetObjectField(TEXT("appearanceInfo"), appearanceParams);
params->SetStringField(TEXT("name"), name);
FString paramsString;
TSharedRef<TJsonWriter<TCHAR>> JsonWriter = TJsonWriterFactory<>::Create(¶msString);
FJsonSerializer::Serialize(params.ToSharedRef(), JsonWriter);
Request->SetURL(CreateNewCharacterApiPath);
Request->SetVerb("POST");
SetHeaders(Request, AccessToken);
Request->SetContentAsString(paramsString);
Request->ProcessRequest();
}
What did we add above?
TSharedPtr<FJsonObject> appearanceParams = MakeShareable(new FJsonObject);
for (auto& Elem : appearanceInfo)
{
appearanceParams->SetStringField(Elem.Key, Elem.Value);
}
params->SetObjectField(TEXT("appearanceInfo"), appearanceParams);
This will simply add on to the payload which will add a map of keys and values that were inserted from the blueprints.
Finally, we also need to handle the new response object which will include the appearance info, we do this here:
void UMyUserAccountWidget::OnGetCharacterResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
bool success = false;
if (bWasSuccessful)
{
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
if (FJsonSerializer::Deserialize(Reader, JsonObject))
{
if (JsonObject->HasField("accountCharacters"))
{
TArray<TSharedPtr<FJsonValue>> Rows = JsonObject->GetArrayField("accountCharacters");
FAccountCharacterResponse Response;
success = true;
for (int i = 0; i < Rows.Num(); i++)
{
FAccountCharacterBase BaseCharacter;
TSharedPtr<FJsonObject> tempRow = Rows[i]->AsObject();
BaseCharacter.AccountName = tempRow->GetStringField("accountName");
BaseCharacter.Name = tempRow->GetStringField("name");
BaseCharacter.Xp = tempRow->GetNumberField("xp");
TMap<FString, FString> appearanceInfo;
TSharedPtr<FJsonObject> appearanceItems = tempRow->GetObjectField("appearanceInfo");
// Iterate over Json Values
for (auto currJsonValue = appearanceItems->Values.CreateConstIterator(); currJsonValue; ++currJsonValue)
{
appearanceInfo.Add(currJsonValue->Key, appearanceItems->GetStringField(currJsonValue->Key));
}
BaseCharacter.AppearanceInfo = appearanceInfo;
Response.CharacterList.Add(BaseCharacter);
}
NotifyGetCharacterSuccess(Response);
}
}
}
if (success == false)
{
NotifyGetCharacterFail();
}
}
The specific addition which handles it is:
TMap<FString, FString> appearanceInfo;
TSharedPtr<FJsonObject> appearanceItems = tempRow->GetObjectField("appearanceInfo");
// Iterate over Json Values
for (auto currJsonValue = appearanceItems->Values.CreateConstIterator(); currJsonValue; ++currJsonValue)
{
appearanceInfo.Add(currJsonValue->Key, appearanceItems->GetStringField(currJsonValue->Key));
}
This basically iterates over the json object (which is a map of strings) and adds it to BaseCharacter
appearance info.
That’s it for C++ prep, quite small changes! 🙂
Preparing Backend to receive new data
I’ve already modified the Java server code to handle this new payload. The additions are relatively small but still larger than above so I will skip going into detail about what its doing.
However the server code is available here and the relevant commit which handled these changes are here.
Basically we just needed to alter the request and response objects to support the new field, modify the relevant DTOs (Data Transfer Objects), overload the create character functions to take in this new information and add it to the save database schema.
Unreal Engine Blueprint modifications
Ok so now to the interesting bit, we prepared the C++ and Server code to support this new data entries, so now we can start utilizing it in our blueprints.
For this exercise I used the Stylized Modular Character
asset pack, but you can apply it to any that you like – you will just have to configure the data entries differently. The approach itself is universal.
The first thing I did was create a new blueprint class which was a clone of ThirdPersonCharacter
. I then merged the assets together and made to have several functions to toggle between different appearances, such as next hair
and next face
. This work is asset dependent therefore I will not go into depth with how its done, but its basically having an array of available meshes and setting the rendered mesh based on the selected index.
Populating Character Component Widget
In the Character Component Widget
, which was spawned by the Character Selection Widget
I added a new variable for the Character
which was of type Account Character Base
. This way we have a lot more information about the character, including the new appearance info. I modified how we set the character name label like so:
Here just note the new variable
of Character
and note that we just set the text of the button to character name from the Account character base
payload.
Next, we update the OnClick
event for character selected
We simply reference the character that’s referenced back to the Character Selection Widget
. We will shortly go over the creation of the New Character Selected
function
Character Selection Widget
First let’s create a new function to handle the selection of the new character.
This function should set the selected character as a reference (variable), enable the login button for the user, and draw the mesh that’s to be displayed (I will show how to spawn this mesh shortly).
And now we’ll start setting the appearance info. Note this part will be custom depending on how you’re implementing the character – simply use the information that you’ve setup in the payload to reflect the pawn that you have.
In my case, I’ve saved a couple of details in a map of strings (universal) which will have different content depending on your approach.
{
"gender": "male",
"face": "1",
"hair": "2"
}
here’s an example of my expected payload. Note the face/hair payload will just be indexes. I would recommend you using string key words, such as "short beard asset"
which will be better as it will not be relative. But the above can show the principles in action.
Not sure how visible it is, but here’s the full screenshot of how I do it:
In your constructor for the Character Selection Widget
just add a method to spawn your character blueprint mesh
I have it disabled by default as no character is initially selected and a new character is not being created at the start.
And speaking of creating new character, let’s add this character reference to the character creation widget with this:
We spawn the Create Character Widget
and we give it the reference to the character mesh and self
which is the Character Selection Widget
. I will show how to add these variables to constructors shortly.
We then add the new Create Character
to viewport, and collapse this current widget. We don’t want to destroy it, as we will pop it back onto screen soon, that’s why we pass the reference.
Create Character Widget
Ok so now we want to look at Create Character Widget
. First, we want to add 2 variables for us to use: Character Mesh
and Selection Screen
.
In order to effectively use them in our constructor, we want to tick two boxes in the details pane:
- Instance Editable
- Expose on Spawn
Now, we will modify our constructor and reference the character mesh, making it visible:
Note this Set Visibility
function is a custom one because the mesh was comprised of multiple meshes. Simply for reference purposes, here’s the content of that function:
In the designer view for the widget, I added several buttons to toggle some appearance related things:
These toggle functionalities can be found like so:
The functionality is abstracted as it belongs to the character blueprint. This will be custom depending on the asset used, but very simply, there will be an array of possible options, and you’re just incrementing that array index.
The toggle gender button:
To toggle gender is simple, but we also toggle the contents of the button text, which will go from ‘Male’ -> ‘Female’ on click.
Finally, when you click the create button, we just want to gather information about the appearance that’s been set and pass it as payload over our API.
Let’s dive into what this new function holds
This can be done in 1000 different ways. So here, I just create a new function and start populating the data.
First check, is the mesh set to male? if so, add to our map of data: { “gender”: “male” }.
Then I look through our other details, which is simply the face and hair for the character. I then add 2 more keys, {"face": <int>}
and {"hair": <int>}
.
This is stored as a local variable for the API to seamlessly use.
Ok we’re ready to start testing!
Testing the flow
Cool, so when we click the buttons, the face / hair changes. The gender will toggle between M/F meshes likewise.
Going back to selection screen
if you click one of the characters, the mesh will populate:
The details are pulled from the server and applied to the mesh
If we click on another character, their details are populated:
Linking it to the next map
This is kind of ‘bonus’ section. The character selection screen is working, but now we need to send this information to an actual level.
To do this, we will use the global variable that we’ve set up in Game Instance Settings
and add the Appearance Info
variable with public visibility.
Now we go back to our Character Selection Widget
and modify the handle login
button:
We load the game instance settings class, we set the appearance info for the map to use in near future, and we load the level. Here we use the Third Person Example
level.
In the Stylized Third Person
blueprint, I added a function, very similar to one labelled setting appearance information for the pawn
which will load the mesh based on the Appearance Info
. It just went into the constructor, where I load the settings from Game Instance Settings
object and if valid (not null) then I run that code.
I tried to fit the code into one screen, and incase its useful, here it is:
With all of this in place we can try and play!