Skip to content

Commit

Permalink
UE5 game communication support (#116)
Browse files Browse the repository at this point in the history
* Ingame comms/auth/stats/pinging

* Updated UI integrations

* Added new logic for pre-game lobby and timer WIP

* Dynamic class loadingª

* Pinger implementation

* Added server pinging and discovery

* Fixed ping mock service

* Updated ping service params

* Updated server port struct

* Updated launcher parameter for frontend aip

* Updated ping server format and added frontend url as a param reading from the launcher

* Updated launcher, frontend api and game server logic

---------

Co-authored-by: Nikolai Danylchyk <[email protected]>
  • Loading branch information
kennycoder and ext-ndanylchyk authored Mar 7, 2023
1 parent dae9ae4 commit bd35d19
Show file tree
Hide file tree
Showing 50 changed files with 510 additions and 25 deletions.
8 changes: 4 additions & 4 deletions game/Config/DefaultEngine.ini
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@


[/Script/EngineSettings.GameMapsSettings]
EditorStartupMap=/Game/MainMenu/MainMenu.MainMenu
EditorStartupMap=/Game/MainMenu/DS_MainMenuLevel.DS_MainMenuLevel
LocalMapOptions=
TransitionMap=None
bUseSplitscreen=True
Expand All @@ -10,9 +10,9 @@ ThreePlayerSplitscreenLayout=FavorTop
FourPlayerSplitscreenLayout=Grid
bOffsetPlayerGamepadIds=False
GameInstanceClass=/Script/Engine.GameInstance
GameDefaultMap=/Game/MainMenu/MainMenu.MainMenu
ServerDefaultMap=/Game/MainMap.MainMap
GlobalDefaultGameMode=/Game/MainMenu/MainMenuGameModeBP.MainMenuGameModeBP_C
GameDefaultMap=/Game/MainMenu/DS_MainMenuLevel.DS_MainMenuLevel
ServerDefaultMap=/Game/MainGame/DS_MainMapLevel.DS_MainMapLevel
GlobalDefaultGameMode=/Game/MainMenu/DS_MainMenuGameModeBP.DS_MainMenuGameModeBP_C
GlobalDefaultServerGameMode=None

[/Script/HardwareTargeting.HardwareTargetingSettings]
Expand Down
Binary file not shown.
Binary file removed game/Content/DroidshooterGameModeBlueprint.uasset
Binary file not shown.
Binary file modified game/Content/Level_sharedassets/Layer1_LayerInfo.uasset
Binary file not shown.
Binary file modified game/Content/Level_sharedassets/Layer2_LayerInfo.uasset
Binary file not shown.
Binary file modified game/Content/Level_sharedassets/Layer3_LayerInfo.uasset
Binary file not shown.
Binary file modified game/Content/Level_sharedassets/Layer4_LayerInfo.uasset
Binary file not shown.
Binary file modified game/Content/M_Landscape_Blend_Inst.uasset
Binary file not shown.
Binary file added game/Content/MainGame/DS_CountdownWidgetBP.uasset
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added game/Content/MainGame/DS_GameModeBP.uasset
Binary file not shown.
Binary file added game/Content/MainGame/DS_GameState_BP.uasset
Binary file not shown.
Binary file added game/Content/MainGame/DS_MainMapLevel.umap
Binary file not shown.
Binary file not shown.
Binary file added game/Content/MainGame/DS_TimerWidgetBP.uasset
Binary file not shown.
Binary file not shown.
Binary file added game/Content/MainGame/MainMap.umap
Binary file not shown.
Binary file modified game/Content/MainMap.umap
Binary file not shown.
Binary file not shown.
Binary file added game/Content/MainMenu/DS_MainMenuLevel.umap
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified game/Content/MainMenu/MainMenu.umap
Binary file not shown.
Binary file modified game/Content/MainMenu/MainMenuGameModeBP.uasset
Binary file not shown.
Binary file modified game/Content/MainMenu/WelcomeScreenBP.uasset
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added game/Content/Player/DS_PlayerPawnBP.uasset
Binary file not shown.
Binary file removed game/Content/Player/PlayerHUDBlueprint.uasset
Binary file not shown.
Binary file removed game/Content/Player/PlayerPawnBlueprint.uasset
Binary file not shown.
6 changes: 3 additions & 3 deletions game/Droidshooter.uproject
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"FileVersion": 3,
"EngineAssociation": "{5C3B4018-4CD1-8E97-940E-36B7C3CCB0C8}",
"EngineAssociation": "{3D778D7F-47CA-B084-BA54-C0848789AF10}",
"Category": "",
"Description": "",
"Modules": [
Expand All @@ -9,7 +9,8 @@
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine"
"Engine",
"UMG"
]
}
],
Expand All @@ -22,6 +23,5 @@
"Enabled": true,
"Name": "Agones"
}

]
}
6 changes: 3 additions & 3 deletions game/GameLauncher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@ func updateUI(playerName string) {
}

