online_leaderboard_tutorial_2

in Tutorials, UE4

How to make an online leaderboard (Part 2)

Now that we have the server side ready to go we need to implement the code for the client side. This part needs to manage the results of the http request as well as make the calls to the game controller with this data.

Part 1: Server files
Part 2: Client files
Part 3: UE4 game integration

C++

We need to open the visual studio solution of our game project and add a new class LeaderboardManager, or we can use the same method explained in the previous tutorial about how to add c++ containers.

project solution
Project solution location

This class must manage the data retrieved from the server about the leaderboard, the top 100 list and the personal data and send the user high score, and must take care of the communication with our server.

So we make a success/error callback for each one of our two requests. The UI must show the related widget for each situation using this calls.

  • SendScore
    • ShowSuccessSendScore
    • ShowFailedSend
  • GetLeaderboard
    • ShowSuccessList
    • ShowFailedList
#pragma once

#include "Object.h"
#include "Runtime/Online/HTTP/Public/Http.h"
#include "LeaderboardManager.generated.h"

UCLASS(Blueprintable, BlueprintType)
class PINGVIN_API ULeaderboardManager : public UObject
{
	GENERATED_BODY()

private:
	FHttpModule* Http;
	FString	serverPHPpath;

	//http callbacks
	void OnResponseSendScore(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
	void OnResponseGetLeaderboard(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);

public:

	//personal userId
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Variable")
		FString userId;

	//Leaderboard data Top 100
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Variable")
		TArray t100_names;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Variable")
		TArray t100_score;

	//Leaderboard personal data
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Variable")
		int32 personal_pos;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Variable")
		FString personal_name;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Variable")
		int32 personal_score;

	// returned error message
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Variable")
		FString errorMessage;

	// client functions
	UFUNCTION(BlueprintCallable, Category = "LBM_funtions")
		void setUserId(const FString& newuserId);
	UFUNCTION(BlueprintCallable, Category = "LBM_funtions")
		void sendScore(const FString& userName, const int32& score);
	UFUNCTION(BlueprintCallable, Category = "LBM_funtions")
		void getLeaderboard();

	//Callbacks
	UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "Function")
		void ShowSuccessSendScore();
	UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "Function")
		void ShowFailedSend();

	UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "Function")
		void ShowSuccessList();
	UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "Function")
		void ShowFailedList();

	// Sets default values for this actor's properties
	ULeaderboardManager();
};

We need to add the dependencies list in the ProjectName.Build.cs file before we can continue. In this case for the http and json tools.

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

Now we can implement the communication with the server. Note that we must add the code related to the basic security implemented in the php files, we need to generate the hash for the same data structure with the same words of the phpfiles. If you need a sha256 implementation I will include the files in a downloadable package at the end of this post. The sendScore will look like this:

void ULeaderboardManager::sendScore(const FString& userName, const int32& score)
{
	FString data = "funcName=sendScore";
	std::string output1;
	std::string userName_str(TCHAR_TO_UTF8(*userName));
	if (!userId.IsEmpty())
	{
		data += "&userId=" + userId;
		std::string userid_str(TCHAR_TO_UTF8(*userId));
		output1 = sha256(userid_str + userName_str + "secret1" + to_string(score) + "secret2");
	}
	else
	{
		//is a new user
		output1 = sha256(userName_str + "secret1" + to_string(score) +"secret2");
	}	
	data += "&userName=" + userName + "&score=" + FString::FromInt(score) + "&userData=" + FString(output1.c_str());

	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = Http->CreateRequest();
	Request->OnProcessRequestComplete().BindUObject(this, &ULeaderboardManager::OnResponseSendScore);
	//This is the url on which to process the request
	Request->SetURL(serverPHPpath);
	Request->SetVerb("POST");
	Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
	Request->SetHeader("Content-Type", "application/x-www-form-urlencoded");
	Request->SetContentAsString(data);
	Request->ProcessRequest();
}

The callback for the sendScore http request OnResponseSendScore must check if the request was successful and the score data insertion was OK. To do this we need to check the code return of the our php function, if the value is equal to zero we can use a call to our function ShowSuccessSendScore() to report the success. It it fails, we’ll use the ShowFailedSend(), filling the errorMessage member of our leaderboard manager with the retrieved error message, so the game can access it later.

