10. Unreal Engine + MMO: Character creation screen simplified

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.

UE Character creation walkthrough

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(&paramsString);
	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.

Stylized Modular Character

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:

Add Character variable and set name based on this info

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

On Click for character selected, feed the character information

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.

Create new function for selecting a 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).

Set selected character, enable the login button

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.

Set appearance information

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:

setting appearance information for the pawn

In your constructor for the Character Selection Widget just add a method to spawn your character blueprint mesh

Spawn character in your screen

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:

On button create new character click

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.

Add Character Mesh and Selection Screen to our variables

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:

Make the character mesh 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:

Set visibility inside the stylized character blueprint

In the designer view for the widget, I added several buttons to toggle some appearance related things:

Add buttons to toggle your appearance

These toggle functionalities can be found like so:

Toggle appearance info

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:

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.

Populate appearance info and send it over API

Let’s dive into what this new function holds

Populate information to send over API

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.

Testing another head mesh

Going back to selection screen

if you click one of the characters, the mesh will populate:

Draw the character that we created

The details are pulled from the server and applied to the mesh

If we click on another character, their details are populated:

populate another mesh

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.

Add Appearance info as global variable

Now we go back to our Character Selection Widget and modify the handle login button:

Handle login implementation

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:

Load appearance info and update mesh accordingly

With all of this in place we can try and play!

Log in with newly created character

2 Comments

Comments are closed