OnlineSubsystemEOS の動作確認: EOS_Platform_Tick の確認

前回 で動作確認用のプロジェクトのセットアップをしました。

まずは SDK で確認したような認証部分から見ていきたいと思います。

Epic Online Services サンプルの AuthAndFriends のコードリーディング ⑤ - You are done!

上記で確認した内容のうち、今回は EOS_Platform_Tick について見ていきたいと思います。
というのも、これが実行されていないと EOS へのリクエストの Callback が実行されないので、これがどこで実行されているか確認しておきたいためです。

EOS_Platform_Tick の呼び出し

EOSShared Plugin の EOSSDKManager.cpp で呼び出されています。

bool FEOSSDKManager::Tick(float)
{
    //LLM_SCOPE(ELLMTag::EOSSDK); // TODO
    for (EOS_HPlatform PlatformHandle : PlatformHandles)
    {
        QUICK_SCOPE_CYCLE_COUNTER(FEOSSDKManager_Tick);
        EOS_Platform_Tick(PlatformHandle);
    }

    return true;
}

void FEOSPlatformHandle::Tick()
{
    QUICK_SCOPE_CYCLE_COUNTER(FEOSPlatformHandle_Tick);
    EOS_Platform_Tick(PlatformHandle);
}

QUICK_SCOPE_CYCLE_COUNTER に関しては以下を参照。
統計システムの概要 | Unreal Engine 4.26 ドキュメント

後者の単独の PlatformHandle を呼び出している方は、 VoiceChat の方から呼び出されているので、今回は省略します。前者の FEOSSDKManager::Tick を中心に見ていきます。

FEOSSDKManager::Tick の呼び出し

同じファイルの以下のメソッドから呼び出されています。

IEOSPlatformHandlePtr FEOSSDKManager::CreatePlatform(const EOS_Platform_Options& PlatformOptions)
{
    IEOSPlatformHandlePtr SharedPlatform;

    if (IsInitialized())
    {
        const EOS_HPlatform PlatformHandle = EOS_Platform_Create(&PlatformOptions);
        if (PlatformHandle)
        {
            PlatformHandles.Emplace(PlatformHandle);
            SharedPlatform = MakeShared<FEOSPlatformHandle, ESPMode::ThreadSafe>(*this, PlatformHandle);

            if (!TickerHandle.IsValid())
            {
                TickerHandle = FTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FEOSSDKManager::Tick), 0.0f);
            }
        }
        else
        {
            UE_LOG(LogEOSSDK, Warning, TEXT("FEOSSDKManager::CreatePlatform failed. EosPlatformHandle=nullptr"));
        }
    }
    else
    {
        UE_LOG(LogEOSSDK, Warning, TEXT("FEOSSDKManager::CreatePlatform failed. SDK not initialized"));
    }

    return SharedPlatform;
}

EOS_Platform_Create

Epic Online Services サンプルの AuthAndFriends のコードリーディング ② - You are done!

上記でも触れていますが、SDK の Sample でも EOS_Platform_Create を呼び出しています。
加えて SDK の Sample では、main ループの中で EOS_Platform_Tick も呼び出されています。

では、Unreal Engine としては、どうやってループ処理に EOS_Platform_Tick が組み込まれているのでしょうか。

FTicker

FTicker に今回の処理が追加されています。FTicker については以下のスライドの 24 ページ目に説明があります。
処理の起点となっている FEngineLoop::Tick に紐付いているようです。つまり、EOS_Platform_Tick が期待通り呼び出されていることが確認できました。

UE4プログラマー勉強会 in 大阪 -エンジンの内部挙動について | PPT

ここまでで、一旦今回の目的は果たせたのですが、ついでに OnlineSubsytemEOS Plugin の初期化に関して掴んでおきたいと思います。

FEOSSDKManager::CreatePlatform 以降の呼び出し階層

- FEOSSDKManager::CreatePlatform
  - FEOSHelpers::CreatePlatform
    - FOnlineSubsystemEOS::PlatformCreate
      - FOnlineSubsystemEOS::Init
        - FOnlineFactoryEOS::CreateSubsystem
---------------- 以降は OnlineSubsystem Plugin ------
          - FOnlineSubsystemModule::GetOnlineSubsystem

