SDK для Unreal Engine 5 (C++ / Blueprints)
UGameInstanceSubsystem с готовыми Blueprint-нодами: подпишитесь на OnAuthCompleted, OnAchievementUnlocked и OnCloudSaveSynced прямо в графе Blueprint — вызовы HTTP полностью асинхронны.
Установка
- Добавьте файлы ниже в
Source/[GameName]/Public/BlueStudz/иSource/[GameName]/Private/BlueStudz/. - В
[GameName].Build.csдобавьте зависимость"HTTP", "Json", "JsonUtilities". - В Blueprint:
Get Game Instance Subsystem→ классUCPP_BS_SDKSubsystem→ заполнитеSecret Tokenи вызовитеInit.
Справочник методов UGameInstanceSubsystem
Исходный код подсистемы (C++)
BS_SDKSubsystem.h
// BS_SDKSubsystem.h - place under Source/[GameName]/Public/BlueStudz/
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Interfaces/IHttpRequest.h"
#include "BS_SDKSubsystem.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnBSAuthCompleted, bool, bSuccess);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnBSAchievementUnlocked, bool, bSuccess);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnBSCloudSaveSynced, bool, bSuccess, bool, bConflict);
/**
* BlueStudz SDK - Game Instance Subsystem.
* Access from any Blueprint via "Get Game Instance Subsystem" -> UCPP_BS_SDKSubsystem.
* Reads BLUESTUDZ_API / BLUESTUDZ_GAME_ID / BLUESTUDZ_LAUNCH_TOKEN from the
* environment (set automatically by the BlueStudz Launcher).
*/
UCLASS()
class UCPP_BS_SDKSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
UPROPERTY(BlueprintAssignable, Category = "BlueStudz")
FOnBSAuthCompleted OnAuthCompleted;
UPROPERTY(BlueprintAssignable, Category = "BlueStudz")
FOnBSAchievementUnlocked OnAchievementUnlocked;
UPROPERTY(BlueprintAssignable, Category = "BlueStudz")
FOnBSCloudSaveSynced OnCloudSaveSynced;
/** Secret token from your BlueStudz developer dashboard (SDK tab). Set in Project Settings, never ship in a public repo. */
UPROPERTY(EditDefaultsOnly, Category = "BlueStudz")
FString SecretToken;
UFUNCTION(BlueprintCallable, Category = "BlueStudz")
void Init();
UFUNCTION(BlueprintCallable, Category = "BlueStudz")
void UnlockAchievement(const FString& AchievementId);
UFUNCTION(BlueprintCallable, Category = "BlueStudz")
void SyncCloudSave(const FString& Sha256, int64 SizeBytes, const FString& StorageKey);
UFUNCTION(BlueprintCallable, Category = "BlueStudz")
void SetPresenceStatus(bool bActive);
UFUNCTION(BlueprintCallable, Category = "BlueStudz")
void SyncBattlePassProgress(int32 Xp, int32 Tier, bool bHasPremium);
UFUNCTION(BlueprintPure, Category = "BlueStudz")
bool IsAuthenticated() const { return bAuthenticated; }
private:
FString ApiBase;
FString GameId;
FString UserToken;
FString UserId;
bool bAuthenticated = false;
void PostJson(const FString& Path, const TSharedRef<class FJsonObject>& Body, bool bWithAuth, bool bWithSecret,
TFunction<void(bool bOk, TSharedPtr<class FJsonObject> Response)> OnComplete);
};
BS_SDKSubsystem.cpp
// BS_SDKSubsystem.cpp
#include "BlueStudz/BS_SDKSubsystem.h"
#include "HttpModule.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Misc/Base64.h"
void UCPP_BS_SDKSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
ApiBase = FPlatformMisc::GetEnvironmentVariable(TEXT("BLUESTUDZ_API"));
if (ApiBase.IsEmpty()) ApiBase = TEXT("https://api.bluestudz.ru");
GameId = FPlatformMisc::GetEnvironmentVariable(TEXT("BLUESTUDZ_GAME_ID"));
}
void UCPP_BS_SDKSubsystem::Init()
{
const FString LaunchToken = FPlatformMisc::GetEnvironmentVariable(TEXT("BLUESTUDZ_LAUNCH_TOKEN"));
if (GameId.IsEmpty() || LaunchToken.IsEmpty())
{
UE_LOG(LogTemp, Warning, TEXT("[BlueStudzSDK] Not launched via BlueStudz Launcher - running unauthenticated."));
OnAuthCompleted.Broadcast(false);
return;
}
TSharedRef<FJsonObject> VerifyBody = MakeShared<FJsonObject>();
VerifyBody->SetStringField(TEXT("launchToken"), LaunchToken);
PostJson(TEXT("/auth/game/verify"), VerifyBody, false, false,
[this](bool bOk, TSharedPtr<FJsonObject> Response)
{
if (!bOk || !Response.IsValid()) { OnAuthCompleted.Broadcast(false); return; }
UserToken = Response->GetStringField(TEXT("gameToken"));
UserId = Response->GetObjectField(TEXT("user"))->GetStringField(TEXT("id"));
TSharedRef<FJsonObject> AuthBody = MakeShared<FJsonObject>();
AuthBody->SetStringField(TEXT("gameId"), GameId);
AuthBody->SetStringField(TEXT("secretToken"), SecretToken);
AuthBody->SetStringField(TEXT("userId"), UserId);
AuthBody->SetStringField(TEXT("userToken"), UserToken);
PostJson(TEXT("/sdk/auth/verify"), AuthBody, false, false,
[this](bool bAuthOk, TSharedPtr<FJsonObject> AuthResponse)
{
bAuthenticated = bAuthOk && AuthResponse.IsValid() && AuthResponse->GetBoolField(TEXT("success"));
OnAuthCompleted.Broadcast(bAuthenticated);
});
});
}
void UCPP_BS_SDKSubsystem::UnlockAchievement(const FString& AchievementId)
{
TSharedRef<FJsonObject> Body = MakeShared<FJsonObject>();
Body->SetStringField(TEXT("achievementId"), AchievementId);
PostJson(TEXT("/sdk/achievements/unlock"), Body, true, true,
[this](bool bOk, TSharedPtr<FJsonObject> Response)
{
OnAchievementUnlocked.Broadcast(bOk && Response.IsValid() && Response->GetBoolField(TEXT("success")));
});
}
void UCPP_BS_SDKSubsystem::SyncCloudSave(const FString& Sha256, int64 SizeBytes, const FString& StorageKey)
{
// Upload the save archive to the presigned URL from POST /storage/presign
// separately - this call only registers the resulting file for BlueStudz's
// cross-device conflict detection.
TSharedRef<FJsonObject> Body = MakeShared<FJsonObject>();
Body->SetStringField(TEXT("sha256"), Sha256);
Body->SetNumberField(TEXT("sizeBytes"), (double)SizeBytes);
Body->SetStringField(TEXT("storageKey"), StorageKey);
Body->SetField(TEXT("baseVersion"), MakeShared<FJsonValueNull>());
PostJson(TEXT("/sdk/cloudsaves/sync"), Body, true, false,
[this](bool bOk, TSharedPtr<FJsonObject> Response)
{
const bool bConflict = Response.IsValid() && Response->GetBoolField(TEXT("conflict"));
OnCloudSaveSynced.Broadcast(bOk, bConflict);
});
}
void UCPP_BS_SDKSubsystem::SetPresenceStatus(bool bActive)
{
TSharedRef<FJsonObject> Body = MakeShared<FJsonObject>();
Body->SetBoolField(TEXT("active"), bActive);
PostJson(TEXT("/sdk/presence/status"), Body, true, false, nullptr);
}
void UCPP_BS_SDKSubsystem::SyncBattlePassProgress(int32 Xp, int32 Tier, bool bHasPremium)
{
TSharedRef<FJsonObject> Body = MakeShared<FJsonObject>();
Body->SetNumberField(TEXT("xp"), Xp);
Body->SetNumberField(TEXT("tier"), Tier);
Body->SetBoolField(TEXT("hasPremium"), bHasPremium);
Body->SetArrayField(TEXT("claimedTiers"), TArray<TSharedPtr<FJsonValue>>());
PostJson(TEXT("/sdk/battlepass/sync"), Body, true, true, nullptr);
}
void UCPP_BS_SDKSubsystem::PostJson(const FString& Path, const TSharedRef<FJsonObject>& Body, bool bWithAuth,
bool bWithSecret, TFunction<void(bool, TSharedPtr<FJsonObject>)> OnComplete)
{
FString BodyString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&BodyString);
FJsonSerializer::Serialize(Body, Writer);
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
Request->SetURL(ApiBase + Path);
Request->SetVerb(TEXT("POST"));
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
if (bWithAuth)
{
Request->SetHeader(TEXT("Authorization"), TEXT("Bearer ") + UserToken);
Request->SetHeader(TEXT("x-bs-gameid"), GameId);
}
if (bWithSecret) Request->SetHeader(TEXT("x-bs-secrettoken"), SecretToken);
Request->SetContentAsString(BodyString);
Request->OnProcessRequestComplete().BindLambda(
[OnComplete](FHttpRequestPtr, FHttpResponsePtr Response, bool bConnectedSuccessfully)
{
if (!OnComplete) return;
const bool bOk = bConnectedSuccessfully && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode());
TSharedPtr<FJsonObject> Parsed;
if (Response.IsValid())
{
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
FJsonSerializer::Deserialize(Reader, Parsed);
}
OnComplete(bOk, Parsed);
});
Request->ProcessRequest();
}
