diff --git a/sdks/unreal/Agones/Agones.uplugin b/sdks/unreal/Agones/Agones.uplugin index 4244e2d11a..b56de87015 100644 --- a/sdks/unreal/Agones/Agones.uplugin +++ b/sdks/unreal/Agones/Agones.uplugin @@ -13,7 +13,7 @@ "MarketplaceURL": "", "Modules": [ { - "LoadingPhase": "PreLoadingScreen", + "LoadingPhase": "Default", "Name": "Agones", "Type": "Runtime" } diff --git a/sdks/unreal/Agones/Source/Agones/Agones.Build.cs b/sdks/unreal/Agones/Source/Agones/Agones.Build.cs index f3ccd8c3bb..1842b6084c 100644 --- a/sdks/unreal/Agones/Source/Agones/Agones.Build.cs +++ b/sdks/unreal/Agones/Source/Agones/Agones.Build.cs @@ -33,9 +33,7 @@ public Agones(ReadOnlyTargetRules target) : base(target) new[] { "CoreUObject", - "Engine", - "Slate", - "SlateCore" + "Engine" }); DynamicallyLoadedModuleNames.AddRange(new string[]{ }); } diff --git a/sdks/unreal/Agones/Source/Agones/Private/AgonesComponent.cpp b/sdks/unreal/Agones/Source/Agones/Private/AgonesSubsystem.cpp similarity index 71% rename from sdks/unreal/Agones/Source/Agones/Private/AgonesComponent.cpp rename to sdks/unreal/Agones/Source/Agones/Private/AgonesSubsystem.cpp index 3fd26d90b0..91a898b17a 100644 --- a/sdks/unreal/Agones/Source/Agones/Private/AgonesComponent.cpp +++ b/sdks/unreal/Agones/Source/Agones/Private/AgonesSubsystem.cpp @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "AgonesComponent.h" +#include "AgonesSubsystem.h" #include "Engine/World.h" #include "HttpModule.h" @@ -34,48 +34,13 @@ typedef ANSICHAR UTF8FromType; template > bool JsonObjectToJsonString(const TSharedRef& JsonObject, FString& OutJson, int32 Indent = 0) { - TSharedRef> JsonWriter = TJsonWriterFactory::Create(&OutJson, Indent); + TSharedRef> JsonWriter = TJsonWriterFactory::Create(&OutJson, Indent); bool bSuccess = FJsonSerializer::Serialize(JsonObject, JsonWriter); JsonWriter->Close(); return bSuccess; } -UAgonesComponent::UAgonesComponent() -{ - PrimaryComponentTick.bCanEverTick = false; -} - -void UAgonesComponent::BeginPlay() -{ - Super::BeginPlay(); - HealthPing(HealthRateSeconds); - - if (bDisableAutoConnect) - { - return; - } - Connect(); -} - -void UAgonesComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) -{ - Super::EndPlay(EndPlayReason); - - const UWorld* World = GetWorld(); - if (World != nullptr) - { - World->GetTimerManager().ClearTimer(ConnectDelTimerHandle); - World->GetTimerManager().ClearTimer(HealthTimerHandler); - World->GetTimerManager().ClearTimer(EnsureWebSocketTimerHandler); - } - - if (WatchWebSocket != nullptr && WatchWebSocket->IsConnected()) - { - WatchWebSocket->Close(); - } -} - -void UAgonesComponent::UpdateCounter(const FString& Key, const int64* Count, const int64* Capacity, const int64* CountDiff, FUpdateCounterDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::UpdateCounter(const FString& Key, const int64* Count, const int64* Capacity, const int64* CountDiff, FUpdateCounterDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) { TSharedRef JsonObject = MakeShareable(new FJsonObject()); @@ -113,7 +78,7 @@ void UAgonesComponent::UpdateCounter(const FString& Key, const int64* Count, con Request->ProcessRequest(); } -FHttpRequestRef UAgonesComponent::BuildAgonesRequest(const FString Path, const FHttpVerb Verb, const FString Content) +FHttpRequestRef UAgonesSubsystem::BuildAgonesRequest(const FString Path, const FHttpVerb Verb, const FString Content) { FHttpModule* Http = &FHttpModule::Get(); FHttpRequestRef Request = Http->CreateRequest(); @@ -130,7 +95,69 @@ FHttpRequestRef UAgonesComponent::BuildAgonesRequest(const FString Path, const F return Request; } -void UAgonesComponent::HealthPing(const float RateSeconds) +UAgonesSubsystem* UAgonesSubsystem::Get(const UObject* WorldContext) +{ + auto World = GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::LogAndReturnNull); + + const UGameInstance* GameInstance = World ? GameInstance = World->GetGameInstance() : Cast(WorldContext->GetOuter()); + + return GameInstance ? GameInstance->GetSubsystem() : nullptr; +} + +UAgonesSubsystem::UAgonesSubsystem() + : UGameInstanceSubsystem() +{ + if (!HasAllFlags(RF_ClassDefaultObject)) + { + const FTickerDelegate TickDelegate = FTickerDelegate::CreateUObject(this, &UAgonesSubsystem::Tick); + TickHandle = FTSTicker::GetCoreTicker().AddTicker(TickDelegate); + + TimerManager = MakeUnique(); + } +} + +UAgonesSubsystem::~UAgonesSubsystem() +{ + FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); +} + +bool UAgonesSubsystem::ShouldCreateSubsystem(UObject *Outer) const +{ + return UE_SERVER; +} + +void UAgonesSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + if (!bDisableAutoHealthPing) + { + HealthPing(HealthRateSeconds); + } + + if (!bDisableAutoConnect) + { + Connect(); + } +} + +void UAgonesSubsystem::Deinitialize() +{ + Super::Deinitialize(); + + if (WatchWebSocket != nullptr && WatchWebSocket->IsConnected()) + { + WatchWebSocket->Close(); + } +} + +bool UAgonesSubsystem::Tick(float DeltaTime) +{ + TimerManager->Tick(DeltaTime); + return true; +} + +void UAgonesSubsystem::HealthPing(const float RateSeconds) { if (RateSeconds <= 0.0f) { @@ -138,29 +165,29 @@ void UAgonesComponent::HealthPing(const float RateSeconds) } FTimerDelegate TimerDel; - TimerDel.BindUObject(this, &UAgonesComponent::Health, FHealthDelegate(), FAgonesErrorDelegate()); - GetWorld()->GetTimerManager().ClearTimer(HealthTimerHandler); - GetWorld()->GetTimerManager().SetTimer(HealthTimerHandler, TimerDel, RateSeconds, true); + TimerDel.BindUObject(this, &UAgonesSubsystem::Health, FHealthDelegate(), FAgonesErrorDelegate()); + GetTimerManager()->ClearTimer(HealthTimerHandler); + GetTimerManager()->SetTimer(HealthTimerHandler, TimerDel, RateSeconds, true); } -void UAgonesComponent::Connect() +void UAgonesSubsystem::Connect() { FGameServerDelegate SuccessDel; SuccessDel.BindUFunction(this, FName("ConnectSuccess")); FTimerDelegate ConnectDel; - ConnectDel.BindUObject(this, &UAgonesComponent::GameServer, SuccessDel, FAgonesErrorDelegate()); - GetWorld()->GetTimerManager().ClearTimer(ConnectDelTimerHandle); - GetWorld()->GetTimerManager().SetTimer(ConnectDelTimerHandle, ConnectDel, 5.f, true); + ConnectDel.BindUObject(this, &UAgonesSubsystem::GameServer, SuccessDel, FAgonesErrorDelegate()); + GetTimerManager()->ClearTimer(ConnectDelTimerHandle); + GetTimerManager()->SetTimer(ConnectDelTimerHandle, ConnectDel, 5.f, true); } -void UAgonesComponent::ConnectSuccess(const FGameServerResponse GameServerResponse) +void UAgonesSubsystem::ConnectSuccess(const FGameServerResponse GameServerResponse) { - GetWorld()->GetTimerManager().ClearTimer(ConnectDelTimerHandle); + GetTimerManager()->ClearTimer(ConnectDelTimerHandle); Ready({}, {}); ConnectedDelegate.Broadcast(GameServerResponse); } -void UAgonesComponent::Ready(const FReadyDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::Ready(const FReadyDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest("ready"); Request->OnProcessRequestComplete().BindWeakLambda(this, @@ -175,13 +202,13 @@ void UAgonesComponent::Ready(const FReadyDelegate SuccessDelegate, const FAgones Request->ProcessRequest(); } -void UAgonesComponent::GameServer(const FGameServerDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::GameServer(const FGameServerDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest("gameserver", FHttpVerb::Get, ""); Request->OnProcessRequestComplete().BindWeakLambda(this, [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { TSharedPtr JsonObject; - + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) { return; @@ -192,91 +219,91 @@ void UAgonesComponent::GameServer(const FGameServerDelegate SuccessDelegate, con Request->ProcessRequest(); } -void UAgonesComponent::EnsureWebSocketConnection() -{ - if (WatchWebSocket == nullptr) - { - if (!FModuleManager::LoadModulePtr(TEXT("WebSockets"))) - { - return; - } - - TMap Headers; - - // Make up a WebSocket-Key value. It can be anything! - Headers.Add(TEXT("Sec-WebSocket-Key"), FGuid::NewGuid().ToString(EGuidFormats::Short)); - Headers.Add(TEXT("Sec-WebSocket-Version"), TEXT("13")); - Headers.Add(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent")); - - // Unreal WebSockets are not able to do DNS resolution for localhost for some reason - // so this is using the IPv4 Loopback Address instead. - WatchWebSocket = FWebSocketsModule::Get().CreateWebSocket( - FString::Format(TEXT("ws://127.0.0.1:{0}/watch/gameserver"), - static_cast( - TArray>{ - FStringFormatArg(HttpPort) - } - ) - ), - TEXT("") - ); - - WatchWebSocket->OnRawMessage().AddUObject(this, &UAgonesComponent::HandleWatchMessage); - } - - if (WatchWebSocket != nullptr) - { - if (!WatchWebSocket->IsConnected()) - { - WatchWebSocket->Connect(); - } - - // Only start the timer if there is a websocket to check. - // This timer has nothing to do with health and only matters if the agent is somehow - // restarted, which would be a failure condition in normal operation. - if (!EnsureWebSocketTimerHandler.IsValid()) - { - FTimerDelegate TimerDel; - TimerDel.BindUObject(this, &UAgonesComponent::EnsureWebSocketConnection); - GetWorld()->GetTimerManager().SetTimer( - EnsureWebSocketTimerHandler, TimerDel, 15.0f, true); - } - } -} - -void UAgonesComponent::WatchGameServer(const FGameServerDelegate WatchDelegate) -{ - WatchGameServerCallbacks.Add(WatchDelegate); - EnsureWebSocketConnection(); -} - - void UAgonesComponent::DeserializeAndBroadcastWatch(FString const& JsonString) -{ - TSharedRef> const JsonReader = TJsonReaderFactory::Create(JsonString); - - TSharedPtr JsonObject; - const TSharedPtr* ResultObject = nullptr; - - if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || - !JsonObject.IsValid() || - !JsonObject->TryGetObjectField(TEXT("result"), ResultObject) || - !ResultObject->IsValid()) - { - UE_LOG(LogAgones, Error, TEXT("Failed to parse json: %s"), *JsonString); - return; - } - - FGameServerResponse const Result = FGameServerResponse(*ResultObject); - for (FGameServerDelegate const& Callback : WatchGameServerCallbacks) - { - if (Callback.IsBound()) - { - Callback.Execute(Result); - } - } -} - -void UAgonesComponent::HandleWatchMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining) +void UAgonesSubsystem::EnsureWebSocketConnection() +{ + if (WatchWebSocket == nullptr) + { + if (!FModuleManager::LoadModulePtr(TEXT("WebSockets"))) + { + return; + } + + TMap Headers; + + // Make up a WebSocket-Key value. It can be anything! + Headers.Add(TEXT("Sec-WebSocket-Key"), FGuid::NewGuid().ToString(EGuidFormats::Short)); + Headers.Add(TEXT("Sec-WebSocket-Version"), TEXT("13")); + Headers.Add(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent")); + + // Unreal WebSockets are not able to do DNS resolution for localhost for some reason + // so this is using the IPv4 Loopback Address instead. + WatchWebSocket = FWebSocketsModule::Get().CreateWebSocket( + FString::Format(TEXT("ws://127.0.0.1:{0}/watch/gameserver"), + static_cast( + TArray>{ + FStringFormatArg(HttpPort) + } + ) + ), + TEXT("") + ); + + WatchWebSocket->OnRawMessage().AddUObject(this, &UAgonesSubsystem::HandleWatchMessage); + } + + if (WatchWebSocket != nullptr) + { + if (!WatchWebSocket->IsConnected()) + { + WatchWebSocket->Connect(); + } + + // Only start the timer if there is a websocket to check. + // This timer has nothing to do with health and only matters if the agent is somehow + // restarted, which would be a failure condition in normal operation. + if (!EnsureWebSocketTimerHandler.IsValid()) + { + FTimerDelegate TimerDel; + TimerDel.BindUObject(this, &UAgonesSubsystem::EnsureWebSocketConnection); + GetTimerManager()->SetTimer( + EnsureWebSocketTimerHandler, TimerDel, 15.0f, true); + } + } +} + +void UAgonesSubsystem::WatchGameServer(const FGameServerDelegate WatchDelegate) +{ + WatchGameServerCallbacks.Add(WatchDelegate); + EnsureWebSocketConnection(); +} + + void UAgonesSubsystem::DeserializeAndBroadcastWatch(FString const& JsonString) +{ + TSharedRef> const JsonReader = TJsonReaderFactory::Create(JsonString); + + TSharedPtr JsonObject; + const TSharedPtr* ResultObject = nullptr; + + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || + !JsonObject.IsValid() || + !JsonObject->TryGetObjectField(TEXT("result"), ResultObject) || + !ResultObject->IsValid()) + { + UE_LOG(LogAgones, Error, TEXT("Failed to parse json: %s"), *JsonString); + return; + } + + FGameServerResponse const Result = FGameServerResponse(*ResultObject); + for (FGameServerDelegate const& Callback : WatchGameServerCallbacks) + { + if (Callback.IsBound()) + { + Callback.Execute(Result); + } + } +} + +void UAgonesSubsystem::HandleWatchMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining) { if (BytesRemaining <= 0 && (WatchMessageBuffer.Num() == 0)) { @@ -296,7 +323,7 @@ void UAgonesComponent::HandleWatchMessage(const void* Data, SIZE_T Size, SIZE_T WatchMessageBuffer.Empty(); } -void UAgonesComponent::SetLabel( +void UAgonesSubsystem::SetLabel( const FString& Key, const FString& Value, const FSetLabelDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { const FKeyValuePair Label = {Key, Value}; @@ -326,7 +353,7 @@ void UAgonesComponent::SetLabel( Request->ProcessRequest(); } -void UAgonesComponent::Health(const FHealthDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::Health(const FHealthDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest("health"); Request->OnProcessRequestComplete().BindWeakLambda(this, @@ -341,7 +368,7 @@ void UAgonesComponent::Health(const FHealthDelegate SuccessDelegate, const FAgon Request->ProcessRequest(); } -void UAgonesComponent::Shutdown(const FShutdownDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::Shutdown(const FShutdownDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest("shutdown"); Request->OnProcessRequestComplete().BindWeakLambda(this, @@ -356,7 +383,7 @@ void UAgonesComponent::Shutdown(const FShutdownDelegate SuccessDelegate, const F Request->ProcessRequest(); } -void UAgonesComponent::SetAnnotation( +void UAgonesSubsystem::SetAnnotation( const FString& Key, const FString& Value, const FSetAnnotationDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { const FKeyValuePair Label = {Key, Value}; @@ -365,11 +392,11 @@ void UAgonesComponent::SetAnnotation( { ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("error serializing key-value pair ({0}: {1}})"), static_cast( - TArray>{ - FStringFormatArg(Key), - FStringFormatArg(Value) - } - ) + TArray>{ + FStringFormatArg(Key), + FStringFormatArg(Value) + } + ) )}); return; } @@ -387,7 +414,7 @@ void UAgonesComponent::SetAnnotation( Request->ProcessRequest(); } -void UAgonesComponent::Allocate(const FAllocateDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::Allocate(const FAllocateDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest("allocate"); Request->OnProcessRequestComplete().BindWeakLambda(this, @@ -402,7 +429,7 @@ void UAgonesComponent::Allocate(const FAllocateDelegate SuccessDelegate, const F Request->ProcessRequest(); } -void UAgonesComponent::Reserve( +void UAgonesSubsystem::Reserve( const int64 Seconds, const FReserveDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { const FDuration Duration = {Seconds}; @@ -426,7 +453,7 @@ void UAgonesComponent::Reserve( Request->ProcessRequest(); } -void UAgonesComponent::PlayerConnect( +void UAgonesSubsystem::PlayerConnect( const FString PlayerId, const FPlayerConnectDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { const FAgonesPlayer Player = {PlayerId}; @@ -444,7 +471,7 @@ void UAgonesComponent::PlayerConnect( Request->OnProcessRequestComplete().BindWeakLambda(this, [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { TSharedPtr JsonObject; - + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) { return; @@ -455,7 +482,7 @@ void UAgonesComponent::PlayerConnect( Request->ProcessRequest(); } -void UAgonesComponent::PlayerDisconnect( +void UAgonesSubsystem::PlayerDisconnect( const FString PlayerId, const FPlayerDisconnectDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { const FAgonesPlayer Player = {PlayerId}; @@ -473,7 +500,7 @@ void UAgonesComponent::PlayerDisconnect( Request->OnProcessRequestComplete().BindWeakLambda(this, [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { TSharedPtr JsonObject; - + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) { return; @@ -484,7 +511,7 @@ void UAgonesComponent::PlayerDisconnect( Request->ProcessRequest(); } -void UAgonesComponent::SetPlayerCapacity( +void UAgonesSubsystem::SetPlayerCapacity( const int64 Count, const FSetPlayerCapacityDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { const FPlayerCapacity PlayerCapacity = {Count}; @@ -508,7 +535,7 @@ void UAgonesComponent::SetPlayerCapacity( Request->ProcessRequest(); } -void UAgonesComponent::GetCounter(FString Key, FGetCounterDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::GetCounter(FString Key, FGetCounterDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest(FString::Format(TEXT("v1beta1/counters/{0}"), {Key}), FHttpVerb::Get, ""); Request->OnProcessRequestComplete().BindWeakLambda(this, @@ -525,7 +552,7 @@ void UAgonesComponent::GetCounter(FString Key, FGetCounterDelegate SuccessDelega Request->ProcessRequest(); } -void UAgonesComponent::IncrementCounter(FString Key, int64 Amount, FIncrementCounterDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::IncrementCounter(FString Key, int64 Amount, FIncrementCounterDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) { const auto UpdateSuccessDelegate = FUpdateCounterDelegate::CreateLambda([SuccessDelegate](const FEmptyResponse&) { @@ -534,7 +561,7 @@ void UAgonesComponent::IncrementCounter(FString Key, int64 Amount, FIncrementCou UpdateCounter(Key, nullptr, nullptr, &Amount, UpdateSuccessDelegate, ErrorDelegate); } -void UAgonesComponent::DecrementCounter(FString Key, int64 Amount, FDecrementCounterDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::DecrementCounter(FString Key, int64 Amount, FDecrementCounterDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) { const int64 NegativeAmount = -Amount; const auto UpdateSuccessDelegate = FUpdateCounterDelegate::CreateLambda([SuccessDelegate](const FEmptyResponse&) @@ -544,7 +571,7 @@ void UAgonesComponent::DecrementCounter(FString Key, int64 Amount, FDecrementCou UpdateCounter(Key, nullptr, nullptr, &NegativeAmount, UpdateSuccessDelegate, ErrorDelegate); } -void UAgonesComponent::SetCounterCount(FString Key, int64 Count, FSetCounterCountDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::SetCounterCount(FString Key, int64 Count, FSetCounterCountDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) { const auto UpdateSuccessDelegate = FUpdateCounterDelegate::CreateLambda([SuccessDelegate](const FEmptyResponse&) { @@ -553,7 +580,7 @@ void UAgonesComponent::SetCounterCount(FString Key, int64 Count, FSetCounterCoun UpdateCounter(Key, &Count, nullptr, nullptr, UpdateSuccessDelegate, ErrorDelegate); } -void UAgonesComponent::SetCounterCapacity(FString Key, int64 Capacity, FSetCounterCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::SetCounterCapacity(FString Key, int64 Capacity, FSetCounterCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) { const auto UpdateSuccessDelegate = FUpdateCounterDelegate::CreateLambda([SuccessDelegate](const FEmptyResponse&) { @@ -562,13 +589,18 @@ void UAgonesComponent::SetCounterCapacity(FString Key, int64 Capacity, FSetCount UpdateCounter(Key, nullptr, &Capacity, nullptr, UpdateSuccessDelegate, ErrorDelegate); } -void UAgonesComponent::GetPlayerCapacity(FGetPlayerCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +FTimerManager* UAgonesSubsystem::GetTimerManager() const +{ + return TimerManager.Get(); +} + +void UAgonesSubsystem::GetPlayerCapacity(FGetPlayerCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest("alpha/player/capacity", FHttpVerb::Get, ""); Request->OnProcessRequestComplete().BindWeakLambda(this, [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { TSharedPtr JsonObject; - + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) { return; @@ -579,13 +611,13 @@ void UAgonesComponent::GetPlayerCapacity(FGetPlayerCapacityDelegate SuccessDeleg Request->ProcessRequest(); } -void UAgonesComponent::GetPlayerCount(FGetPlayerCountDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +void UAgonesSubsystem::GetPlayerCount(FGetPlayerCountDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest("alpha/player/count", FHttpVerb::Get, ""); Request->OnProcessRequestComplete().BindWeakLambda(this, [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { TSharedPtr JsonObject; - + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) { return; @@ -596,7 +628,7 @@ void UAgonesComponent::GetPlayerCount(FGetPlayerCountDelegate SuccessDelegate, F Request->ProcessRequest(); } -void UAgonesComponent::IsPlayerConnected( +void UAgonesSubsystem::IsPlayerConnected( const FString PlayerId, const FIsPlayerConnectedDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest( @@ -614,7 +646,7 @@ void UAgonesComponent::IsPlayerConnected( Request->OnProcessRequestComplete().BindWeakLambda(this, [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { TSharedPtr JsonObject; - + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) { return; @@ -625,7 +657,7 @@ void UAgonesComponent::IsPlayerConnected( Request->ProcessRequest(); } -void UAgonesComponent::GetConnectedPlayers( +void UAgonesSubsystem::GetConnectedPlayers( const FGetConnectedPlayersDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) { FHttpRequestRef Request = BuildAgonesRequest("alpha/player/connected/{0}", FHttpVerb::Get, ""); @@ -643,7 +675,7 @@ void UAgonesComponent::GetConnectedPlayers( Request->ProcessRequest(); } -bool UAgonesComponent::IsValidResponse(const bool bSucceeded, const FHttpResponsePtr HttpResponse, FAgonesErrorDelegate ErrorDelegate) +bool UAgonesSubsystem::IsValidResponse(const bool bSucceeded, const FHttpResponsePtr HttpResponse, FAgonesErrorDelegate ErrorDelegate) { if (!bSucceeded) { @@ -658,8 +690,8 @@ bool UAgonesComponent::IsValidResponse(const bool bSucceeded, const FHttpRespons static_cast( TArray>{ FStringFormatArg(FString::FromInt(HttpResponse->GetResponseCode())) - }) - ) + }) + ) } ); return false; @@ -668,7 +700,7 @@ bool UAgonesComponent::IsValidResponse(const bool bSucceeded, const FHttpRespons return true; } -bool UAgonesComponent::IsValidJsonResponse(TSharedPtr& JsonObject, const bool bSucceeded, const FHttpResponsePtr HttpResponse, FAgonesErrorDelegate ErrorDelegate) +bool UAgonesSubsystem::IsValidJsonResponse(TSharedPtr& JsonObject, const bool bSucceeded, const FHttpResponsePtr HttpResponse, FAgonesErrorDelegate ErrorDelegate) { if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) { @@ -684,8 +716,8 @@ bool UAgonesComponent::IsValidJsonResponse(TSharedPtr& JsonObject, static_cast( TArray>{ FStringFormatArg(Json) - }) - ) + }) + ) }); return false; } diff --git a/sdks/unreal/Agones/Source/Agones/Public/AgonesComponent.h b/sdks/unreal/Agones/Source/Agones/Public/AgonesSubsystem.h similarity index 86% rename from sdks/unreal/Agones/Source/Agones/Public/AgonesComponent.h rename to sdks/unreal/Agones/Source/Agones/Public/AgonesSubsystem.h index 307466a814..d12e5fe6ec 100644 --- a/sdks/unreal/Agones/Source/Agones/Public/AgonesComponent.h +++ b/sdks/unreal/Agones/Source/Agones/Public/AgonesSubsystem.h @@ -19,8 +19,10 @@ #include "CoreMinimal.h" #include "Interfaces/IHttpRequest.h" #include "IWebSocket.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "TimerManager.h" -#include "AgonesComponent.generated.h" +#include "AgonesSubsystem.generated.h" DECLARE_DYNAMIC_DELEGATE_OneParam(FAgonesErrorDelegate, const FAgonesError&, Error); @@ -103,16 +105,21 @@ class FHttpVerb }; /** - * \brief UAgonesComponent is the Unreal Component to call to the Agones SDK. + * \brief UAgonesSubsystem is the Unreal Component to call to the Agones SDK. * See - https://agones.dev/ for more information. */ -UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent), Config = Game, defaultconfig) -class AGONES_API UAgonesComponent final : public UActorComponent +UCLASS(Config = Game, defaultconfig) +class AGONES_API UAgonesSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: - UAgonesComponent(); + /** + * \brief Retrive subsystem component from game instance. Subsystem exist only on server builds! See ShouldCreateSubsystem. + * \param WorldContext - context of the world + */ + UFUNCTION(BlueprintPure, meta = (WorldContext = "WorldContext"), DisplayName = "Get Agones Subsystem", Category = "Agones | Utility") + static UAgonesSubsystem* Get(const UObject* WorldContext); /** * \brief HttpPort is the default Agones HTTP port to use. @@ -123,13 +130,19 @@ class AGONES_API UAgonesComponent final : public UActorComponent /** * \brief HealthRateSeconds is the frequency to send Health calls. Value of 0 will disable auto health calls. */ - UPROPERTY(EditAnywhere, Category = Agones, Config) + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Agones, Config) float HealthRateSeconds = 10.f; /** - * \brief bDisableAutoConnect will stop the component auto connecting (calling GamesServer and Ready). + * \brief bDisableAutoHealthPing will stop call to HealhPing() during initialization */ - UPROPERTY(EditAnywhere, Category = Agones, Config) + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Agones, Config) + bool bDisableAutoHealthPing; + + /** + * \brief bDisableAutoConnect will stop auto connecting (calling GamesServer and Ready) during initialization. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Agones, Config) bool bDisableAutoConnect; /** @@ -138,16 +151,29 @@ class AGONES_API UAgonesComponent final : public UActorComponent UPROPERTY(BlueprintAssignable, Category = Agones) FConnectedDelegate ConnectedDelegate; + UAgonesSubsystem(); + ~UAgonesSubsystem(); + /** - * \brief BeginPlay is a built in UE4 function that is called as the component is created. + * \brief ShouldCreateSubsystem is a built in subsystem function that is called before Initialize. */ - virtual void BeginPlay() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; /** - * \brief EndPlay is a built in UE4 function that is called as the component is destroyed. - * \param EndPlayReason reason for Ending Play. - */ - virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + * \brief Initialize is a built in subsystem function for initilization of subsystem. + * \param Collection can help initialize subsystem dependencies + */ + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + + /** + * \brief Deinitialize is a built in subsystem function called during destruction of GameInstance. + */ + virtual void Deinitialize() override; + + /** + * \brief Tick is a built in ticker function called every frame. + */ + bool Tick(float DeltaTime); /** * \brief HealthPing loops calling the Health endpoint. @@ -353,6 +379,8 @@ class AGONES_API UAgonesComponent final : public UActorComponent void SetCounterCapacity(FString Key, int64 Capacity, FSetCounterCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); private: + FTimerManager* GetTimerManager() const; + DECLARE_DELEGATE_OneParam(FUpdateCounterDelegate, const FEmptyResponse&); void UpdateCounter(const FString& Key, const int64* Count, const int64* Capacity, const int64* CountDiff, FUpdateCounterDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); @@ -365,12 +393,16 @@ class AGONES_API UAgonesComponent final : public UActorComponent void EnsureWebSocketConnection(); + FTSTicker::FDelegateHandle TickHandle; + FTimerHandle ConnectDelTimerHandle; FTimerHandle HealthTimerHandler; FTimerHandle EnsureWebSocketTimerHandler; + TUniquePtr TimerManager; + TSharedPtr WatchWebSocket; TArray WatchMessageBuffer; diff --git a/site/content/en/docs/Guides/Client SDKs/unreal.md b/site/content/en/docs/Guides/Client SDKs/unreal.md index f3038be1cd..e143ca6067 100644 --- a/site/content/en/docs/Guides/Client SDKs/unreal.md +++ b/site/content/en/docs/Guides/Client SDKs/unreal.md @@ -123,48 +123,27 @@ PublicDependencyModuleNames.AddRange( "Agones", }); ``` -- Add component in header +- Add subsystem in header ```c++ -#include "AgonesComponent.h" - -UPROPERTY(EditAnywhere, BlueprintReadWrite) -UAgonesComponent* AgonesSDK; +#include "AgonesSubsystem.h" ``` -- Initialize component in GameMode -```c++ -#include "AgonesComponent.h" -#include "Classes.h" -ATestGameMode::ATestGameMode() -{ - AgonesSDK = CreateDefaultSubobject(TEXT("AgonesSDK")); -} -``` - -- Use the Agones component to call PlayerReady +- Use the Agones subsystem to call PlayerReady ```c++ void APlatformGameSession::PostLogin(APlayerController* NewPlayer) { - // Empty brances are for callbacks on success and errror. - AgonesSDK->PlayerConnect("netspeak-player", {}, {}); + UAgonesSubsystem* AgonesSDK = UAgonesSubsystem::Get(this); + if (AgonesSDK) // Check for nullptr is a must. + { + // Empty brances are for callbacks on success and errror. + AgonesSDK->PlayerConnect("netspeak-player", {}, {}); + } } ``` -#### Using Blueprints (UE5) -- Add Component to your Blueprint GameMode -![component](../../../../images/unreal5_bp_component.png) -- This will automatically call `/health` every 10 seconds and once `/gameserver` calls are succesful it will call `/ready`. - -- Accessing other functionality of Agones can be done via adding a node in Blueprints. -![actions](../../../../images/unreal5_bp_actions.png) - -#### Using Blueprints (UE4) -- Add Component to your Blueprint GameMode -![component](../../../../images/unreal_bp_component.png) -- This will automatically call `/health` every 10 seconds and once `/gameserver` calls are succesful it will call `/ready`. - -- Accessing other functionality of Agones can be done via adding a node in Blueprints. -![actions](../../../../images/unreal_bp_actions.png) +#### Using Blueprints +- Accessing Agones functionality can be done via Blueprints. +![usage](../../../../images/unreal_bp_usage.png) ## Configuration Options @@ -174,6 +153,7 @@ A number of options can be altered via config files in Unreal these are supplied [/Script/Agones.AgonesComponent] HttpPort=1337 HealthRateSeconds=5.0 +bDisableAutoHealthPing=false bDisableAutoConnect=true ``` diff --git a/site/static/images/unreal5_bp_actions.png b/site/static/images/unreal5_bp_actions.png deleted file mode 100644 index acddecbefa..0000000000 Binary files a/site/static/images/unreal5_bp_actions.png and /dev/null differ diff --git a/site/static/images/unreal5_bp_component.png b/site/static/images/unreal5_bp_component.png deleted file mode 100644 index 13fa522441..0000000000 Binary files a/site/static/images/unreal5_bp_component.png and /dev/null differ diff --git a/site/static/images/unreal_bp_actions.png b/site/static/images/unreal_bp_actions.png deleted file mode 100644 index 8330db949e..0000000000 Binary files a/site/static/images/unreal_bp_actions.png and /dev/null differ diff --git a/site/static/images/unreal_bp_component.png b/site/static/images/unreal_bp_component.png deleted file mode 100644 index c733c62a5e..0000000000 Binary files a/site/static/images/unreal_bp_component.png and /dev/null differ diff --git a/site/static/images/unreal_bp_usage.png b/site/static/images/unreal_bp_usage.png new file mode 100644 index 0000000000..c800ac7b1b Binary files /dev/null and b/site/static/images/unreal_bp_usage.png differ