Unreal MMO Development with Yaz

11. Unreal Engine MMORPG – Simple way you can handle motion

In the previous post we’ve created a character creation screen and we’re able to log into the game.

Now we will aim to make the map a shared space, i.e. you can have multiple players connected and see each other move etc.

A lot of this post is based on the content from this video, which was made using C# and I ported over to Java for a starting point.

I already covered some of this before in posts 1-3 and post 3 contains some of what we’re going to implement specifically in this project.

Here’s a rundown of what’s covered in this post as well as in the next post which covers how to synchronize jump.

Synchronize character motion

So let’s get straight into it.

Setting up connected character

First of all, open up your character blueprint and confirm its parent class

Confirm parent class of your character blueprint

In most cases it should just be Character.

What you will want to do is click Tools -> New C++ class

Create new C++ class
Choose parent class

If your parent class was Character, let’s put that here too, otherwise browse it in the All Classes as necessary.

Name the class and hit ‘Create class’

I will name this class ConnectedCharacter and hit Create class. You can name it as you like, but you may need to make modifications in code provided later to adjust for it.

Adding C++ code to support the motion

Ok now that you’ve created the ConnectedCharacter class, you will want to populate it in C++.

There’s two files that you will need to work with: ConnectedCharacter.h and ConnectedCharacter.cpp

Before we go into the code, let’s quickly discuss: What does the code do?

We’re going to have this be a parent class of any character blueprint in the UE game.

We will have the blueprints fetch the character position, rotation, velocity and if is falling and insert it into the functions we create here.

These functions will then take that data and feed it into our server to update.

We will also fetch all nearby players and return that data back to the client. With this extra data, we will render more characters around, so we can essentially interact with them.

ConnectedCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyUserAccountWidget.h"
#include "Runtime/Online/HTTP/Public/HttpModule.h"
#include "ConnectedCharacter.generated.h"

USTRUCT(BlueprintType)
struct FProxyCharacter
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
		FString PlayerName;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
		FVector PlayerLocation;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
		FRotator PlayerRotation;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
		FVector PlayerVelocity;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
		bool PlayerFalling;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
		FAccountCharacterBase CharacterBase;
};

struct FMotion
{
	FVector Location;
	FRotator Rotation;
	FVector Velocity;
	bool isFalling;
};


UCLASS()
class MMO_PROJECT_API AConnectedCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	AConnectedCharacter();

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
		UClass* ProxyCharacterClass;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
		TMap<FString, FProxyCharacter> ProxyCharacters;

	UFUNCTION(BlueprintCallable, Category = "MMO")
		void UpdateLocationOnServer();

	void OnUpdateLocationOnServerResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);

	UFUNCTION(BlueprintImplementableEvent, Category = "MMO")
		void NotifyNewCharacter(const FProxyCharacter& ProxyCharacter);

	UFUNCTION(BlueprintImplementableEvent, Category = "MMO")
		void NotifyUpdateCharacter(const FProxyCharacter& ProxyCharacter);

	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	UFUNCTION(BlueprintCallable, Category = "MMO")
		void SetApiAccessToken(FString accessToken);

	UFUNCTION(BlueprintCallable, Category = "MMO")
		void SetAccountCharacterBase(FAccountCharacterBase CharacterBase);

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	FString PathToAPI;

private:
	FHttpModule* Http;
	FString AccessToken;
	FAccountCharacterBase CurrentCharacter;
};

If you’ve named the class the same as me, the only change you will need to make to above, is the project name. So find and replace the string MMO_PROJECT_API with your project name as necessary.

ConnectedCharacter.cpp

This class holds the implementation details.

#include "ConnectedCharacter.h"
#include "Runtime/Online/HTTP/Public/Http.h"
#include "Runtime/Engine/Classes/GameFramework/PlayerController.h"
#include "Runtime/Engine/Classes/GameFramework/PlayerState.h"
#include "Runtime/Engine/Classes/GameFramework/CharacterMovementComponent.h"
#include "Runtime/Online/HTTP/Public/HttpModule.h"

