Epic Online Services サンプルの P2PNAT のコードリーディング ①

前回AuthAndFriendsP2PNAT のプロジェクトにおける Shared 部分とそうでない部分の比較をしつつ、どこを読めばだいたい概要をつかめるかを整理しました。

今回はそのまま P2PNAT を見ていきます。単純に自分がこの機能を使いたいと思っているためです。

初期化部分

前回の記事に習い、まずは以下をみていきます。

  • FGame, FMenu
    • コンストラク
    • Create
    • UpdateLayout
    • Init
    • OnGameEvent (EGameEventType::CheckAutoLogin イベントのみ)

FGame

FGame::FGame() noexcept(false)
{
    Menu = std::make_unique<FMenu>(Console);
    Level = std::make_unique<FLevel>();
    P2PNATComponent = std::make_unique<FP2PNAT>();

    CreateConsoleCommands();
}

void FGame::OnGameEvent(const FGameEvent& Event)
{
    FBaseGame::OnGameEvent(Event);

    P2PNATComponent->OnGameEvent(Event);
}

FP2PNAT を利用しているようです。後ほど見ていきます。

FMenu

FMenu::FMenu(std::weak_ptr<FConsole> InConsole) noexcept(false):
    FBaseMenu(InConsole)
{
}

void FMenu::Create()
{
    CreateFriendsDialog();
    CreateP2PNATDialog();
    CreateNATDocsButton();

    FBaseMenu::Create();

    AuthDialogs->SetSingleUserOnly(true);
}

void FMenu::CreateP2PNATDialog()
{
    const float FX = 100.0f;
    const float FY = P2PDialogPositionY;
    const float Width = 300.0f;
    const float Height = 300.0f;

    P2PNATDialog = std::make_shared<FP2PNATDialog>(
        Vector2(FX, FY),
        Vector2(Width, Height),
        DefaultLayer - 2,
        NormalFont->GetFont(),
        BoldSmallFont->GetFont(),
        TinyFont->GetFont(),
        LargeFont->GetFont());

    P2PNATDialog->SetBorderColor(Color::UIBorderGrey);

    P2PNATDialog->Create();

    AddDialog(P2PNATDialog);
}

void FMenu::CreateNATDocsButton()
{
    Vector2 NATDocsButtonSize = Vector2(150.0f, 35.0f);

    NATDocsButton = std::make_shared<FButtonWidget>(
        Vector2(800.0f, 100.0f),
        NATDocsButtonSize,
        DefaultLayer - 1,
        L"DOCUMENTATION",
        assets::LargeButtonAssets,
        NormalFont->GetFont());
    NATDocsButton->SetOnPressedCallback([]()
    {
        FUtils::OpenURL(NATDocsURL);
    });
    //NATDocsButton->SetBackgroundColors(assets::DefaultButtonColors);
    NATDocsButton->SetBorderColor(Color::UIBorderGrey);
    NATDocsButton->Create();
    NATDocsButton->Hide();
}

void FMenu::OnGameEvent(const FGameEvent& Event)
{
    FBaseMenu::OnGameEvent(Event);
    if (P2PNATDialog) P2PNATDialog->OnGameEvent(Event);
}

void FMenu::UpdateLayout(int Width, int Height)
{
    // レイアウトの更新なので省略
}

f:id:you1dan:20220117212256p:plain

P2PNATDialogNATDocsButton を擁していますが、NATDocsButton に関しては、 Hide されているため表示されていません。
また、AuthDialogSetSingleUserOnly(true) が呼び出さており、アプリケーションごとに 1 ユーザーのみログイン可能に制限されているのが AuthAndFriends サンプルとの相違点です。

では、ここから、新たに登場した FP2PNATFP2PNATDialog の初期化部分も見ていきます。

FP2PNAT

FP2PNAT::FP2PNAT()
{   
}

void FP2PNAT::OnGameEvent(const FGameEvent& Event)
{
    // CheckAutoLogin は扱わないため省略
}

特にさらに依存するものはなさそうです。

FP2PNATDialog

