Unreal MMO Development with Yaz

32. How to integrate OWS and UE to make character creation

In this post I will be looking to create a new character with OWS – I will be using the template for UE5.1.

This is extension to previous post which covered how to create login screen.

I had to make quite a few changes in order to get this working, so hopefully I don’t miss anything, raise it in comments if something is missing.

OWS Create and Select character for login

Let’s go through the points we will need to cover:

Note that I will make some modifications to OWS code in order to support the functionality I need – I don’t suggest making it the same. The calls are ok for proof of concept, but there’s multiple issues for production use.

Furthermore, the character select/create screen I originally created in this post.

Character Select / Create screen

It was split into multiple parts and these two are pre-requisites for this one:

The following part was connecting it to custom server – we will now connect it to OWS instead – you can check and see that the steps are generally similar but obviously the endpoints are different.

Creating character

In order to create a character, we can cross reference with existing widget from OWS.

The blueprints were as follows:

If you double click the CreateCharacter function, you will be navigated to the C++ code responsible for it.

We’re going to want to make an adjustment to this call, we want the Create Character call to accept a map of custom data.

This custom data will refer to the base cosmetic data (and perhaps anything else you want to add).

It’s a key/value pair, therefore a map of data.

As such, you will want to change the fingerprint of this function in OWSPlayerController.h.

// OWSPlayerController.h
// Create Character
UFUNCTION(BlueprintCallable, Category = "Login")
    void CreateCharacter(FString UserSessionGUID, FString CharacterName, FString ClassName, TMap<FString, FString> CharacterAppearance);

Now let’s look at the c++ implementation


//OWSPlayerController.cpp
void AOWSPlayerController::CreateCharacter(FString UserSessionGUID, FString CharacterName, FString ClassName, TMap<FString, FString> CharacterAppearance)
{
	OWSPlayerControllerComponent->CreateCharacter(UserSessionGUID, CharacterName, ClassName, CharacterAppearance);
}

We simply pass the CharacterAppearance on to OWSPlayerControllerComponent to deal with this request. Note this may be deprecated in future by OWS.

Then let’s reference it in OWSPlayerControllerComponent too

// OWSPlayerControllerComponent.h
// Create Character
UFUNCTION(BlueprintCallable, Category = "Character")
void CreateCharacter(FString UserSessionGUID, FString CharacterName, FString ClassName, TMap<FString, FString> CharacterAppearance);

Part of this header class, we also want to reference two more things at the bottom of the file:

// OWSPlayerControllerComponent.h
private:
	TMap<FString, FString> NewCharacterAppearance;
	void ConfigureAppearanceForNewCharacter(FString CharacterName);

Let’s start putting it together in c++.

// OWSPlayerControllerComponent.cpp
// CreateCharacter
void UOWSPlayerControllerComponent::CreateCharacter(FString UserSessionGUID, FString CharacterName, FString ClassName, TMap<FString, FString> CharacterAppearance)
{
	// Prepare the appearance data for if/when the create character response is successful.
	NewCharacterAppearance = CharacterAppearance;

	FCreateCharacterJSONPost CreateCharacterJSONPost;
	CreateCharacterJSONPost.UserSessionGUID = UserSessionGUID;
	CreateCharacterJSONPost.CharacterName = CharacterName;
	CreateCharacterJSONPost.ClassName = ClassName;
	FString PostParameters = "";
	if (FJsonObjectConverter::UStructToJsonObjectString(CreateCharacterJSONPost, PostParameters))
	{
		ProcessOWS2POSTRequest("PublicAPI", "api/Users/CreateCharacter", PostParameters, &UOWSPlayerControllerComponent::OnCreateCharacterResponseReceived);
	}
	else
	{
		UE_LOG(OWS, Error, TEXT("CreateCharacter Error serializing CreateCharacterJSONPost!"));
	}
}

Here we can see that we populate the NewCharacterAppearance = CharacterAppearance;

Note that the Create Character API call does not currently accept custom variables part of the initial call – this should be optimized as such, so we will put a workaround which is not ideal but will be used for proof of concept.

This means that the flow is:

So on the error path – we don’t need to do anything, because we haven’t started processing any of the custom data.

On success path we will create a new function (already referenced in header file) and call it when successful, it’s marked bold in code below.

