Epic Online Services サンプルの AuthAndFriends のコードリーディング ②

前回SDLMain.cppmain 関数の SDL2 を軸にした簡単な流れだけ見ました。

int main(int Argc, char* Args[])
{
    // Ignore first param (executable path)
    std::vector<std::wstring> CmdLineParams;
    for (int i = 1; i < Argc; ++i)
    {
        CmdLineParams.push_back(FStringUtils::Widen(Args[i]));
    }
    FCommandLine::Get().Init(CmdLineParams);

    FSettings::Get().Init();

    Main = std::make_unique<FMain>();

    Main->InitPlatform();

上記は `main のイベントループに入る前のコードです。

コマンドライン引数を FCommandLine に格納、設定ファイルの指定があれば FSettings に格納しています。
どちらもシングルトンの実装がされていて Getインスタンスを取得できます。

参考: シングルトンのベターな実装方法 - Qiita

FMain

これが本体です。

コンストラクタ / デストラク

DirectX の部分を除けば以下のように小さなコードです。

FMain::FMain() noexcept(false):
    bIsFullScreen(false)
{
    FDebugLog::Init();
    FDebugLog::AddTarget(FDebugLog::ELogTarget::DebugOutput);
    FDebugLog::AddTarget(FDebugLog::ELogTarget::Console);
    FDebugLog::AddTarget(FDebugLog::ELogTarget::File);
}

FMain::~FMain()
{
    FDebugLog::Close();

    Game = nullptr;
}

bIsFullScreen, Game のメンバ変数定義は以下です。

public:
    /** True if main window is currently in fullscreen mode */
    bool bIsFullScreen;
private:
    std::unique_ptr<FGame> Game;

InitPlatform

以下のコードは全体ですがログ出力は省いています。

void FMain::InitPlatform()
{
    // Init EOS SDK
    EOS_InitializeOptions SDKOptions = {};
    SDKOptions.ApiVersion = EOS_INITIALIZE_API_LATEST;
    SDKOptions.AllocateMemoryFunction = nullptr;
    SDKOptions.ReallocateMemoryFunction = nullptr;
    SDKOptions.ReleaseMemoryFunction = nullptr;
    SDKOptions.ProductName = SampleConstants::GameName;
    SDKOptions.ProductVersion = "1.0";
    SDKOptions.Reserved = nullptr;
    SDKOptions.SystemInitializeOptions = nullptr;
    SDKOptions.OverrideThreadAffinity = nullptr;

    EOS_EResult InitResult = EOS_Initialize(&SDKOptions);
    if (InitResult != EOS_EResult::EOS_Success) return;

    EOS_EResult SetLogCallbackResult = EOS_Logging_SetCallback(&EOSSDKLoggingCallback);

    const bool bCreateSuccess = FPlatform::Create();
}

EOS_Initialize に関しては以下の説明がありますので、 EOS_Shutdown もここで追っておきます。

This function must only be called one time and must have a corresponding EOS_Shutdown call

EOS_Shutdown が呼ばれる箇所

main 関数において、メインループ終了後に Main->OnShutdown(); が呼び出されます。この内部でさらに Game->OnShutdown(); が呼び出されます。
Game のクラスである FGame の親クラスの FBaseGame::OnShutdown に処理が移譲されており、そこで Authentication->Shutdown(); が呼び出されます。
AuthenticationFAuthenticationインスタンスであり、その Shutdown メソッド内で EOS_Shutdown() が呼び出されています。

void FAuthentication::Shutdown()
{
    RemoveNotifyLoginStatusChanged();

    RemoveConnectAuthExpirationNotification();

    if (FPlatform::GetPlatformHandle())
    {
        EOS_Platform_Release(FPlatform::GetPlatformHandle());
    }

    EOS_EResult ShutdownResult = EOS_Shutdown();
    if (ShutdownResult != EOS_EResult::EOS_Success) // ログ

    FPlatform::Release();
}

ここの最後で呼び出されている FPlatformFMain::InitPlatform メソッド内でも FPlatform::Create が呼び出されており、シャットダウン処理はこの FAuthentication::Shutdown が担っているようです。

FPlatform

以下のコメントが書かれているため、EOS の Platform 関連にアクセスするようです。
Platform インターフェース | Epic Online Services ドキュメンテーション

/**
* Creates EOS SDK Platform
*/
class FPlatform
bool FPlatform::Create()
{
    bIsInit = false;

    // Create platform instance
    EOS_Platform_Options PlatformOptions = {};
    PlatformOptions.ApiVersion = EOS_PLATFORM_OPTIONS_API_LATEST;
    PlatformOptions.bIsServer = FCommandLine::Get().HasFlagParam(CommandLineConstants::Server);
    PlatformOptions.EncryptionKey = SampleConstants::EncryptionKey;
    PlatformOptions.OverrideCountryCode = nullptr;
    PlatformOptions.OverrideLocaleCode = nullptr;
    PlatformOptions.Flags = EOS_PF_WINDOWS_ENABLE_OVERLAY_D3D9 | EOS_PF_WINDOWS_ENABLE_OVERLAY_D3D10 | EOS_PF_WINDOWS_ENABLE_OVERLAY_OPENGL; // Enable overlay support for D3D9/10 and OpenGL. This sample uses D3D11 or SDL.
    PlatformOptions.CacheDirectory = FUtils::GetTempDirectory();

    // コマンドラインパラメータから ProductId を取得し設定
    // コマンドラインパラメータから SandboxId を取得し設定
    // コマンドラインパラメータから DeploymentId を取得し設定
    // コマンドラインパラメータから ClientId を取得し設定
    // コマンドラインパラメータから ClientSecret を取得し設定

    // (未設定など)不正な値があれば失敗
    if (bHasInvalidParamProductId ||
        bHasInvalidParamSandboxId ||
        bHasInvalidParamDeploymentId ||
        bHasInvalidParamClientCreds)
    {
        return false;
    }

    EOS_Platform_RTCOptions RtcOptions = { 0 };
    RtcOptions.ApiVersion = EOS_PLATFORM_RTCOPTIONS_API_LATEST;

    // 以下は Windows で SDL を利用している場合
    std::string XAudio29DllPath = SDL_GetBasePath();
    XAudio29DllPath.append("/xaudio2_9redist.dll");

    EOS_Windows_RTCOptions WindowsRtcOptions = { 0 };
    WindowsRtcOptions.ApiVersion = EOS_WINDOWS_RTCOPTIONS_API_LATEST;
    WindowsRtcOptions.XAudio29DllPath = XAudio29DllPath.c_str();
    RtcOptions.PlatformSpecificOptions = &WindowsRtcOptions;

    PlatformOptions.RTCOptions = &RtcOptions;

    PlatformHandle = EOS_Platform_Create(&PlatformOptions);

    if (PlatformHandle == nullptr) return true;

    bIsInit = true;

    return true;

期待通り EOS の Platform のインスタンスを作成し保持していることが確認できます。

EOS_Windows_RTCOptions に関してはドキュメントを見つけられませんでしたが、SDK にヘッダファイルは含まれていました。 EOS_Platform_RTCOptions::PlatformSpecificOptions に設定するものなので、Windows 用の設定ということなのだと思います。

ここでいう RTC はドキュメントから明示的になにかわからなかったのですが、WebRTC のことなのでしょうか。

EOS_Platform_Create のドキュメントには、これが single instance であることと、EOS_Platform_Release に渡して開放することが記載されています。
すでに FAuthentication::Shutdown() の中で EOS_Platform_Release(FPlatform::GetPlatformHandle()); が呼び出されていることを確認できています。

ついでに FPlatform::Release も見ておきます。

void FPlatform::Release()
{
    bIsInit = false;
    PlatformHandle = nullptr;
    bIsShuttingDown = true;
}

init 関数 (SDLMain.cpp)

続いてイベントループの直前の init 関数を見ます。

int main(int Argc, char* Args[]) {
    // Main->InitPlatform(); まで

    //Start up SDL and create window
    if (!Init())
    {
        printf("Failed to initialize!\n");
    }
    else
    { // ここからイベントループ

SDL2 の初期設定のコードが多いので、その辺は省略し、FMain クラスに関わる部分を中心にみていきます。

bool Init()
{
    // SDL_Init, SDL_GL_SetAttribute の設定

    int Width, Height;
    Main->GetDefaultSize(Width, Height);

    Main->InitCommandLine();

    SDL_Window* Window = nullptr;

    //Create window
    if (Main->bIsFullScreen) // Window = SDL_CreateWindow が分岐でそれぞれ実行

    //Create context
    SDL_GLContext GLContext = SDL_GL_CreateContext(Window);

    //Initialize GLEW
    glewExperimental = GL_TRUE;
    GLenum glewError = glewInit();

    //Vsync settings
    //First try to use adaptive VSync
    if (SDL_GL_SetSwapInterval(-1) < 0) // Vsync の設定を何度か行う

    //init True Type Fonts lib
    if (TTF_Init() != 0)

    if (!Main->bIsFullScreen)
    {
        int MinWidth = 1024;
        int MinHeight = 768;
        Main->GetMinimumSize(MinWidth, MinHeight);
        SDL_SetWindowMinimumSize(Window, MinWidth, MinHeight);
    }

    SDL_GetWindowSize(Window, &Width, &Height);

    //Initialize OpenGL
    if (!InitGraphics(Window, Width, Height)) return false;

    Main->Initialize(Window, GLContext, Width, Height);

    return true;
}

FMain::InitCommandLine

void FMain::InitCommandLine()
{
    bIsFullScreen = HasFullScreenCommandLine();
}

bool FMain::HasFullScreenCommandLine()
{
    return FCommandLine::Get().HasFlagParam(CommandLineConstants::Fullscreen);
}

コマンドライン引数から fullscreen を取得しているだけです。

FMain::Initialize

void FMain::Initialize(SDL_Window* Window, SDL_GLContext InGLContext, int Width, int Height)
{
    SDLWindow = Window;
    GLContext = InGLContext;

    Game = std::make_unique<FGame>();

    CreateDeviceDependentResources();
    CreateWindowSizeDependentResources();

    Game->UpdateLayout(Width, Height);
    Game->Init();
}

void FMain::CreateDeviceDependentResources()
{
    if (Game)
        Game->Create();
}

void FMain::CreateWindowSizeDependentResources()
{
    Vector2 Size;

    int Width = 0, Height = 0;
    SDL_GetWindowSize(SDLWindow, &Width, &Height);
    Size = Vector2(float(Width), float(Height));
    float aspectRatio = float(Size.x) / float(Size.y);
}

Window サイズに関する処理もしていますが、 Game インスタンスの作成・保持や初期化を行っています。

続いて Game のクラスである FGame を見ていきます。

FGame

コンストラクタ / デストラク

FGame::FGame() noexcept(false) :
    FBaseGame()
{
    Menu = std::make_shared<FMenu>(Console);
    Level = std::make_unique<FLevel>();
    CustomInvites = std::make_unique<FCustomInvites>();

    CreateConsoleCommands();
}

FGame::~FGame()
{
}

void FGame::CreateConsoleCommands()
{
    FBaseGame::CreateConsoleCommands();
    if (Console)
    {
        const std::vector<const wchar_t*> ExtraHelpMessageLines =
        {
            L" SIMULATECUSTOMINVITE - simulates an incoming custom invite, debug testing only",
        };
        AppendHelpMessageLines(ExtraHelpMessageLines);
    }
}

FGameFMenu, FLevel, FCustomInvites の管理をしているようです。
さらに ConsoleFConsoleshared_ptr のため、 FConsole の管理もしているようです。

続いて、親クラスの FBaseGame です。

FBaseGame のコンストラクタ / デストラク

FBaseGame::FBaseGame() noexcept(false):
    TheImpl(std::make_unique<FBaseGame::Impl>(this))
{
    Input = std::make_unique<FInput>();
    Console = std::make_shared<FConsole>();
    Users = std::make_unique<FUsers>();
    TextureManager = std::make_unique<FTextureManager>();
    Authentication = std::make_shared<FAuthentication>();
    Friends = std::make_unique<FFriends>();
    Metrics = std::make_unique<FMetrics>();
    PlayerManager = std::make_unique<FPlayerManager>();
    VectorRender = std::make_unique<FVectorRender>();
    EosUI = std::make_unique<FEosUI>();
}

FBaseGame::~FBaseGame()
{
}

親クラスもまとめて見ると、FGame は、ほとんどを管理しています。そりゃそうですよね😅

メンバ初期化で指定されている TheImplFBaseGame::Impl というクラスになっておりコメントを見るに singleton として動くようです。

FBaseGame::Create

Game->Create() も実態としては FBaseGame の方になっています。

void FBaseGame::Create()
{
    TheImpl->UISpriteBatch = std::make_unique<FSDLSpriteBatch>();

    Menu->CreateFonts();
    Menu->Create();
    Level->Create();
    VectorRender->Create();
}

構成要素の Create が順次呼び出されているようです。

UpdateLayout / Init

Game->UpdateLayout(), Game->Init() も同様に FBaseGame の方にあります。

void FBaseGame::UpdateLayout(int Width, int Height)
{
    if (VectorRender)
    {
        //recreate vector render
        bool bIsEnabled = VectorRender->IsDebugRenderEnabled();
        VectorRender = std::make_unique<FVectorRender>();
        VectorRender->Create();
        VectorRender->SetDebugRenderEnabled(bIsEnabled);
    }

    if (Menu)
    {
        Menu->UpdateLayout(Width, Height);
    }
}

void FBaseGame::Init()
{
    Metrics->Init();
    Menu->Init();
    EosUI->Init();

    FGameEvent Event(EGameEventType::CheckAutoLogin);
    OnGameEvent(Event);
}

void FBaseGame::OnGameEvent(const FGameEvent& Event)
{
    PlayerManager->OnGameEvent(Event);
    Friends->OnGameEvent(Event);
    Menu->OnGameEvent(Event);
    Authentication->OnGameEvent(Event);
    Metrics->OnGameEvent(Event);
    Level->OnGameEvent(Event);
    EosUI->OnGameEvent(Event);
}

VectorRenderFBaseGame のコンストラクタで生成されていましたが、コメント通りですが recreate されているようです。

そして、FGameEvent が出てきました。まだ内容はわかりませんがイベントループに関連する部分になりそうです。

まとめ

今回は main 関数の中の、イベントループに入る直前までの設定や初期化の部分を中心にみていきました。

SDL2 と DirectX に別れた main から、実際の main を司る FMain, EOS Platform インターフェースを wrap するFPlatform、そしておそらくこのサンプルゲームの中心になる FGame, FBaseGame を順に追っていき、FGame がゲームの UI やイベントを管理しているであろうところまでいきました。

次回はイベントループに入っていきたいと思います。