FP2PNATDialog::FP2PNATDialog(
    Vector2 DialogPos,
    Vector2 DialogSize,
    UILayer DialogLayer,
    FontPtr DialogNormalFont,
    FontPtr DialogSmallFont,
    FontPtr DialogTinyFont,
    FontPtr DialogTitleFont) :
    FDialog(DialogPos, DialogSize, DialogLayer)
{
    HeaderLabel = std::make_shared<FTextLabelWidget>(
        DialogPos,
        Vector2(DialogSize.x, 25.0f),
        Layer - 1,
        std::wstring(L"Header"),
        L"Assets/wide_label.dds",
        FColor(1.f, 1.f, 1.f, 1.f),
        FColor(1.f, 1.f, 1.f, 1.f),
        EAlignmentType::Left
        );
    HeaderLabel->SetFont(DialogNormalFont);

    BackgroundImage = std::make_shared<FSpriteWidget>(
        DialogPos,
        DialogSize,
        DialogLayer,
        L"Assets/texteditor.dds");

    NATStatusLabel = std::make_shared<FTextLabelWidget>(
        DialogPos + Vector2(HeaderLabel->GetSize().x + HeaderLabel->GetSize().x - 170.0f, 0.0f),
        Vector2(170.0f, 25.0f),
        Layer - 1,
        L"NAT Status: Unknown",
        L"",
        FColor(1.f, 1.f, 1.f, 1.f),
        FColor(1.f, 1.f, 1.f, 1.f),
        EAlignmentType::Left
        );
    NATStatusLabel->SetFont(DialogNormalFont);

    Vector2 ChatInputFieldSize = Vector2(DialogSize.x, 30.0f);
    Vector2 ChatInputFieldPos = DialogPos + Vector2(0.0f, DialogSize.y) - Vector2(0.0f, ChatInputFieldSize.y);

    ChatInputField = std::make_shared<FTextFieldWidget>(
        ChatInputFieldPos,
        ChatInputFieldSize,
        Layer - 1,
        L"Chat...",
        L"Assets/textfield.dds",
        DialogNormalFont,
        FTextFieldWidget::EInputType::Normal,
        EAlignmentType::Left);
    ChatInputField->SetBorderColor(Color::UIBorderGrey);

    ChatInputField->SetOnEnterPressedCallback(
        [this](const std::wstring& Value)
    {
        this->OnSendMessage(Value);
    }
    );

    Vector2 ChatTextViewPos = HeaderLabel->GetPosition() + Vector2(0.0f, HeaderLabel->GetSize().y + 5.0f);
    Vector2 ChatTextViewSize = Vector2(DialogSize.x, DialogSize.y - ChatInputFieldSize.y - HeaderLabel->GetSize().y - 10.0f);

    ChatTextView = std::make_shared<FTextViewWidget>(
        ChatTextViewPos,
        ChatTextViewSize,
        Layer - 1,
        L"Choose friend to start chat...",
        L"Assets/textfield.dds",
        DialogNormalFont);

    ChatTextView->SetBorderOffsets(Vector2(10.0f, 10.0f));
    ChatTextView->SetScrollerOffsets(Vector2(5.f, 5.0f));
    ChatTextView->SetFont(DialogNormalFont);

    ChatInfoLabel = std::make_shared<FTextLabelWidget>(
        ChatTextViewPos,
        ChatTextViewSize,
        Layer - 2,
        L"",
        L"",
        FColor(1.f, 1.f, 1.f, 1.f),
        FColor(1.f, 1.f, 1.f, 1.f),
        EAlignmentType::Center
        );
    ChatInfoLabel->SetFont(DialogTitleFont);

    std::vector<std::wstring> EmptyButtonAssets = {};
    Vector2 CloseChatButtonSize = Vector2(150.0f, 35.0f);
    CloseCurrentChatButton = std::make_shared<FButtonWidget>(
        ChatTextViewPos + Vector2(ChatTextViewSize.x - CloseChatButtonSize.x - 10.0f, 10.0f),
        CloseChatButtonSize,
        Layer - 2,
        L"CLOSE CHAT",
        std::vector<std::wstring>({ L"Assets/solid_white.dds" }),
        DialogNormalFont,
        Color::UIHeaderGrey);
    CloseCurrentChatButton->SetFont(DialogNormalFont);
    CloseCurrentChatButton->SetBorderColor(Color::UIBorderGrey);
    CloseCurrentChatButton->SetOnPressedCallback([]()
    {
        static_cast<FMenu&>(*FGame::Get().GetMenu()).GetP2PNATDialog()->CloseCurrentChat();
    });
    CloseCurrentChatButton->Hide();
}

void FP2PNATDialog::Create()
{
    if (HeaderLabel) HeaderLabel->Create();
    if (BackgroundImage) BackgroundImage->Create();
    if (NATStatusLabel) NATStatusLabel->Create();
    if (ChatTextView) ChatTextView->Create();
    if (ChatInputField)   ChatTextView->Create();
    if (ChatInfoLabel) ChatInfoLabel->Create();
    if (CloseCurrentChatButton) CloseCurrentChatButton->Create();

    AddWidget(HeaderLabel);
    AddWidget(BackgroundImage);
    AddWidget(NATStatusLabel);
    AddWidget(ChatTextView);
    AddWidget(ChatInputField);
    AddWidget(ChatInfoLabel);
    AddWidget(CloseCurrentChatButton);
}