void ULeaderboardManager::OnResponseSendScore(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	if (bWasSuccessful)
	{
		//Create a pointer to hold the json serialized data
		TSharedPtr JsonObject;

		FString contenico = Response->GetContentAsString();
		//Create a reader pointer to read the json data
		TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());

		//Deserialize the json data given Reader and the actual object to deserialize
		if (FJsonSerializer::Deserialize(Reader, JsonObject))
		{
			//Get the value of the json object by field name
			int32 returnCode = JsonObject->GetIntegerField("returnCode");
			if (returnCode == 0)
			{
				FString receivedUserId = JsonObject->GetStringField("returnData");

				if (userId.IsEmpty())
					userId = receivedUserId;
				ShowSuccessSendScore();
			}
			else
			{
				//show error
				errorMessage = JsonObject->GetStringField("returnData");
				ShowFailedSend();
			}
		}
		else
		{
			errorMessage = "Error on data deserialization";
			ShowFailedSend();
		}
	}
	else
	{
		errorMessage = "Error on http request sendscore";
		ShowFailedSend();
	}
}

The getLeaderboard function has a bit more simple call, it only needs the userId, and the php function name, to retrieve the top 100 list and the current user position if he is in the top. A value of zero in their position indicates that he is not in the top.

void ULeaderboardManager::getLeaderboard()
{
	//Top100+ personalHS
	FString data = "funcName=getLeaderboard";

	if (!userId.IsEmpty())
		data += "&userId=" + userId;

	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = Http->CreateRequest();
	Request->OnProcessRequestComplete().BindUObject(this, &ULeaderboardManager::OnResponseGetLeaderboard);
	//This is the url on which to process the request
	Request->SetURL(serverPHPpath);
	Request->SetVerb("POST");
	Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
	Request->SetHeader("Content-Type", "application/x-www-form-urlencoded");
	Request->SetContentAsString(data);
	Request->ProcessRequest();
}

In the request callback we’ll extract the list info and fill the class members to let the game to access to it on the success function call ShowSuccessList(). We need to manage the error in this function too, with the same method as in OnResponseSendScore

void ULeaderboardManager::OnResponseGetLeaderboard(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	if (bWasSuccessful)
	{
		//Create a pointer to hold the json serialized data
		TSharedPtr JsonObject;

		//Create a reader pointer to read the json data
		TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());

		//Deserialize the json data given Reader and the actual object to deserialize
		if (FJsonSerializer::Deserialize(Reader, JsonObject))
		{
			int32 returnCode = JsonObject->GetIntegerField("returnCode");
			if (returnCode == 0)
			{
				//Get the value of the json object by field name
				TArray<TSharedPtr> arr = JsonObject->GetArrayField("returnData");

				t100_names.Empty();
				t100_score.Empty();

				for (int i = 0; i < arr.Num() - 1; ++i) { auto elemento = arr[i]->AsArray();

					t100_names.Add(elemento[1]->AsString());
					t100_score.Add(elemento[2]->AsNumber());
				}

				//personal result
				auto elemento = arr[arr.Num()-1]->AsArray();
				if (elemento[0]->AsNumber() != 0)
				{
					personal_pos = elemento[0]->AsNumber();
					personal_name = elemento[1]->AsString();
					personal_score = elemento[2]->AsNumber();
				}
				else
				{
					personal_name = "";
					personal_score = 0;
				}

				//Output it to the engine
				ShowSuccessList();
			}
			else
			{
				//muestra error
				errorMessage = JsonObject->GetStringField("returnData");
				ShowFailedSend();
			}
		}
		else
		{
			errorMessage = "Error on data deserialization 2";
			ShowFailedSend();
		}
	}
	else
	{
		errorMessage = "Error on http request leaderboard";
		ShowFailedList();
	}
}

Finally the .cpp file will look like this: (Don’t forget to put the URL to our gameFunctions.php file in the constructor of the LeaderboardManager)

#include "LeaderboardManager.h"

#include "Tutorial.h"
#include "sha256.h"
#include <string>
#include <sstream>

//NDK fix
template 
std::string to_string(T value)
{
	std::ostringstream os;
	os << value;
	return os.str();
}

ULeaderboardManager::ULeaderboardManager()
{
	//When the object is constructed, Get the HTTP module
	Http = &FHttpModule::Get();
	serverPHPpath = "http://*****************/gameFunctions.php"; // replace with the gameFunctions URL
}

