Перейти к содержимому

SDK для Unreal Engine 5 (C++ / Blueprints)

UGameInstanceSubsystem с готовыми Blueprint-нодами: подпишитесь на OnAuthCompleted, OnAchievementUnlocked и OnCloudSaveSynced прямо в графе Blueprint — вызовы HTTP полностью асинхронны.

Установка

  1. Добавьте файлы ниже в Source/[GameName]/Public/BlueStudz/ и Source/[GameName]/Private/BlueStudz/.
  2. В [GameName].Build.cs добавьте зависимость "HTTP", "Json", "JsonUtilities".
  3. В Blueprint: Get Game Instance Subsystem → класс UCPP_BS_SDKSubsystem → заполните Secret Token и вызовите Init.

Справочник методов UGameInstanceSubsystem

МетодПараметрыВозвращаетОписание
Init(AppId, SecretToken)FString AppId, FString SecretTokenvoidАсинхронная инициализация. По завершении вызывает делегат OnAuthCompleted.
UnlockAchievement(Id)FString AchievementIdvoidРазблокировка достижения. Вызывает делегат OnAchievementUnlocked.
SyncCloudSave(...)FString Hash, int64 Size, FString Key, int32 BaseVervoidРегистрация облачного сохранения. Вызывает делегат OnCloudSaveSynced.
SetPresenceStatus(Active)bool bActivevoidОбновление Rich Presence игрока для друзей в лаунчере.

Исходный код подсистемы (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();
}