void FP2PNATDialog::OnGameEvent(const FGameEvent& Event)
{
    // CheckAutoLogin は扱わないため省略
}

上記ですでにログイン前の UI イメージを貼りましたが、以下はログインし Chat の往復をした場合のイメージです。
上記コードのコンストラクタと Create メソッドの内容がなんとなく読み取れます。

f:id:you1dan:20220117213623p:plain

FP2PNATDialog のイベントハンドラ

上記のコードからイベントハンドラ部分だけひろっておきます。

FP2PNATDialog::FP2PNATDialog(
    Vector2 DialogPos,
    Vector2 DialogSize,
    UILayer DialogLayer,
    FontPtr DialogNormalFont,
    FontPtr DialogSmallFont,
    FontPtr DialogTinyFont,
    FontPtr DialogTitleFont) :
    FDialog(DialogPos, DialogSize, DialogLayer)
{
    // 略

    ChatInputField->SetOnEnterPressedCallback(
        [this](const std::wstring& Value)
    {
        this->OnSendMessage(Value);
    }
    );

    // 略

    CloseCurrentChatButton->SetOnPressedCallback([]()
    {
        static_cast<FMenu&>(*FGame::Get().GetMenu()).GetP2PNATDialog()->CloseCurrentChat();
    });

それぞれ、Chat の入力フィールドと、Chat 開始後の Close ボタンに関するイベント Callback です。

入力メッセージに関しては、おそらく EOS SDK に関わってくると思いますが、まだ P2P 機能の初期化も出てきていないのでいったん保留します。

P2P 接続が行われるのはどこか

ログイン後、Friends もログイン状態である場合には以下のような UI になります。以下の変化があります。

  • 自身がログインすると NAT StatusUnknown から変化する(私の環境だと Moderate )
  • Friends 一覧に CHAT ボタンが表示される

f:id:you1dan:20220117215056p:plain

ログイン時

ユーザーのログイン状態に変化がある場合には、OnGameEventFAuthentication からのイベント発火が行われます。ログイン関連のイベントは多く存在しますが、P2PNATDialog がまとめて処理しています。

void FP2PNATDialog::OnGameEvent(const FGameEvent& Event)
{
    if (Event.GetType() == EGameEventType::UserLoggedIn)
    {
        UpdateNATStatus();
    }
    else if (Event.GetType() == EGameEventType::UserLoginRequiresMFA)
    {
        UpdateNATStatus();
        SetFocused(false);
    }
    else if (Event.GetType() == EGameEventType::UserLoginEnteredMFA)
    {
        UpdateNATStatus();
    }
    else if (Event.GetType() == EGameEventType::UserLoggedOut)
    {
        UpdateNATStatus();
    }
    else if (Event.GetType() == EGameEventType::ShowPrevUser)
    {
        UpdateNATStatus();
    }
    else if (Event.GetType() == EGameEventType::ShowNextUser)
    {
        UpdateNATStatus();
    }
    else if (Event.GetType() == EGameEventType::NewUserLogin)
    {
        UpdateNATStatus();
    }
    else if (Event.GetType() == EGameEventType::CancelLogin)
    {
        UpdateNATStatus();
    }
    else if (Event.GetType() == EGameEventType::StartChatWithFriend)
    {
        // 略
    }
}

void FP2PNATDialog::UpdateNATStatus()
{
    if (NATStatusLabel)
    {
        FGame::Get().GetP2PNAT()->RefreshNATType();
    }
}
void FP2PNAT::RefreshNATType()
{
    EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle());

    EOS_P2P_QueryNATTypeOptions Options;
    Options.ApiVersion = EOS_P2P_QUERYNATTYPE_API_LATEST;

    EOS_P2P_QueryNATType(P2PHandle, &Options, nullptr, OnRefreshNATTypeFinished);
}

void EOS_CALL FP2PNAT::OnRefreshNATTypeFinished(const EOS_P2P_OnQueryNATTypeCompleteInfo* Data)
{
    if (Data)
    {
        if (Data->ResultCode != EOS_EResult::EOS_Success)
        {
            FDebugLog::LogError(L"EOS P2PNAT QueryNATType callback: error code %ls", FStringUtils::Widen(EOS_EResult_ToString(Data->ResultCode)).c_str());
        }
    }
    else
    {
        FDebugLog::LogError(L"EOS P2PNAT QueryNATType callback: bad data returned");
    }
}

NAT Status の更新

