Epic Online Services サンプルの Shared 部分と各プロジェクトの読み方の整理

AuthAndFriends サンプルをざっくり読んできましたが、他のプロジェクトも読むにあたり共有コード部分を個別のプロジェクトのコードに関して読み方を自分なりに整理しておきます。

各プロジェクトのファイル共有状況

以下は VS のソリューションエクスプローラーでのツリーなので、実態のファイルの場所とは異なります。
ここでは、 AuthAndFriendsP2PNAT の 2 つのサンプルを例にとって、Shared とそうでない部分の比較をしていきます。

Samples/
  + AuthAndFriends/
    + EOSSDK/
    + SharedAssets/
    + SharedSource/
      + Graphics/GUI/Dialogs/
        + AuthDialogs.h(cpp)
        + ConsoleDialog.h(cpp)
        + FriendsDialog.h(cpp)
        + ExitDialog.h(cpp)
        + PopupDialog.h(cpp)
      + Main/
        + SDL/SDLMain.h(cpp)
        + Main.h(cpp)
      + BaseGame.h(cpp)
      + BaseMenu.h(cpp)
    + Source/
      + Game.h(cpp)
      + Menu.h(cpp)
      + CustomInvitesDialog.h(cpp)
      + CustomInvites.h(cpp)
  + P2PNAT/
    + EOSSDK/
    + SharedAssets/
    + SharedSource/
      + Graphics/GUI/Dialogs/
        + AuthDialogs.h(cpp)
        + ConsoleDialog.h(cpp)
        + FriendsDialog.h(cpp)
        + ExitDialog.h(cpp)
        + PopupDialog.h(cpp)
      + Main/
        + SDL/SDLMain.h(cpp)
        + Main.h(cpp)
      + BaseGame.h(cpp)
      + BaseMenu.h(cpp)
    + Source/
      + Game.h(cpp)
      + Menu.h(cpp)
      + P2PNATDialog.h(cpp)
      + P2PNAT.h(cpp)

上記は (SDL2 を利用した場合の) 主要なファイルのみを記載したものです。

EOSSDK, SharedAssets, SharedSource はいずれも共有コードです。(ただ、AuthAndFriends には Steam に関するコードがあったりと全く同じファイルがある訳ではありません。ただ、同名のファイルがあれば内容は同じです)

共有部分には、ゲームの起動・終了・イベントループを司る main 部分に加えて FBaseGame, FBaseMenu などの基底クラスが存在し、各プロジェクトにそれぞれの FGame, FMenu が存在しています。

また、UI 要素としても全プロジェクトに存在しているものは SharedSource/Graphics/GUI/Dialogs に配置されています。
個別のプロジェクトでのみ利用する CustomInvitesDialog, P2PNATDialog は各プロジェクトの Source フォルダに配置されています。

共有ファイルと個別ファイルの読み進め方

初期化部分

SDLMain.cppInit 関数が初期化の起点になり、そこから各プロジェクトに関する呼び出しの重要な部分は 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 FBaseGame::Create()
{
    Menu->CreateFonts();
    Menu->Create();
    Level->Create();
}

void FBaseGame::UpdateLayout(int Width, int Height)
{
    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);
}
void FBaseMenu::Create()
{
    BackgroundImage->Create();

    TitleLabel->Create();
    TitleLabel->SetFont(BoldLargeFont->GetFont());

    CreateConsoleDialog();
    CreateAuthDialogs();
    CreateExitDialog();
    CreatePopupDialog();
}

void FBaseMenu::UpdateLayout(int Width, int Height)
{
    Vector2 WindowSize = Vector2((float)Width, (float)Height);

    BackgroundImage // 略

    if (ConsoleDialog)
    {
        // 略

        if (FriendsDialog)
        {
            // 略
        }
    }

    if (PopupDialog)
    {
        // 略
    }

    if (ExitDialog)
    {
        // 略
    }

    if (AuthDialogs) AuthDialogs->UpdateLayout();
}

void FBaseMenu::Init()
{
    AuthDialogs->Init();
}

void FBaseMenu::OnGameEvent(const FGameEvent& Event)
{
    if (Event.GetType() == EGameEventType::ShowPrevUser)
    {
        // 略
    }
    else if (Event.GetType() == EGameEventType::ShowNextUser)
    {
        // 略
    }
    else if (Event.GetType() == EGameEventType::CancelLogin)
    {
        // 略
    }
    else if (Event.GetType() == EGameEventType::ToggleFPS)
    {
        // 略
    }
    else if (Event.GetType() == EGameEventType::ExitGame)
    {
        // 略
    }
    else if (Event.GetType() == EGameEventType::ShowPopupDialog)
    {
        // 略
    }

    if (FriendsDialog) // 略

    if (AuthDialogs) // 略
}

よって、 FGame, FMenu に関しては以下を確認すればいいです。プロジェクト固有の Dialog がある場合は、おそらく FMenu で確認できます。

  • コンストラク
  • Create
  • UpdateLayout
  • Init
  • OnGameEvent (EGameEventType::CheckAutoLogin イベントのみ)
  • その他 FBaseGame のメソッドを override しているもの

また呼び出し順序も、override メソッドを除けば上記の並びになっているようです。

イベントループ部分

イベントハンドル

SDLMain.cppProcessSDLEvents 関数が起点になり、そこから各プロジェクトに関する呼び出しの重要な部分は FBaseMenu::OnUIEvent になります。(コードは注目したい部分以外は省略したりカットしています)