以降は気になる部分だけ見ていきます。

FOnlineSubsystemEOS::PlatformCreate

/** Common method for creating the EOS platform */
bool FOnlineSubsystemEOS::PlatformCreate()
{
    FString ArtifactName;
    FParse::Value(FCommandLine::Get(), TEXT("EpicApp="), ArtifactName);
    // Find the settings for this artifact
    FEOSArtifactSettings ArtifactSettings;
    if (!UEOSSettings::GetSettingsForArtifact(ArtifactName, ArtifactSettings))
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS::PlatformCreate() failed to find artifact settings object for artifact (%s)"), *ArtifactName);
        return false;
    }

    // Create platform instance
    FEOSPlatformOptions PlatformOptions;
    FCStringAnsi::Strncpy(PlatformOptions.ClientIdAnsi, TCHAR_TO_UTF8(*ArtifactSettings.ClientId), EOS_OSS_STRING_BUFFER_LENGTH);
    FCStringAnsi::Strncpy(PlatformOptions.ClientSecretAnsi, TCHAR_TO_UTF8(*ArtifactSettings.ClientSecret), EOS_OSS_STRING_BUFFER_LENGTH);
    FCStringAnsi::Strncpy(PlatformOptions.ProductIdAnsi, TCHAR_TO_UTF8(*ArtifactSettings.ProductId), EOS_OSS_STRING_BUFFER_LENGTH);
    FCStringAnsi::Strncpy(PlatformOptions.SandboxIdAnsi, TCHAR_TO_UTF8(*ArtifactSettings.SandboxId), EOS_OSS_STRING_BUFFER_LENGTH);
    FCStringAnsi::Strncpy(PlatformOptions.DeploymentIdAnsi, TCHAR_TO_UTF8(*ArtifactSettings.DeploymentId), EOS_OSS_STRING_BUFFER_LENGTH);
    PlatformOptions.bIsServer = IsRunningDedicatedServer() ? EOS_TRUE : EOS_FALSE;
    PlatformOptions.Reserved = nullptr;
    FEOSSettings EOSSettings = UEOSSettings::GetSettings();
    uint64 OverlayFlags = 0;
    if (!EOSSettings.bEnableOverlay)
    {
        OverlayFlags |= EOS_PF_DISABLE_OVERLAY;
    }
    if (!EOSSettings.bEnableSocialOverlay)
    {
        OverlayFlags |= EOS_PF_DISABLE_SOCIAL_OVERLAY;
    }
    PlatformOptions.Flags = IsRunningGame() ? OverlayFlags : EOS_PF_DISABLE_OVERLAY;
    // Make the cache directory be in the user's writable area

    const FString CacheDir = EOSHelpersPtr->PlatformCreateCacheDir(ArtifactName, EOSSettings.CacheDir);
    FCStringAnsi::Strncpy(PlatformOptions.CacheDirectoryAnsi, TCHAR_TO_UTF8(*CacheDir), EOS_OSS_STRING_BUFFER_LENGTH);
    FCStringAnsi::Strncpy(PlatformOptions.EncryptionKeyAnsi, TCHAR_TO_UTF8(*ArtifactSettings.EncryptionKey), EOS_ENCRYPTION_KEY_MAX_BUFFER_LEN);

#if WITH_EOS_RTC
    EOS_Platform_RTCOptions RtcOptions = { 0 };
    RtcOptions.ApiVersion = EOS_PLATFORM_RTCOPTIONS_API_LATEST;
    RtcOptions.PlatformSpecificOptions = nullptr;
    PlatformOptions.RTCOptions = &RtcOptions;
#endif

    EOSPlatformHandle = EOSHelpersPtr->CreatePlatform(PlatformOptions);
    if (EOSPlatformHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS::PlatformCreate() failed to init EOS platform"));
        return false;
    }
    
    return true;
}

UEOSSettings::GetSettingsForArtifact の部分は以下なのですが、Project Settings の EOS Settings で Artifacts に設定した内容を取得しているのが分かります。

bool UEOSSettings::GetSettingsForArtifact(const FString& ArtifactName, FEOSArtifactSettings& OutSettings)
{
    return UEOSSettings::ManualGetSettingsForArtifact(ArtifactName, OutSettings);
}

