online_leaderboard_tutorial_2

en Tutoriales, UE4

Como crear una tabla de clasificación online (Parte 2)

Ahora que tenemos la parte del servidor lista para funcionar necesitamos implementar el código del lado del cliente. Esta parte se debe encargar de gestionar los resultados de las peticiones http y también de realizar las llamadas al controlador del juego para que muestre por pantalla estos datos.

Parte 1: Archivos del servidor
Parte 2: Archivos del cliente
Parte 3: Integración con el proyecto UE4

C++

Tenemos que abrir las solución de Visual Studio de nuestro proyecto UE4 y añadir una nueva clase LeaderboardManager, o podemos utilizar el mismo método visto en el tutorial sobre cómo añadir contenedores c++.

project solution
Project solution location

Esta clase tiene que gestionar los datos recibidos desde el servidor sobre la tabla de clasificación, tanto la tabla del top 100 como los datos del jugador que hace la petición, y, por supuesto, debe encargarse de la comunicación con nuestro servidor también.

Por ello vamos a preparar un pareja de callbacks éxito/error para cada una de las dos peticiones. La UI deberá mostrar el widget relacionado con cada una de las situaciones usando estas llamadas.

  • 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();
};

Tenemos que añadir la lista de dependencias en el archivo ProjectName.Build.cs antes de poder continuar.En este caso tenemos que añadir las dependencias para las herramientas de http y json.

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

Ahora ya podemos implementar la comunicación con el servidor. Debemos tener en cuenta que hay que añadir el código relacionado con la seguridad básica implementada en los anteriores archivos php, tenemos que generar el hash para la misma estructura de datos usando las mismas palabras secretas que en los php. Si necesitas una implementación en C++ de sha256 te incluyo los ficheros de la clase en un paquete descargable al final de esta publicación. La función encargado del envío de la puntuación, sendScore deberá parecerse a esto:

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

El callback para la anterior petición de sendScore OnResponseSendScore debe comprobar si la petición se ha completado éxitosamente, lo que indicará si las puntuaciones se han insertado correctamente. Para ello tenemos que comprobar el valor devuelto por la función php, si el valor es igual a cero podemos realizar la llamada a la función ShowSuccessSendScore() y que la UI se actualize. En caso de que fallara, extraeríamos el mensaje de error y lo guardaríamos en la variable de nuestro leaderboardManager correspondiente para después realizar la llamada a ShowFailedSend()

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

La función para recuperar la lista de puntuaciones getLeaderboard tiene una parametrización php un poco más simple, solo necesita el userId, y el nombre de la función. Nos devolverá la lista de las mejores 100 puntuaciones y la posición del jugador en el caso de que se encuentre dentro de esta lista. Un valor cero en su posición indicaría que no está en ese Top 100.

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

En el callback de esta petición vamos a extraer la información de la lista y rellenar los miembros de la clase según corresponda, el juego podrá acceder a esta información de manera total o parcial una vez se realice la llamada a ShowSuccessList(). También debemos gestionar la situación de error de la misma manera que hemos hecho en 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();
	}
}

Finalmente el archivo .cpp queda tal que así: (No te olvides de poner la URL de tu archivo gameFunctions.php en el constructor de 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

Es hora de integrar este cliente con nuestro proyecto Blueprint. Cómo crear una tabla de clasificación online (parte 3)

2021/10/05 – Updated to UE4 4.27

Ayudanos con este blog!

El último año he estado dedicando cada vez más tiempo a la creación de tutoriales, en su mayoria sobre desarrollo de videojuegos. Si crees que estos posts te han ayudado de alguna manera o incluso inspirado, por favor considera ayudarnos a mantener este blog con alguna de estas opciones. Gracias por hacerlo posible!

Escribe un comentario

Comentario