// OWSPlayerControllerComponent.cpp
void UOWSPlayerControllerComponent::OnCreateCharacterResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	if (bWasSuccessful)
	{
		FString ResponseString = Response->GetContentAsString();

		FCreateCharacter CreateCharacter;

		if (!FJsonObjectConverter::JsonObjectStringToUStruct(ResponseString, &CreateCharacter, 0, 0))
		{
			OnErrorCreateCharacterDelegate.ExecuteIfBound(TEXT("Could not deserialize CreateCharacter JSON to CreateCharacter struct!"));
			return;
		}

		if (!CreateCharacter.ErrorMessage.IsEmpty())
		{
			OnErrorCreateCharacterDelegate.ExecuteIfBound(*CreateCharacter.ErrorMessage);
			return;
		}

		UE_LOG(OWS, Verbose, TEXT("OnCreateCharacterResponseReceived Success!"));
		UE_LOG(OWS, Verbose, TEXT("Calling ConfigureAppearanceForNewCharacter to complete character creation!"));
		// We need to hook to OnAddOrUpdateCustomCharacterDataResponseReceived - if we get error, it means the character was not created successfully. but not much we can do at this point.
		ConfigureAppearanceForNewCharacter(CreateCharacter.CharacterName);

		OnNotifyCreateCharacterDelegate.ExecuteIfBound(CreateCharacter);
	}
	else
	{
		UE_LOG(OWS, Error, TEXT("OnCreateCharacterResponseReceived Error accessing login server!"));
		OnErrorCreateCharacterDelegate.ExecuteIfBound(TEXT("Unknown error connecting to server!"));
	}
}

and its definition

// OWSPlayerControllerComponent.cpp
void UOWSPlayerControllerComponent::ConfigureAppearanceForNewCharacter(FString CharacterName)
{
	for (const TPair<FString, FString>& pair : NewCharacterAppearance)
	{
		// No batch method, update each custom field one by one.
		// Listen on error event
		AddOrUpdateCustomCharacterData(CharacterName, pair.Key, pair.Value);
	}
	UE_LOG(OWS, Verbose, TEXT("ConfigureAppearanceForNewCharacter completed!"));
}

We put NewCharacterAppearance into private variable for the class while the async API call processes.

If successful, we iterate over each property in the map (key value pair) and we reuse existing functionality of AddOrUpdateCustomCharacterData to insert the custom values.

Ideally this would support a batch insert, so this could be a good optimization to consider.

That’s all the insertions we need to make for CreateCharacter flow, one thing to reference is the delegate in the OWSPlayerController.

OWSPlayerControllerComponent->OnNotifyCreateCharacterDelegate.BindUObject(this, &AOWSPlayerController::NotifyCreateCharacter);
OWSPlayerControllerComponent->OnErrorCreateCharacterDelegate.BindUObject(this, &AOWSPlayerController::ErrorCreateCharacter);

This just references the callbacks in UE blueprints that we need to utilize.

Create Character blueprints

From the screenshot above, we know we want to pass in quite a few different options.

Almost all of them will go into custom character data, except potentially class name + character name, this makes the implementation a bit simpler.

Again, the widgets above were mostly created in these two posts:

So now let’s look at the on-clicked event for Create.

First we want to do some basic validation – make sure that the character name is at least 4 characters. Things like checking character name is unique will be an implicit check when trying to create the character.

If the character name is too short, we create the error widget to surface the error.

If the character name is fine, we start preparing the Map of appearance data. This is the new piece that we added to our C++ code.

There’s numerous ways of achieving this, do whatever you prefer, the goal is, create a Map<String, String> variable which contains all the custom information that you need on the character.

So the way I’ve done it is take my CharacterAppearance structure and add each parameter inside this structure into the Map type, then return the map.

Ok let’s now go back to the flow;

Ok so once you got yourself the Map<String, String> which contains all of the appearance information, one thing that I add to it is the character name.

The reason for this is because the GET Character custom data does not return the character name. This makes it awkward when fetching custom data for multiple characters at the same – they’re async calls so you can’t associate custom data to the character you care about.

This is more of a workaround, the solution should be to add CharacterName to the response of custom character data for the character.

Ok once you have the custom data prepared, all we need to do now is get the OWSLoginPlayerController and call CreateCharacter feeding the inputs we have.

Note the callback is inside the PlayerController class, rather than this widget now, so let’s go there.

And the function to get the CreateCharacterWidget is as follows

Ok so from above we can see that, if Create character callback is successful, we will simply take our create character widget and remove it from viewport, then create the select character widget and add that to the viewport.

If we get an error on create character, we will create error message widget and add that to the viewport, referencing the create character widget in order to disable it while error is up.

That’s it for the create character flow!