void FBaseMenu::OnUIEvent(const FUIEvent& Event)
{
    if (Event.GetType() == EUIEventType::MousePressed)
    {
        for (DialogPtr NextDialog : Dialogs)
        {
            if (NextDialog->CheckCollision(Event.GetVector()))
            {
                NextDialog->OnUIEvent(Event);
            }
            else
            {
                NextDialog->SetFocused(false);
            }
        }
    }
    else
    {
        for (DialogPtr NextDialog : Dialogs)
        {
            NextDialog->OnUIEvent(Event);
        }
    }

    if (AuthDialogs) AuthDialogs->OnUIEvent(Event);
}

イベントハンドル部分に関しては以下を確認することになります。

  • FMenu::OnUIEvent
    • 必ずあるわけではないですが、override されている場合があります
  • プロジェクトで参照されている各 DialogOnUIEvent
    • 上記コードの Dialogs に入っているものすべてですが、各プロジェクトの FMenu 内で AddDialog されているものを探せばだいたい揃います

Tick

SDLMain.cppmain 関数内で FMain::Tick が呼び出されます。(Render は省略します)

void FMain::Tick()
{
    Timer.Tick([&]()
    {
        Update();
    });

    Render();
}

void FMain::Update()
{
    if (Game)
    {
        Game->Update();
    }
}
void FBaseGame::Update()
{
    Input->Update();

    if (Input->IsKeyPressed(FInput::InputCommands::Exit) ||
        Input->IsGamePadButtonPressed(FInput::InputCommands::Exit))
    {
        FGameEvent Event(EGameEventType::ExitGame);
        OnGameEvent(Event);
    }

    Users->Update();
    Menu->Update();
    Level->Update();
    Friends->Update();

    FPlatform::Update();
}
void FBaseMenu::Update()
{
    BackgroundImage->Update();
    TitleLabel->Update();
    FPSLabel->Update();

    std::unique_ptr<FInput> const& Input = FBaseGame::GetBase().GetInput();

    if (Input)
    {
        // check if characters were typed
        if (Input->IsAnyKeyPressed())
        {
            //Check if we need to generate repeated key press events
            if (KeyCurrentlyHeld != FInput::None)
            {
                if (!Input->IsKeyReleased(static_cast<FInput::Keys>(toupper(int(KeyCurrentlyHeld)))))
                {
                    KeyCurrentlyHeldSeconds += static_cast<float>(Main->GetTimer().GetElapsedSeconds());

                    if (KeyCurrentlyHeldSeconds >= SecondsTilKeyRepeat ||
                        (KeyCurrentlyHeld == FInput::Back && KeyCurrentlyHeldSeconds >= (SecondsTilKeyRepeat / 2.0f)) ||
                        (KeyCurrentlyHeld == FInput::Delete && KeyCurrentlyHeldSeconds >= (SecondsTilKeyRepeat / 2.0f)))
                    {
                        KeyCurrentlyHeldSeconds = 0.0f;
                        FUIEvent event(EUIEventType::KeyPressed, KeyCurrentlyHeld);
                        OnUIEvent(event);
                    }
                }
                else
                {
                    KeyCurrentlyHeld = FInput::None;
                    KeyCurrentlyHeldSeconds = 0.0f;
                }
            }
        }
    }

    for (DialogPtr NextDialog : Dialogs)
    {
        NextDialog->Update();
    }

    AuthDialogs->Update();

    UpdateFPS();
}
void FPlatform::Update()
{
    if (PlatformHandle)
    {
        EOS_Platform_Tick(PlatformHandle);
    }

    if (!bIsInit && !bIsShuttingDown)
    {
        if (!bHasShownCreateFailedError)
        {
            // 略
        }
    }

    if (bHasInvalidParamProductId ||
        bHasInvalidParamSandboxId ||
        bHasInvalidParamDeploymentId ||
        bHasInvalidParamClientCreds)
    {
        if (!bHasShownInvalidParamsErrors)
        {
            // 略
        }
    }
}

Tick 部分に関しては以下を確認することになります。

  • FGame::Update
  • プロジェクトで参照されている各 DialogUpdate

OnGameEvent

OnGameEvent は明示的なユーザー操作でトリガされる OnUIEvent とは異なり、ユーザー操作後であったり、通信の前後であったり、コンソールコマンドであったりとあらゆる状況で呼び出される可能性があります。

基本的には FGame 及び FBaseGameOnGameEvent が呼び出され、そこから dispatch されます。

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);
}

上記の Menu->OnGameEvent があるため、FMenu, FBaseMenu が管理する各 Dialog の OnGameEvent にさらに伝搬するケースもあります。

よって、OnGameEvent に関しては以下を確認することになります。

  • FGame::OnGameEvent
  • FMenu::OnGameEvent
  • プロジェクトで参照されている各 DialogOnGameEvent

終了部分

SDLMain.cppmain 関数内で FMain::OnShutdown が呼び出されます。

void FMain::OnShutdown()
{
    if (Game)
    {
        Game->OnShutdown();
    }
}
void FBaseGame::OnShutdown()
{
    // Must explicitly call FEosUI::OnShutdown() before the end of destruction to allow FBaseGame::GetBase() to not throw.
    if (EosUI)
    {
        EosUI->OnShutdown();
    }

    // Must explicitly call FAuthentication::Shutdown() before the end of destruction to allow FBaseGame::GetBase() to not throw.
    if (Authentication)
    {
        Authentication->Shutdown();
    }

    Release();
}

よって、FGame::OnShutdown を確認しておけばよいと思います。

まとめ

AuthAndFriends は共有分のコードを読むのに時間がかかりましたが、他のプロジェクトを読む際はそこを省略できます。

今回は、省略して読むもののより流れを追いつつ簡単に読んでいけるようにサンプルのフレームワーク部分と、各プロジェクトの呼び出し関係を整理しました。

引き続き、他のプロジェクトも読んでいければと思います。