// Sets default values
AConnectedCharacter::AConnectedCharacter()
{
	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	// configure your base API path
	PathToAPI = "http://localhost:8081/";
}

// Called when the game starts or when spawned
void AConnectedCharacter::BeginPlay()
{
	Super::BeginPlay();
}

// Called every frame
void AConnectedCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

// Called to bind functionality to input
void AConnectedCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
}

void AConnectedCharacter::SetApiAccessToken(FString accessToken)
{
	this->AccessToken = accessToken;
}

void AConnectedCharacter::SetAccountCharacterBase(FAccountCharacterBase CharacterBase)
{
	this->CurrentCharacter = CharacterBase;
}

void AConnectedCharacter::UpdateLocationOnServer()
{
	APlayerController* PC = Cast<APlayerController>(this->Controller);
	if (PC && PC->PlayerState)
	{
		//FString playerName = PC->PlayerState->GetPlayerName();

		FString playerName = this->CurrentCharacter.Name;

		Http = &FHttpModule::Get();
		TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
		Request->OnProcessRequestComplete().BindUObject(this, &AConnectedCharacter::OnUpdateLocationOnServerResponseReceived);


		// Set your input params - this is the PlayerMotion DTO in Java
		TSharedPtr<FJsonObject> params = MakeShareable(new FJsonObject);
		params->SetStringField(TEXT("playerName"), playerName);

		TSharedPtr<FJsonObject> inputMotion = MakeShareable(new FJsonObject);
		inputMotion->SetNumberField(TEXT("x"), GetActorLocation().X);
		inputMotion->SetNumberField(TEXT("y"), GetActorLocation().Y);
		inputMotion->SetNumberField(TEXT("z"), GetActorLocation().Z);

		inputMotion->SetNumberField(FString(TEXT("roll")), GetActorRotation().Roll);
		inputMotion->SetNumberField(FString(TEXT("pitch")), GetActorRotation().Pitch);
		inputMotion->SetNumberField(FString(TEXT("yaw")), GetActorRotation().Yaw);

		inputMotion->SetNumberField(FString(TEXT("vx")), GetCharacterMovement()->GetLastUpdateVelocity().X);
		inputMotion->SetNumberField(FString(TEXT("vy")), GetCharacterMovement()->GetLastUpdateVelocity().Y);
		inputMotion->SetNumberField(FString(TEXT("vz")), GetCharacterMovement()->GetLastUpdateVelocity().Z);

		inputMotion->SetBoolField(FString(TEXT("isFalling")), GetCharacterMovement()->IsFalling());

		params->SetObjectField(FString(TEXT("motion")), inputMotion);

		FString paramsString;
		TSharedRef<TJsonWriter<TCHAR>> JsonWriter = TJsonWriterFactory<>::Create(&paramsString);
		FJsonSerializer::Serialize(params.ToSharedRef(), JsonWriter);

		FString url = PathToAPI + FString(TEXT("player/update-motion"));

		Request->SetURL(url);
		Request->SetVerb("POST");
		Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
		Request->SetHeader("Content-Type", TEXT("application/json"));
		Request->SetHeader("Accept", TEXT("application/json"));
		if (AccessToken != "")
		{
			FString TokenString = FString(TEXT("Bearer ") + AccessToken);
			Request->SetHeader("Authorization", TokenString);
		}

		Request->SetContentAsString(paramsString);
		Request->ProcessRequest();
	}
}

