Unreal MMO Development with Yaz

3. UE5 and Java server: Easy steps to Synchronize motion

Note that a lot of ‘movement’ has been updated to use Websockets (and connecting to it via BP only in UE) in chapter 26 and chapter 27.

However, if you’re looking for simple HTTP guide on achieving this, continue reading! 🙂

In the previous post we setup a java server with player controller, which will update a player’s movement.

In this post, we will setup Unreal Engine to update the players movements and display any additional users that may be visible on the map.

To get started, download and install a version of Unreal Engine, I am using 4.26 at the time of writing.

First of all, let’s create a new project we’ll use the third person template for this demonstration.

You may leave the project settings as default and continue. Once UE finishes loading, you will be presented with a screen like this:

Click File -> New C++ Class

Choose Parent Class -> Character

Let’s name the class ConnectedCharacter and hit Create Class.

This will take the project a bit of time to add the code to project. You may have some Visual Studio dependency issues, so please find the libraries that you need to install to have this project running. Visual Studio Installer is very useful to get the dependencies you need. I believe you just need to install the packages for “Unreal C++ games” but take a look through several suggestions here if you’re stuck.

Opening project in Visual Studio

If after creating the new C++ class you’re greeted with terminal window instead of Visual Studio, you may need to open directly from folder;

Simply close the project and find the folder with your project:

Double click the solution and open with your Visual Studio.

After it opens, expand the folder structure and find that your ConnectedCharacter is created as expected.

First of all, just make sure you don’t have any further IDE/Compiler issues, so go ahead and just start the debugger. It will first start compiling and it should run the application. If you’re having issues, try fix them before continuing on.

If all goes well, your debugger should start the UE editor like this:

You can close the application for now as we’ll start making modifications in the code.

Start Making the Connection class

For first step, find your <Project>.Build.cs file (mine called MmoGameBlog.Build.cs) and edit the PublicDependencyModuleNames.

Change it from:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

to:

PublicDependencyModuleNames.AddRange(new string[] {
    "Core",
    "CoreUObject",
    "Engine",
    "InputCore",
    "Json",
    "JsonUtilities",
    "Http"
});

This is because our connected character class will use JSON and HTTP.

Next, open up the ConnectedCharacter.h header file.

Here we will want to do a couple of things:

Connected Character Header Code

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.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;
};

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


UCLASS()
class MMOGAMEBLOG_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);

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

private:
	FHttpModule* Http;

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

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

};

Connected Character C++ Code

// Fill out your copyright notice in the Description page of Project Settings.

#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"

// 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);
#include "Runtime/Online/HTTP/Public/HttpModule.h"
}

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

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

		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"));

		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("playerMotionList"))
			{
				TArray<TSharedPtr<FJsonValue>> Rows = JsonObject->GetArrayField("playerMotionList");

				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");

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


						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"));
	}
}

Now with the code ready, you can start the project (make sure it compiles too!)

We’re ready to start linking the C++ with blueprints and our Java service.

Link Your C++ with Blueprints

Once project is open, navigate to your ThirdPersonBP -> Blueprints folder and create new Blueprint Class:

Next select it to be an ‘Character’ class.

Open it up and select the Mesh component on the left hand side.

Then change the skeletal mesh to a mannequin and set the anim class to Third Person_AnimBP_C. Afterwards, change the location and rotation Z components to -90.

Once this is done compile and save.

On the ProxyCharacter menu, create a new String variable named PlayerName

While you have the Proxy character view open, go to the Event Graph and let’s create some new functionality:

This is skipping a small step, but easier to implement while you’re here.

This is very basic extrapolation/prediction of position for character based on the inputs that we have. I.e. the proxy character are the ‘other’ players in the game. We will have the other players position, rotation and velocity. This bit uses the velocity and moves the actor proportionally to the velocity every delta t. This is better than having the actor ‘teleport’ every delta t but as you can see there’s no smoothing involved here at all. With this approach, if you’re constantly changing direction, there will be clear visibility of the character ‘teleporting’ on updates.

Third Person Character Blueprint setup

Next, let’s open up the ThirdPersonCharacter blueprint.

First thing you will need to do is reparent the class. Click File -> Reparent blueprint then select ConnectedCharacter that we made earlier:

Now click Class defaults and set the proxy character class to the Proxy character we’ve created.

Next, I’d suggest create a new Graph on the left menu, this will make things a little cleaner when proceeding.

Implement Blueprint methods

In the new graph, create new functionality on Notify Update Character

I hope this can be clearly seen, but let’s go through what we’re doing.

First point; the update links to the function from our code (in OnUpdateLocationOnServerResponseReceived):

NotifyUpdateCharacter(tempProxyCharacter);

Let’s look at implementing the next function, Notify New Character

Here, when our code indicates that there’s a new character to display, we spawn that actor to the screen and set their motion components.

Finally, let’s hook up all the components together:

Here we simply wait 1 second after starting and set a timer of 0.05 seconds linked to UpdatePlayerLocation within our C++ class Make sure looping is enabled. Yes, this means we will be sending a lot of requests to our server and expecting to get a lot of responses. Now we can see how the payload can quickly get out of hand.

Let’s start testing!

Test the game server live
Exit mobile version