bool UEOSSettings::ManualGetSettingsForArtifact(const FString& ArtifactName, FEOSArtifactSettings& OutSettings)
{
    static TOptional<FString> CachedDefaultArtifactName;
    static TOptional<TArray<FEOSArtifactSettings>> CachedArtifactSettings;

    if (!CachedDefaultArtifactName.IsSet())
    {
        CachedDefaultArtifactName.Emplace();

        GConfig->GetString(INI_SECTION, TEXT("DefaultArtifactName"), *CachedDefaultArtifactName, GEngineIni);
    }

    if (!CachedArtifactSettings.IsSet())
    {
        CachedArtifactSettings.Emplace();

        TArray<FString> Artifacts;
        GConfig->GetArray(INI_SECTION, TEXT("Artifacts"), Artifacts, GEngineIni);
        for (const FString& Line : Artifacts)
        {
            FEOSArtifactSettings Artifact;
            Artifact.ParseRawArrayEntry(Line);
            CachedArtifactSettings->Add(Artifact);
        }
    }
// 略

以降も設定値を取得し FEOSPlatformOptions を組み立て、CreatePlatform に渡しているのが分かります。

FOnlineSubsystemEOS::Init

bool FOnlineSubsystemEOS::Init()
{
    // Determine if we are the default and if we're the platform OSS
    FString DefaultOSS;
    GConfig->GetString(TEXT("OnlineSubsystem"), TEXT("DefaultPlatformService"), DefaultOSS, GEngineIni);
    FString PlatformOSS;
    GConfig->GetString(TEXT("OnlineSubsystem"), TEXT("NativePlatformService"), PlatformOSS, GEngineIni);
    bIsDefaultOSS = DefaultOSS == TEXT("EOS");
    bIsPlatformOSS = PlatformOSS == TEXT("EOS");

    // Check for being launched by EGS
    bWasLaunchedByEGS = FParse::Param(FCommandLine::Get(), TEXT("EpicPortal"));
    FEOSSettings EOSSettings = UEOSSettings::GetSettings();
    if (!IsRunningDedicatedServer() && IsRunningGame() && !bWasLaunchedByEGS && EOSSettings.bShouldEnforceBeingLaunchedByEGS)
    {
        FString ArtifactName;
        FParse::Value(FCommandLine::Get(), TEXT("EpicApp="), ArtifactName);
        UE_LOG_ONLINE(Warning, TEXT("FOnlineSubsystemEOS::Init() relaunching artifact (%s) via the store"), *ArtifactName);
        FPlatformProcess::LaunchURL(*FString::Printf(TEXT("com.epicgames.launcher://store/product/%s?action=launch&silent=true"), *ArtifactName), nullptr, nullptr);
        FPlatformMisc::RequestExit(false);
        return false;
    }

    EOSSDKManager = IEOSSDKManager::Get();
    if (!EOSSDKManager)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS::Init() failed to get EOSSDKManager interface"));
        return false;
    }

    if (!PlatformCreate())
    {
        return false;
    }

    // Get handles for later use
    AuthHandle = EOS_Platform_GetAuthInterface(*EOSPlatformHandle);
    if (AuthHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get auth handle"));
        return false;
    }
    UserInfoHandle = EOS_Platform_GetUserInfoInterface(*EOSPlatformHandle);
    if (UserInfoHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get user info handle"));
        return false;
    }
    UIHandle = EOS_Platform_GetUIInterface(*EOSPlatformHandle);
    if (UIHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get UI handle"));
        return false;
    }
    FriendsHandle = EOS_Platform_GetFriendsInterface(*EOSPlatformHandle);
    if (FriendsHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get friends handle"));
        return false;
    }
    PresenceHandle = EOS_Platform_GetPresenceInterface(*EOSPlatformHandle);
    if (PresenceHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get presence handle"));
        return false;
    }
    ConnectHandle = EOS_Platform_GetConnectInterface(*EOSPlatformHandle);
    if (ConnectHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get connect handle"));
        return false;
    }
    SessionsHandle = EOS_Platform_GetSessionsInterface(*EOSPlatformHandle);
    if (SessionsHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get sessions handle"));
        return false;
    }
    StatsHandle = EOS_Platform_GetStatsInterface(*EOSPlatformHandle);
    if (StatsHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get stats handle"));
        return false;
    }
    LeaderboardsHandle = EOS_Platform_GetLeaderboardsInterface(*EOSPlatformHandle);
    if (LeaderboardsHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get leaderboards handle"));
        return false;
    }
    MetricsHandle = EOS_Platform_GetMetricsInterface(*EOSPlatformHandle);
    if (MetricsHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get metrics handle"));
        return false;
    }
    AchievementsHandle = EOS_Platform_GetAchievementsInterface(*EOSPlatformHandle);
    if (AchievementsHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get achievements handle"));
        return false;
    }
    P2PHandle = EOS_Platform_GetP2PInterface(*EOSPlatformHandle);
    if (P2PHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get p2p handle"));
        return false;
    }
    // Disable ecom if not part of EGS
    if (bWasLaunchedByEGS)
    {
        EcomHandle = EOS_Platform_GetEcomInterface(*EOSPlatformHandle);
        if (EcomHandle == nullptr)
        {
            UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get ecom handle"));
            return false;
        }
        StoreInterfacePtr = MakeShareable(new FOnlineStoreEOS(this));
    }
    TitleStorageHandle = EOS_Platform_GetTitleStorageInterface(*EOSPlatformHandle);
    if (TitleStorageHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get title storage handle"));
        return false;
    }
    PlayerDataStorageHandle = EOS_Platform_GetPlayerDataStorageInterface(*EOSPlatformHandle);
    if (PlayerDataStorageHandle == nullptr)
    {
        UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get player data storage handle"));
        return false;
    }

    SocketSubsystem = MakeShareable(new FSocketSubsystemEOS(this));
    FString ErrorMessage;
    SocketSubsystem->Init(ErrorMessage);

    UserManager = MakeShareable(new FUserManagerEOS(this));
    SessionInterfacePtr = MakeShareable(new FOnlineSessionEOS(this));
    // Set the bucket id to use for all sessions based upon the name and version to avoid upgrade issues
    SessionInterfacePtr->Init(EOSSDKManager->GetProductName() + TEXT("_") + EOSSDKManager->GetProductVersion());
    StatsInterfacePtr = MakeShareable(new FOnlineStatsEOS(this));
    LeaderboardsInterfacePtr = MakeShareable(new FOnlineLeaderboardsEOS(this));
    AchievementsInterfacePtr = MakeShareable(new FOnlineAchievementsEOS(this));
    TitleFileInterfacePtr = MakeShareable(new FOnlineTitleFileEOS(this));
    UserCloudInterfacePtr = MakeShareable(new FOnlineUserCloudEOS(this));

    // We initialized ok so we can tick
    StartTicker();

    return true;
}