ついでなので NAT Status ラベルの更新情報も見ておきます。実際にはこの流れではなく、FP2PNATDialog::Update の方で処理されます。以下は、その中でラベルの更新のために NATType を取得する先の処理です。

EOS_ENATType FP2PNAT::GetNATType() const
{
    EOS_ENATType NATType = EOS_ENATType::EOS_NAT_Unknown;
    EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle());

    EOS_P2P_GetNATTypeOptions Options;
    Options.ApiVersion = EOS_P2P_GETNATTYPE_API_LATEST;

    EOS_EResult Result = EOS_P2P_GetNATType(P2PHandle, &Options, &NATType);

    if (Result == EOS_EResult::EOS_NotFound)
    {
        //NAT type has not been queried yet. (Or query is not finished)        
        return EOS_ENATType::EOS_NAT_Unknown;
    }

    if (Result != EOS_EResult::EOS_Success)
    {
        FDebugLog::LogError(L"EOS P2PNAT GetNatType: error while retrieving NAT Type: %ls", FStringUtils::Widen(EOS_EResult_ToString(Result)).c_str());
        return EOS_ENATType::EOS_NAT_Unknown;
    }

    return NATType;
}

ドキュメントには以下の記述があります。よって、API を呼び出していますが、基本は事前に Query していたものの Cache を取得するだけという形のようです。

EOS_EResult::EOS_Success - if we have cached data EOS_EResult::EOS_NotFound - If we do not have queried data cached

FriendsInfoWidget

続いて CHAT ボタンですが、以下のように Friends を表示する Widget (これは Shared にある)に分岐がありました。
そこで StartChatWithFriend のイベントが発火されていることがわかります。

void FFriendInfoWidget::Create()
{
    // 略

#elif defined(EOS_SAMPLE_P2P) || defined (EOS_SAMPLE_VOICE)
            //Start chat button (p2p demo only)
            const float ButtonSize = Size.x * 0.3f;
            Vector2 ButtonOffset(Size.x - ButtonSize - 10.0f, Size.y * 0.25f);

            FEpicAccountId FriendUserId = FriendData.UserId;
            FProductUserId FriendProductUserId = FriendData.UserProductUserId;
            std::wstring FriendName = FriendData.Name;
            if (FriendProductUserId.IsValid() && FriendData.Presence.Status != EOS_Presence_EStatus::EOS_PS_Offline)
            {
                Button1 = std::make_shared<FButtonWidget>(
                    Position + ButtonOffset,
                    Vector2(ButtonSize, Size.y / 2.0f),
                    Layer - 1,
                    L"CHAT",
                    std::vector<std::wstring>({ L"Assets/button.dds" }),
                    SmallFont,
                    ChatButtonBackgroundColors[static_cast<size_t>(FButtonWidget::EButtonVisualState::Idle)]);
                Button1->Create();
                Button1->SetOnPressedCallback([FriendUserId, FriendProductUserId, FriendName]()
                {
                    FGameEvent Event(EGameEventType::StartChatWithFriend, FriendUserId, FriendProductUserId, FriendName);
                    FGame::Get().OnGameEvent(Event);
                });
                Button1->SetBackgroundColors(ChatButtonBackgroundColors);
            }

こちらも FP2PNATDialog::OnGameEvent で処理されています。

void FP2PNATDialog::OnGameEvent(const FGameEvent& Event)
{
    // 略
    else if (Event.GetType() == EGameEventType::StartChatWithFriend)
    {
        // Open chat with friend
        CurrentChat = Event.GetProductUserId();

        bChatViewDirty = true;
        ChatTextView->Clear();
    }
}

ここに関しては、Chat の UI の準備をしているだけで API に関しては何もしていないようです。

ここまでのまとめ

P2PNAT サンプルの初期化部分を中心に眺めてきました。

NAT P2P インターフェース | Epic Online Services ドキュメンテーション

現状、ここまででは上記の資料の「NAT タイプを決定する」の部分に出てきた API ぐらいしか使っておらず、これ以降に接続やデータ送受信が出てくると思われます。

今回、上記の資料を読む前にコードを見始めてしまったので順序が逆になってしまったのですが、キャッシュに関しても以下のように記載されていました。

EOS_P2P_QueryNATType が完了するとユーザーの NAT タイプの値がキャッシュされるので、関数 EOS_P2P_GetNATType を代わりに呼び出してこの値をすぐに返すことができます。これが正常に返されるのは EOS_P2P_QueryNATType が少なくとも 1 回は完了した場合のみです。EOS_P2P_GetNATType 関数の呼び出しには以下のパラメータを指定する必要があります。

次回も引き続き P2PNAT に関してみていければと思います。