void AConnectedCharacter::OnUpdateLocationOnServerResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	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");

				for (int i = 0; i < Rows.Num(); i++)
				{
					FProxyCharacter tempProxyCharacter;

					TSharedPtr<FJsonObject> tempRow = Rows[i]->AsObject();

					tempProxyCharacter.PlayerName = tempRow->GetStringField("playerName");
					TSharedPtr<FJsonObject> motion = tempRow->GetObjectField("motion");

					tempProxyCharacter.PlayerLocation.X = motion->GetNumberField("x");
					tempProxyCharacter.PlayerLocation.Y = motion->GetNumberField("y");
					tempProxyCharacter.PlayerLocation.Z = motion->GetNumberField("z");

					tempProxyCharacter.PlayerRotation.Pitch = motion->GetNumberField("pitch");
					tempProxyCharacter.PlayerRotation.Roll = motion->GetNumberField("roll");
					tempProxyCharacter.PlayerRotation.Yaw = motion->GetNumberField("yaw");

					tempProxyCharacter.PlayerVelocity.X = motion->GetNumberField("vx");
					tempProxyCharacter.PlayerVelocity.Y = motion->GetNumberField("vy");
					tempProxyCharacter.PlayerVelocity.Z = motion->GetNumberField("vz");

					tempProxyCharacter.PlayerFalling = motion->GetBoolField("isFalling");

					// Set Character Base information:
					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;
					tempProxyCharacter.CharacterBase = BaseCharacter;

					if (ProxyCharacters.Contains(tempProxyCharacter.PlayerName)) // Update
					{
						ProxyCharacters[tempProxyCharacter.PlayerName].PlayerLocation = tempProxyCharacter.PlayerLocation;
						ProxyCharacters[tempProxyCharacter.PlayerName].PlayerRotation = tempProxyCharacter.PlayerRotation;
						ProxyCharacters[tempProxyCharacter.PlayerName].PlayerVelocity = tempProxyCharacter.PlayerVelocity;
						ProxyCharacters[tempProxyCharacter.PlayerName].PlayerFalling  = tempProxyCharacter.PlayerFalling;

						NotifyUpdateCharacter(tempProxyCharacter);
					}
					else // Add
					{
						ProxyCharacters.Add(tempProxyCharacter.PlayerName, tempProxyCharacter);
						NotifyNewCharacter(tempProxyCharacter);
					}
				}
			}
		}
		else
		{
			UE_LOG(LogTemp, Error, TEXT("OnUpdateLocationOnServerResponseReceived Server received no data"));
		}
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("OnUpdateLocationOnServerResponseReceived Error accessing server"));
	}
}

This class takes in the motion inputs, position, rotation, velocity and whether is falling and sending that information for storage to the server. It also receives back a list of character motions – essentially other players that are on the map. We can use this data to draw up other meshes around the player as required.

We expose several functions that can be called from our blueprint classes:

Set API Access token does as it says – we link the access token from login for our API authentication.

Set Account character base – this will link the logged in character into this class so when we do updates, we do it on correct character.

Event notify – both of these functions will be used to spawn/update the proxy characters around the player.

Linking C++ to Blueprints

Preparing Proxy character class

What is a proxy character? This is basically the class that we’re going to use which represents other players. It’s essentially like pawn class but is very similar to the character class that you’re playing (just that you’re not controlling it, its controlled from server).

Since it’s very similar to our StylisedThirdPerson blueprint, we will copy it over to make the ProxyCharacter class, but before that, let’s refactor one function out which will make things a little easier down the road.

Locate the function LoadCharacterFromSettings in the blueprint that we created earlier.

LocateLoadCharacterFromSettings

The highlighted block can be refactored out – it’s simply setting local variables based on AppearanceInfo contents.

So let’s create a new function, I’ll call it SyncAppearanceInfo and let’s just copy that out and put into new function:

Refactor the functionality into new function

I don’t show all the function as I didn’t modify it – it’s the same as before, but now will be easier to call it.

So now if we go back to LoadCharacterFromSettings we will see a much easier to read function:

LoadCharacterFromSettings after refactor

This will allow us to sync appearance information based on appearance info data – this will help when synchronizing data runtime from server.

Now that we made this function, let’s make a copy of the StylizedThirdPerson blueprint and make our ProxyCharacter.

Copy StylisedThirdPerson blueprint

One quick thing is that few things can be removed from the ProxyCharacter blueprint. Namely anything to do with main character being in control.