Select character widget

The next part that we’ll look at is select character widget.

This widget was created part of this post. We will change the APIs to get the data from OWS instead.

I will try cover some most of the requirements here again.

The general principle is:

As some of the functionality is repeated, I will mainly reference the blueprints without describing too much – check out the post above if you need more info about it. I will explain the new integrated functionality concerning OWS.

Ok, first OWS related function that we will cover is Request to fetch characters.

First let’s understand how it works, and the connecting of it part of Event Construct (Fetch Available Characters) will be shown a bit further down (you can ctrl+F to find it below if want to jump down to see it).

We get the logged in characters via OWSPlayerController – Get All Characters.

The response comes inside the PlayerController, so let’s navigate to those blueprints.

The response comes into Event Notify Get All Characters or Event Error Get All Characters.

The response from this API call does not include custom data.

This means we need to get those custom parameters for each character that we receive.

Note: In create character, we put the appearance data + character name into the custom data. The reason why we added the character name is because as you can see here, we will get multiple characters in response and for each one we want to get their appearance data. We need to associate appearance to character so we need either character ID or name to do so.

Let’s see the C++ implementation for GetCustomCharacterData.


// OWSPlayerControllerComponent.cpp
// GetCustomCharacterData
void UOWSPlayerControllerComponent::GetCustomCharacterData(FString CharName)
{
	FGetCustomCharacterDataJSONPost GetCustomCharacterDataJSONPost;
	GetCustomCharacterDataJSONPost.CharacterName = CharName;
	FString PostParameters = "";
	if (FJsonObjectConverter::UStructToJsonObjectString(GetCustomCharacterDataJSONPost, PostParameters))
	{
		ProcessOWS2POSTRequest("CharacterPersistenceAPI", "api/Characters/GetCustomData", PostParameters, &UOWSPlayerControllerComponent::OnGetCustomCharacterDataResponseReceived);
	}
	else
	{
		UE_LOG(OWS, Error, TEXT("GetCustomCharacterData Error serializing GetCharacterStatsJSONPost!"));
	}
}

I don’t think I modified anything in this function, but bear in mind you will need to make some changes specifically on the delegates.

If you follow this function through, it will execute on success:

OnNotifyGetCustomCharacterDataDelegate.ExecuteIfBound(JsonObject);

and on failure:

		OnErrorGetCustomCharacterDataDelegate.ExecuteIfBound(TEXT("Unknown error connecting to server!"));

These delegates are bound on PlayerController.cpp

OWSPlayerControllerComponent->OnNotifyGetCustomCharacterDataDelegate.BindUObject(this, &AOWSPlayerController::NotifyGetCustomCharacterData);

OWSPlayerControllerComponent->OnErrorGetCustomCharacterDataDelegate.BindUObject(this, &AOWSPlayerController::ErrorCustomCharacterData);

so we know we need to make some changes here.

Let’s check the changes required in NotifyGetCustomCharacterData, some of the important aspects are highlighted.

void AOWSPlayerController::NotifyGetCustomCharacterData(TSharedPtr<FJsonObject> JsonObject)
{
	UE_LOG(OWS, Verbose, TEXT("AOWSPlayerController - NotifyGetCustomCharacterData Started"));
	AOWSCharacter* OWSCharacter = Cast<AOWSCharacter>(GetPawn());

	if (OWSCharacter != NULL)
	{
		OWSCharacter->ProcessCustomCharacterData(JsonObject);
	}

	//Logic extracted from OnGetCosmeticCustomCharacterDataResponseReceived
	TArray<FCustomCharacterDataStruct> CustomCharacterData;

	if (JsonObject->HasField("rows"))
	{
		TArray<TSharedPtr<FJsonValue>> Rows = JsonObject->GetArrayField("rows");

		for (int RowNum = 0; RowNum != Rows.Num(); RowNum++) {
			FCustomCharacterDataStruct tempCustomData;
			TSharedPtr<FJsonObject> tempRow = Rows[RowNum]->AsObject();
			tempCustomData.CustomFieldName = tempRow->GetStringField("CustomFieldName");
			tempCustomData.FieldValue = tempRow->GetStringField("FieldValue");

			CustomCharacterData.Add(tempCustomData);
		}

		NotifyGetCosmeticCustomCharacterData(CustomCharacterData);
	}
	else
	{
		UE_LOG(OWS, Warning, TEXT("ProcessCustomCharacterData Server returned no data!  This can happen when the Character has no Custom Data and might not be an error."));
		ErrorGetCosmeticCustomCharacterData(TEXT("ProcessCustomCharacterData Server returned no data!  This can happen when the Character has no Custom Data and might not be an error."));
	}
}

