API Requests mit der Unreal Engine
In diesem Tutorial zeige ich wie man API Requests mit der Unreal Engine macht. Dazu benutze ich das HTTP Modul von Unreal und spreche im C++ Code meine in C# geschriebene .NET API auf meinem Ubuntu Server an.
API Requests mit der Unreal Engine
Fast jede Applikation hat mittlerweile ein Backend, dass eine API bereitstellt um Daten auf einer Cloud oder zumindest zentral für alle User zu speichern. Der Vorteil einer zentralen Speicherung von Daten liegt klar auf der Hand: Daten sind das neue Gold. Die Unreal Engine ist eine sehr beliebte Engine für alle möglichen Clients. Von Spielen angefangen über Renderings bis zu Simulationen. Das Einsatzspektrum reicht von Jus-For-Fun bis zu wissenschaftliche Visualisierungen oder gar Simulationen. Klar, dass hier Daten auch immer eine Rolle spielen.
Ausgangslage
In einem vorangegangenen Artikel habe ich bereits ein Ubuntu Server mit einer Datenbank angelegt und darauf eine simple .NET API Anwendung deployed. Der virtuelle Server läuft während der Entwicklung im lokalen Netzwerk um kosten zu sparen, kann aber jederzeit für eine Produktionsumgebung in die Cloud migriert werden. Weiters habe ich mir die aktuelle Version der Unreal Engine von GitHub heruntergeladen und die Unreal Engine selber kompiliert. Nun wird es Zeit für ein erstes Projekt um Daten vom Server abzufragen.
Neues Projekt
Ich lege für diesen Test ein neues C++ Projekt an:
In der PROJEKTNAME.Base.cs habe ich die Module für HTTP, Json und die JsonUtilities hinzugefügt. Die Funktionalität um HTTP Requests auf eine Rest Schnittstelle mit Json als Austauschformat auszuführen ist damit für das Projekt aktiviert.
In den einzelnen Code Dateien fügt man die nötigen Header für Http und Json hinzu.
Implementierung
Im Unreal Engine Editor habe ich für meine Implementierung eine neue Actor Klasse angelegt. Macht man das, dann öffnet sich der Source Code Editor (Visual Studio oder Visual Studio Code, je nach Einstellung). Dort habe ich folgende Implementierung hinzugefügt:
Im Header eine neue private Variable uriBase. Diese setze ich auf die Domain der REST API. Da diese Variable die Sichtbarkeit EditInstanceOnly besitzt kann man den Wert über das BluePrint im Unreal Editor setzen.
UPROPERTY(EditInstanceOnly, Category="Server") FString uriBase = "https://domain.com/";
In der BeginPlay Methode des Actors rufe ich meine Implementierung GetCountries() auf. Dort hole ich mir die Daten von der REST Schnittstelle.
void ARacingManagerServer::BeginPlay() { Super::BeginPlay(); UE_LOG(LogTemp, Display, TEXT("Game Mode Begin Play called.")); GetCountries(); }
Die vollständige Implementierung für einen Request an die API:
void AMyServer::GetCountries() { UE_LOG(LogTemp, Display, TEXT("GetCountries called.")); FString endpoint = uriBase + "Country"; FHttpModule& httpModule = FHttpModule::Get(); TSharedRef<IHttpRequest, ESPMode::ThreadSafe> pRequest = httpModule.CreateRequest(); pRequest->SetVerb(TEXT("GET")); pRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); pRequest->SetURL(endpoint); pRequest->OnProcessRequestComplete().BindLambda( [&]( FHttpRequestPtr pRequest, FHttpResponsePtr pResponse, bool connectedSuccessfully) mutable { if (connectedSuccessfully) { ProcessCountryResponse(pResponse->GetContentAsString()); } else { switch (pRequest->GetStatus()) { case EHttpRequestStatus::Failed_ConnectionError: UE_LOG(LogTemp, Error, TEXT("Connection failed.")); default: UE_LOG(LogTemp, Error, TEXT("Request failed.")); } } } ); pRequest->ProcessRequest(); }
Zuerst wird der Endpoint (die finale URI) erstellt. In meinem Fall ist das einfach die Domain und Country. Als nächstes erstelle ich ein neues HTTP Request Objekt und konfiguriere dieses als Get-Request, mit einer JSON Antwort und dem Endpoint als Ziel. Spannend ist nun die OnProcessRequestComplete Funktion, dort wird eine Lambda Funktion definiert die bei Erfolgreichen Request ausgeführt wird. Da die Antwort etwas Zeit benötigt (bis zu einige Sekunden) will die Engine nicht darauf warten! zuletzt wird noch der Request gestartet.
Die Lambda Funktion für das Ergebnis des Requests hat 3 Parameter:
- Request
ist das Objekt des Requests das wir an den Server geschickt haben. - Response
ist das Objekt des Responses mit der Antwort vom Server. Vom Response bekommt man das JSON als Text mit GetContentAsString(). Das klappt aber nur im Erfolgsfall. Diesen prüfen wir mit dem Flag. - Flag
das ConnectedSuccessfully Flag ist true, wenn der HTTP Status vom Server 200 entspricht. In diesem Fall rufe ich eine Methode auf die mir den Response String parsed. Im Fehlerfall wird eine Fehlermeldung geloggt.
Parsen
In der Lambda Funktion wird der Response String im Fall einer Erfolgreichen Antwort vom Server geparsed. In der REST Schnittstelle ist die Antwort im JSON Format definiert. Da der HTTP Status 200 war, ist die Antwort damit auch das erwartete JSON. Die Aufgabe der ProcessCountryResponse Funktion ist das Deserialisieren des JSON Strings.
void AMyServer::ProcessCountryResponse(const FString& ResponseContent) { check(IsInGameThread()); TSharedRef<TJsonReader> JsonReader = TJsonReaderFactory::Create(ResponseContent); TArray<TSharedPtr> OutArray; if (FJsonSerializer::Deserialize(JsonReader, OutArray)) { ProcessCountryResponse(OutArray); } }
Die Methode startet mit einem Check ob die Ausführung im GameThread passiert. Das ist nötig, da aller Code der mit der Engine zu tun hat in diesem Thread ausgeführt werden sollte. Es wird ein JsonReader für den übergebenen Content über eine Factory Klasse erstellt und ein neues Array für die Ausgabe angelegt. Mit der Deserialize Funktion wird der Content deserialisiert und ins Array kopiert. Dieses Array übergebe ich nun der nächsten Methode mit dem selben Namen:
void AMyServer::ProcessCountryResponse(const TArray<TSharedPtr>& JsonResponseArray) { UE_LOG(LogTemp, Display, TEXT("Countries found: %d"), JsonResponseArray.Num()); for (int i = 0; i < JsonResponseArray.Num(); ++i) { ProcessCountryResponseObject(JsonResponseArray[i]->AsObject()); } }
Das erhaltene Array wird nun Element für Element durchgegangen. Die Debugausgabe zeigt an wie groß das Array ist. In der Schleife wird jedes Element in die elementaren Bestandteile zerlegt. In der ProcessCountryResponseObject wird nun ein solches JSON Objekt in elementare Elemente wie Zahlen oder Texte zerlegt. Als Dokumentation gebe ich immer die JSON Definition des Elements hinzu. In diesem Fall besteht ein Country Objekt aus einer eindeutigen ID, einem Namen, einem Bild (als Pfad auf ein Bild), einer Hymne (als Pfad auf eine Sounddatei), eine Relevanz und einer Sprache beides als numerischer Wert.
void AMyServer::ProcessCountryResponseObject(const TSharedPtr& JsonResponseObject) { /* "id": 0, "name": "string", "picture": "string", "anthem": "string", "relevance": 0, "language": 0 */ if (JsonResponseObject) { int64 Id; JsonResponseObject->TryGetNumberField(TEXT("id"), Id); FString Name = JsonResponseObject->GetStringField(TEXT("name")); // TODO picture FString Anthem = JsonResponseObject->GetStringField(TEXT("anthem")); int32 Relevance; JsonResponseObject->TryGetNumberField(TEXT("relevance"), Relevance); int32 Language; JsonResponseObject->TryGetNumberField(TEXT("language"), Language); UE_LOG(LogTemp, Display, TEXT("Found country: %s"), *Name); } }
Die Einzelnen Werte bekommt man beispielsweise mit den TryGetNumberField (für Zahlen) oder GetStringField (für Texte). Diese Werte kann man nun in eigene C++ Objekte zusammenfügen oder direkt in der Unreal Engine als Objekte erzeugen.
Testen
Die REST Schnittstelle wird über den Code in der BeginPlay Methode des Server Actors ausgeführt. Damit das auch aufgerufen wird legt man von der Actor Klasse eine BluePrint Klasse an und fügt diese einfach dem Projekt hinzu. Der BluePrint hat kein Mesh, ist also in der Szene nicht sichtbar. Startet man die Szene, dann wird die BeginPlay Methode des Actors getriggert und damit die Requests auf die API ausgeführt.
Um die Schnittstelle zu testen habe ich zahlreiche Log Ausgaben im Code hinzugefügt.Nach einem Klick auf Start im Unreal Engine Editor sieht man das Ergebnis auch gleich im Output Log. Die GetCountries Methode wird ausgeführt. Dort wird der Request erstellt und an den Server geschickt. Das Ergebnis im JSON Format wird danach geparsed. Die Anzahl an Datensätzen + die Namen der einzelnen Länder gebe ich ebenfalls im Log aus.
Fazit
Ich habe gezeigt wie man in der Unreal Engine mit einer REST API kommuniziert. Über einzelne Requests holt man sich Daten im JSON Format vom Server. Dadurch kann man als Spieleentwickler bestimmte Daten auf einen Server auslagern und somit alle Instanzen des Spiels mit stets aktuelle Daten versorgen. Durch eine solche Client/Server Kommunikation sind auch weitere Anwendungsfälle wie beispielsweise ein MMO denkbar.