For example, any Gamepad input or Mouse input or Movement input functionality etc – they can all be safely deleted from ProxyCharacter.

Let’s add some smoothness – this is by no means enough for production-ready game, but without it game would look way too choppy.

Adding some smoothness to ProxyCharacter

Remember that our server will provide updates about the proxy character – so if there’s lag you may find the character ‘teleporting’ around.

To make it a bit better, you add a level of ‘prediction’ to it, i.e. basically the velocity that the character has should be added to the position every delta seconds (based on framerates etc).

You’d probably want to dampen this effect but let’s just add it directly for now.

This is now prepared enough for what we need so let’s get other blueprints connected.

Login widgets

From the login screen, we’re going to need to pass additional information to the player world. Namely is the character name (as we only passed appearance info before).

Remember, in order to add these as global variables, we need to add it to the GameInstanceSettings blueprint. So let’s open that up and add the CharacterBase variable.

Add CharacterBase variable to GameInstance

Make sure that the eye is open (public) too.

Open up CharacterSelectionWidget and here we will need to populate this variable when the user logs in.

Populate the character base

Now that we’ve populated it, we could in fact remove the setting of appearance info as its included in the character base – we can do this kind of cleanup a little later though after everything is working as expected.

StylisedThirdPerson – Connected character BP

This is the main part of the work. Here we need to link the class to the C++ code that we created. This allows us to update the server with this players motion information and receive information about other players’ motion.

So first thing you want to do is click on File -> Reparent Blueprint and select the ConnectedCharacter that we created with C++.

Once this is done you should see Parent class: ConnectedCharacter on the top right.

Parent class set to ConnectedCharacter

Let’s keep the next blueprints neatly separated, so let’s create a new Graph for this work:

Add Connected Graph

Open up the newly created Connected graph and let’s start adding some blueprints, first we will add the Event Notify New Character.

Event Notify New Character

This event is called from our C++ code. It notifies when there is a user nearby to your character and provides their appearance info + motion information.

With this information, we want to spawn a ProxyCharacter class that we created earlier.

Once we spawn this ProxyCharacter, we add it to a map of characters variable. So to do that, create a variable like this:

Map of characters variable

This provides easy access using character name to the character object.

So after setting the appearance info, we also call the newly created function SyncAppearanceInfo and UpdateMesh which will sync up the characters appearance info and update the mesh as expected.

The next function that we will add is the Event Notify Update Character.

Event Notify Update Character functionality

So after we created the characters, we will get events notifying their updates in motion.

The notify event provides information about their name, motion and appearance info (inside Character Base).

For now, we will just sync up their motion. First we load the reference to the character with the NearbyCharacters variable, then we set the actor location and rotation, as well as getting the character movement and setting the velocity. There’s actually a small bug here relating to characters isFalling state which I will investigate later – but let’s proceed with these motion settings for now.

Wiring it all up

We added the AccountCharacterBase into the GameInstanceSettings class earlier – so we want to pull this information in StylisedThirdPerson when we LoadCharacterFromSettings.

Let’s open up LoadCharacterFromSettings and load this as a local variable now.

Load Account Character Base information

Before we used to only load in appearance information directly. Now we can pull the AccountCharacterBase and get the AppearanceInfo out of it.

Now we have all the data to connect things together so go to the main EventGraph and at the end of EventBeginPlay let’s add few things:

Connect everything together

First we want to set the API Access token which we can fetch from Game Instance Settings object. We then Set Account Character Base. So note that these set the variables inside the C++ classes themselves.

We add a small delay (not really required) and set a timed event. This event will be called every 0.05 seconds so its very frequent – and make sure that looping is enabled.

Inside this event we simply call the Update Location On Server function from the C++ class.

Remember – if you don’t see these blueprint functions popping up, its probably because the class was not reparented correctly to ConnectedCharacter class.

That’s it, you’re ready to start testing!

Connected characters

Note that it will still look a little choppy and stuff – check the YouTube video with results.

To make it better you’d need to add some custom smoothing

Exit mobile version