func handlePlay() {
params := fmt.Sprintf("-token=%s", myToken)

params := []string{fmt.Sprintf("-token=%s", myToken), fmt.Sprintf("-frontend_api=%s", iniCfg.Section("").Key("frontend_api").String())}
// Get the binary file from the ini
cmd := exec.Command(iniCfg.Section(runtime.GOOS).Key("binary").String(), params)
cmd := exec.Command(iniCfg.Section(runtime.GOOS).Key("binary").String(), params...)
log.Printf("Launching: %s %s", iniCfg.Section(runtime.GOOS).Key("binary").String(), params)

_, err := cmd.CombinedOutput()
Expand Down
7 changes: 6 additions & 1 deletion game/Source/Droidshooter/Droidshooter.Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ public Droidshooter(ReadOnlyTargetRules Target) : base(Target)
"Agones"
});

PrivateDependencyModuleNames.AddRange(new string[] { });
PrivateDependencyModuleNames.AddRange(new string[] {
"HTTP",
"Json",
"JsonUtilities",
"Icmp"
});

// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
Expand Down
15 changes: 12 additions & 3 deletions game/Source/Droidshooter/DroidshooterGameMode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@

ADroidshooterGameMode::ADroidshooterGameMode()
{
static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBlueprint(TEXT("/Game/Player/PlayerPawnBlueprint"));
// Causes the editor to hang. Loading classname during runtime is much better, check respawn function.
/*static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBlueprint(TEXT("/Game/Player/DS_PlayerPawnBP.DS_PlayerPawnBP_C"));
if (PlayerPawnBlueprint.Class != NULL) {
DefaultPawnClass = PlayerPawnBlueprint.Class;
}
}*/

AgonesSDK = CreateDefaultSubobject<UAgonesComponent>(TEXT("AgonesSDK"));
ApiKey = FWindowsPlatformMisc::GetEnvironmentVariable(*FString("DS_FE_API_KEY"));
}

void ADroidshooterGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
Expand Down Expand Up @@ -102,8 +104,15 @@ void ADroidshooterGameMode::Respawn(AController* Controller)
std::mt19937 gen(rd()); // seed the generator
std::uniform_int_distribution<> distr(-10000, 10000); // define the range

FString TheClassPath = "Class'/Game/Player/DS_PlayerPawnBP.DS_PlayerPawnBP_C'";
const TCHAR* TheClass = *TheClassPath;
UClass* PlayerPawnBlueprintClass = LoadObject<UClass>(nullptr, TheClass);

if (PlayerPawnBlueprintClass == NULL)
return;