Note that the logic was mostly extracted from deprecated functionality:

void AOWSPlayerController::OnGetCosmeticCustomCharacterDataResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)

Therefore this is not a ‘sustainable’ solution, its likely the API will change in near future meaning this will need to change along with it. But for now, it will be fine as proof of concept.

Ok but from above, we can see we make a call to:

NotifyGetCosmeticCustomCharacterData

which in the header file is referenced as:

UFUNCTION(BlueprintImplementableEvent, Category = "Stats")
void NotifyGetCosmeticCustomCharacterData(const TArray<FCustomCharacterDataStruct> &CustomCharacterData);

which means we have access of it in Blueprints, so let’s go back there.

Back in the Player Controller, let’s reference these events.

Our logic to process the custom character data is actually in the widget, not the Player Controller. The way we can pass back the response is using event dipatchers.

You create these just under where the variables are referenced.

You will also want to provide it an input, which is an Array of Custom Character Data.

Now we need to bind this event inside your widget to make sure you actually call something when this is executed.

Back inside the Select Character Widget I created a graph for OWS specific logic.

This is part of the Fetch available characters request, which was called in Event construct:

Ok so first we bind the responses for getting our custom character data, then we make the call to fetch the characters that executes the C++ code that we modified.

Let’s now dive a bit deeper into AddCharacterWithAppearanceData functionality.

Essentially, to integrate our widget, we needed to populate the AccountCharacters array, this array contained a list of available characters, which contain the player names and their appearance data.

So ProcessCustomCharacterData is looking to process the custom data and put it into AccountCharacter format. For reference it is:

Note we don’t use accountName at this point.

The CharacterAppearance for reference is:

So ProcessCustomCharacterData is looking to convert Array of Custom Character Data (OWS struct) to AccountCharacter struct.

You don’t need to do this – if you’re generating this logic from scratch, decide your own data structures that work for you, the more structures you have the more painful the conversions could become.

I put this logic in a utils folder, its created as a ‘Blueprint Function Library’ so that it’s accessible by other blueprints.

Essentially the array of variables should contain the following fields that we inserted part of character creation, so we just want to create our struct based off them.

That’s it, after obtaining/converting the OWS response data to our AccountCharacter data, we just switch our selected account character when we click the buttons (referenced part of ReDrawSelectOptions) you can check this part specifically:

Logging into the game

The OWS game uses ThirdPersonCharacterWithAbilities as the main character.

I had my own character component – I can strip all blueprints except the ones which alter its appearance.

You could do one of multiple things:

why you may want to reparent your existing class?

Well this is to make updates easier to handle. Basically you will want and expect OWS to occasionally update their code/blueprints.

If they update their character blueprint and that’s where you made all your changes, it will be difficult to resolve them.

This way, all you need to do is reparent your class and you will inherit all the updates.

If you don’t want to keep anything from the ThirdPersonCharacterWithAbilities, you may want to simply reparent to its parent, MyMMOCharacterWithAbilities which is its parent class.

If you have the ThirdPersonCharacterWithAbilities as your parent, you will just need to ensure that you call the Parent: BeginPlay. You can do this by right clicking the event and selecting Add Call To Parent Function.

For character selection, I use a ‘Proxy’ character, which does not need to call this ‘BeginPlay’. But when you log in as your character into the world, you will want to execute it, so my blueprint looks like this:

By the way, the way the appearance data is pushed to character is via GameInstance, which can be used as short term solution.

There are two ways to populate the appearance data at the start. One is to feed the appearance data in through game instance, then monitor updates.

Another is to pull the data from constructor of your character.

Pulling fresh data on constructor is better but we can cover that next time, because this post is already large!

We can sync up the data from GameInstance because we already put it into the game instance earlier in this post.

The function inside the Character blueprint can look like this:

You can call it from your Character constructor like this:

and that’s it! now you have a character that you can log in and will resolve its appearance correctly with the server on construction.

as a bonus, in order to pull your fresh data, you will want to execute this in your constructor:

But it will need to be executed on server-side.

Then you will want to create a function which will convert this to Appearance Info struct (we re-use same functionality we created in select character screen)

Next you will want to make sure this data is replicated back to client and we can call the Resolve All Appearance. This function needs to be called on client, not the server.

We can look into that next time, best of luck!

Exit mobile version