void ULeaderboardManager::OnResponseSendScore(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	if (bWasSuccessful)
	{
		//Create a pointer to hold the json serialized data
		TSharedPtr JsonObject;

		FString contenico = Response->GetContentAsString();
		//Create a reader pointer to read the json data
		TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());

		//Deserialize the json data given Reader and the actual object to deserialize
		if (FJsonSerializer::Deserialize(Reader, JsonObject))
		{
			//Get the value of the json object by field name
			int32 returnCode = JsonObject->GetIntegerField("returnCode");
			if (returnCode == 0)
			{
				FString receivedUserId = JsonObject->GetStringField("returnData");

				if (userId.IsEmpty())
					userId = receivedUserId;
				ShowSuccessSendScore();
			}
			else
			{
				//show error
				errorMessage = JsonObject->GetStringField("returnData");
				ShowFailedSend();
			}
		}
		else
		{
			errorMessage = "Error on data deserialization";
			ShowFailedSend();
		}
	}
	else
	{
		errorMessage = "Error on http request sendscore";
		ShowFailedSend();
	}
}

void ULeaderboardManager::OnResponseGetLeaderboard(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	if (bWasSuccessful)
	{
		//Create a pointer to hold the json serialized data
		TSharedPtr JsonObject;

		//Create a reader pointer to read the json data
		TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());

		//Deserialize the json data given Reader and the actual object to deserialize
		if (FJsonSerializer::Deserialize(Reader, JsonObject))
		{
			int32 returnCode = JsonObject->GetIntegerField("returnCode");
			if (returnCode == 0)
			{
				//Get the value of the json object by field name
				TArray<TSharedPtr> arr = JsonObject->GetArrayField("returnData");

				t100_names.Empty();
				t100_score.Empty();

				for (int i = 0; i < arr.Num() - 1; ++i) { auto elemento = arr[i]->AsArray();

					t100_names.Add(elemento[1]->AsString());
					t100_score.Add(elemento[2]->AsNumber());
				}

				//personal result
				auto elemento = arr[arr.Num()-1]->AsArray();
				if (elemento[0]->AsNumber() != 0)
				{
					personal_pos = elemento[0]->AsNumber();
					personal_name = elemento[1]->AsString();
					personal_score = elemento[2]->AsNumber();
				}
				else
				{
					personal_name = "";
					personal_score = 0;
				}

				//Output it to the engine
				ShowSuccessList();
			}
			else
			{
				//muestra error
				errorMessage = JsonObject->GetStringField("returnData");
				ShowFailedSend();
			}
		}
		else
		{
			errorMessage = "Error on data deserialization 2";
			ShowFailedSend();
		}
	}
	else
	{
		errorMessage = "Error on http request leaderboard";
		ShowFailedList();
	}
}

void ULeaderboardManager::setUserId(const FString& newuserId)
{
	userId = newuserId;
}

void ULeaderboardManager::sendScore(const FString& userName, const int32& score)
{
	FString data = "funcName=sendScore";
	std::string output1;
	std::string userName_str(TCHAR_TO_UTF8(*userName));
	if (!userId.IsEmpty())
	{
		data += "&userId=" + userId;
		std::string userid_str(TCHAR_TO_UTF8(*userId));
		output1 = sha256(userid_str + userName_str + "secret1" + to_string(score) + "secret2");
	}
	else
	{
		// is a new user
		output1 = sha256(userName_str + "secret1" + to_string(score) +"secret2");
	}	
	data += "&userName=" + userName + "&score=" + FString::FromInt(score) + "&userData=" + FString(output1.c_str());

	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = Http->CreateRequest();
	Request->OnProcessRequestComplete().BindUObject(this, &ULeaderboardManager::OnResponseSendScore);
	//This is the url on which to process the request
	Request->SetURL(serverPHPpath);
	Request->SetVerb("POST");
	Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
	Request->SetHeader("Content-Type", "application/x-www-form-urlencoded");
	Request->SetContentAsString(data);
	Request->ProcessRequest();
}

void ULeaderboardManager::getLeaderboard()
{
	//Top100+ personalHS
	FString data = "funcName=getLeaderboard";

	if (!userId.IsEmpty())
		data += "&userId=" + userId;

	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = Http->CreateRequest();
	Request->OnProcessRequestComplete().BindUObject(this, &ULeaderboardManager::OnResponseGetLeaderboard);
	//This is the url on which to process the request
	Request->SetURL(serverPHPpath);
	Request->SetVerb("POST");
	Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
	Request->SetHeader("Content-Type", "application/x-www-form-urlencoded");
	Request->SetContentAsString(data);
	Request->ProcessRequest();
}

Tutorial files

It’s time to integrate this client with our Blueprint game project. How to make an online leaderboard (part 3)

2021/10/05 – Updated to UE4 4.27

Support this blog!

For the past year I've been dedicating more of my time to the creation of tutorials, mainly about game development. If you think these posts have either helped or inspired you, please consider supporting this blog. Thank you so much for your contribution!

Write a Comment

Comment