FVector Location = FVector(distr(gen), distr(gen), 0);
if (ADroidshooterPlayerPawn* Pawn = GetWorld()->SpawnActor<ADroidshooterPlayerPawn>(DefaultPawnClass, Location, FRotator::ZeroRotator)) {
if (ADroidshooterPlayerPawn* Pawn = GetWorld()->SpawnActor<ADroidshooterPlayerPawn>(PlayerPawnBlueprintClass, Location, FRotator::ZeroRotator)) {
Controller->Possess(Pawn);
ADroidshooterPlayerState* PlayerState = Cast< ADroidshooterPlayerState>(Pawn->GetPlayerState());

Expand Down
2 changes: 2 additions & 0 deletions game/Source/Droidshooter/DroidshooterGameMode.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class DROIDSHOOTER_API ADroidshooterGameMode : public AGameModeBase
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UAgonesComponent* AgonesSDK;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString ApiKey;
private:
TArray<class APlayerStart*> FreePlayerStarts;
};
2 changes: 1 addition & 1 deletion game/Source/Droidshooter/DroidshooterGameStateBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class DROIDSHOOTER_API ADroidshooterGameStateBase : public AGameStateBase
uint16 TotalHits;

UFUNCTION()
void OnRep_TotalHits();
void OnRep_TotalHits();
public:
void PlayerHit();
uint16 GetTotalHits();
Expand Down
256 changes: 256 additions & 0 deletions game/Source/Droidshooter/DroidshooterIntroUserWidget.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// Copyright 2023 Google Inc. All Rights Reserved.Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.


#include "DroidshooterIntroUserWidget.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "Components/TextBlock.h"
#include "Components/EditableTextBox.h"
#include "Components/Button.h"
#include "Droidshooter.h"
#include "Json.h"

void UDroidshooterIntroUserWidget::NativePreConstruct()
{
Super::NativePreConstruct();
// B_Auth->OnClicked.AddDynamic(this, &UDroidshooterIntroUserWidget::AuthenticateCall);
}

void UDroidshooterIntroUserWidget::FindPreferredGameServerLocation(const FString& frontendApi, const FString& accessToken)
{
// Query game server list
// Ping each server
// Create timer to check if all values have been updated
// Save best region
// Query FetchGameServer

UE_LOG(LogDroidshooter, Log, TEXT("Fetch regions to ping with token: %s"), *accessToken);

FString uriPlay = frontendApi + TEXT("/ping");

FHttpModule& httpModule = FHttpModule::Get();
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> pRequest = httpModule.CreateRequest();

pRequest->SetHeader("Authorization", "Bearer " + accessToken);
pRequest->SetVerb(TEXT("GET"));
pRequest->SetURL(uriPlay);

// Set the callback, which will execute when the HTTP call is complete
pRequest->OnProcessRequestComplete().BindLambda(
[&](
FHttpRequestPtr pRequest,
FHttpResponsePtr pResponse,
bool connectedSuccessfully) mutable {

if (connectedSuccessfully) {
ProcessServersToPingResponse(pResponse->GetContentAsString());
}
else {
switch (pRequest->GetStatus()) {
case EHttpRequestStatus::Failed_ConnectionError:
UE_LOG(LogDroidshooter, Log, TEXT("Connection failed."));
default:
UE_LOG(LogDroidshooter, Log, TEXT("Request failed."));
}
}
});

// Finally, submit the request for processing
pRequest->ProcessRequest();
}

void UDroidshooterIntroUserWidget::AuthenticateCall(const FString& frontendApi, const FString& accessToken)
{
UE_LOG(LogDroidshooter, Log, TEXT("Auth call with token: %s"), *accessToken);

FString uriProfile = frontendApi + TEXT("/profile");

FHttpModule& httpModule = FHttpModule::Get();
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> pRequest = httpModule.CreateRequest();

pRequest->SetVerb(TEXT("GET"));
pRequest->SetHeader("Authorization", "Bearer " + accessToken);
pRequest->SetURL(uriProfile);

pRequest->OnProcessRequestComplete().BindLambda(
[&](
FHttpRequestPtr pRequest,
FHttpResponsePtr pResponse,
bool connectedSuccessfully) mutable {

if (connectedSuccessfully) {
ProcessProfileResponse(pResponse->GetContentAsString());
}
else {
switch (pRequest->GetStatus()) {
case EHttpRequestStatus::Failed_ConnectionError:
UE_LOG(LogDroidshooter, Log, TEXT("Connection failed."));
default:
UE_LOG(LogDroidshooter, Log, TEXT("Request failed."));
}
}
});

// Finally, submit the request for processing
pRequest->ProcessRequest();
}

void UDroidshooterIntroUserWidget::FetchGameServer(const FString& frontendApi, const FString& accessToken, const FString preferredRegion, const FString ping)
{
UE_LOG(LogDroidshooter, Log, TEXT("Fetch server/ip call with token: %s"), *accessToken);

FString uriPlay = frontendApi + TEXT("/play?preferred_region=" + preferredRegion + "&ping=" + ping);

FHttpModule& httpModule = FHttpModule::Get();
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> pRequest = httpModule.CreateRequest();

pRequest->SetHeader("Authorization", "Bearer " + accessToken);
pRequest->SetVerb(TEXT("GET"));
pRequest->SetURL(uriPlay);

// Set the callback, which will execute when the HTTP call is complete
pRequest->OnProcessRequestComplete().BindLambda(
[&](
FHttpRequestPtr pRequest,
FHttpResponsePtr pResponse,
bool connectedSuccessfully) mutable {

if (connectedSuccessfully) {
/*std::function<void(const TSharedPtr<FJsonObject>&)> f = [=](const TSharedPtr<FJsonObject>& JsonResponseObject) {
// do stuff here or call a method
};*/
ProcessGameserverResponse(pResponse->GetContentAsString());
}
else {
switch (pRequest->GetStatus()) {
case EHttpRequestStatus::Failed_ConnectionError:
UE_LOG(LogDroidshooter, Log, TEXT("Connection failed."));
default:
UE_LOG(LogDroidshooter, Log, TEXT("Request failed."));
}
}
});

// Finally, submit the request for processing
pRequest->ProcessRequest();
}

void UDroidshooterIntroUserWidget::ProcessProfileResponse(const FString& ResponseContent)
{
TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(ResponseContent);
TSharedPtr<FJsonObject> JsonResponseObject;

if (FJsonSerializer::Deserialize(JsonReader, JsonResponseObject))
{
if (JsonResponseObject)
{
FString Name = JsonResponseObject->GetStringField(TEXT("player_name"));

if (Name.Len() != 0) {
//UE_LOG(LogDroidshooter, Log, TEXT("Logged in player is: %s"), "yes");
UE_LOG(LogDroidshooter, Log, TEXT("Logged in player is: %s"), *Name);

NameTextBlock->SetText(FText::FromString(Name));
}
else {
UE_LOG(LogDroidshooter, Log, TEXT("Unable to fetch player data. Timed out token?"));
}

}
}
}

void UDroidshooterIntroUserWidget::ProcessGameserverResponse(const FString& ResponseContent)
{
TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(ResponseContent);
TSharedPtr<FJsonObject> JsonResponseObject;

if (FJsonSerializer::Deserialize(JsonReader, JsonResponseObject))
{
if (JsonResponseObject)
{
FString IP = JsonResponseObject->GetStringField(TEXT("IP"));
FString Port = JsonResponseObject->GetStringField(TEXT("Port"));

if (IP.Len() != 0 && Port.Len() != 0) {
// Set IP - Port variables!
ServerIPValue = IP;
ServerPortValue = Port;

ServerIPBox->SetText(FText::FromString(IP));
ServerPortBox->SetText(FText::FromString(Port));

UE_LOG(LogDroidshooter, Log, TEXT("Found game server at: %s %s"), *IP, *Port);

}
else {
UE_LOG(LogDroidshooter, Log, TEXT("Unable to server player data. Timed out token?"));
}
}
}
}

void UDroidshooterIntroUserWidget::ProcessServersToPingResponse(const FString& ResponseContent)
{

TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(ResponseContent);
TArray<TSharedPtr<FJsonValue>> JsonResponseArray;

if (FJsonSerializer::Deserialize(JsonReader, JsonResponseArray))
{
ServerPinger.SetServersToValidate(JsonResponseArray.Num());

for (int i = 0; i < JsonResponseArray.Num(); ++i)
{
TSharedPtr<FJsonObject> JsonResponseObject = JsonResponseArray[i]->AsObject();

if (JsonResponseObject)
{
FString Name = JsonResponseObject->GetStringField(TEXT("Name"));
FString Region = JsonResponseObject->GetStringField(TEXT("Region"));
FString Address = JsonResponseObject->GetStringField(TEXT("Address"));
FString Protocol = JsonResponseObject->GetStringField(TEXT("Protocol"));
FString Port = JsonResponseObject->GetStringField(TEXT("Port"));

UE_LOG(LogDroidshooter, Log, TEXT("Gonna ping: %s %s %s %s %s"), *Name, *Region, *Address, *Protocol, *Port);
ServerPinger.CheckIfServerIsOnline(Address, Port);
}
}
}

GetWorld()->GetTimerManager().SetTimer(MemberTimerHandle, this, &UDroidshooterIntroUserWidget::AllServersValidated, 1.0f, true, 2.0f);
}

/*
* Generic handler for json that calls func() passed to it
*/
void UDroidshooterIntroUserWidget::ProcessGenericJsonResponse(const FString& ResponseContent, std::function<void(const TSharedPtr<FJsonObject>&)>& func)
{
TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(ResponseContent);
TSharedPtr<FJsonObject> JsonObject;

if (FJsonSerializer::Deserialize(JsonReader, JsonObject))
{
func(JsonObject);
}
}

void UDroidshooterIntroUserWidget::AllServersValidated()
{
if (ServerPinger.AllServersValidated()) {
auto servers = ServerPinger.GetPingedServers();

for (auto it = servers.begin(); it != servers.end(); ++it)
{
// Sending the first result (best ping) to request servers from openmatch via game frontend
if (it == servers.begin()) {
FetchGameServer(FrontendApi, GlobalAccessToken, it->second, FString::SanitizeFloat(it->first));
}
UE_LOG(LogDroidshooter, Log, TEXT("Ping responses: %.2f %s "), it->first, *it->second);
}

ServerPinger.ClearPingedServers();
GetWorld()->GetTimerManager().ClearTimer(MemberTimerHandle);
}
}
Loading

0 comments on commit bd35d19

Please sign in to comment.