見ての通りですが、PlatformCreate 後に、取得した PlatformHandle を使って、利用する EOS のインターフェースの Handle を次々に取得しています。
このクラスがかなりの依存性を一手に引き受けているようです。

この FOnlineSubsystemEOS クラスが以下のように OnlineSubsystemEOS の主要な公開 API になっているので、使い勝手として集約されているのかもしれません。(この辺の設計のプラクティスはわかっていないです)

class ONLINESUBSYSTEMEOS_API FOnlineSubsystemEOS : 
    public FOnlineSubsystemImpl

余談ですが、継承している FOnlineSubsystemImpl は以下の定義になっており FTickerObjectBase も実装する形になっています。内部では FTicker::GetCoreTicker() が使われており、ExecuteNextTick 関数を通して Callback の実行を登録できるようになっています。

/**
 * FOnlineSubsystemImpl - common functionality to share across online platforms, not intended for direct use
 */
class ONLINESUBSYSTEM_API FOnlineSubsystemImpl 
    : public IOnlineSubsystem
    , public FTickerObjectBase

まとめ

今回は OnlineSubsystemEOS の中で EOS_Platform_Tick がどのように呼び出されているかを SDK の Sample と比較しつつ確認しました。
また、初期化部分を簡単に眺めることで、FOnlineSubsystemEOS クラスがプラグインAPI として多くの役割を持っていることが確認できました。

次回は認証部分が SDK の Sample と比較してどのように実装されているかを確認していきたいと思います。