前回 、AuthAndFriends
と P2PNAT
のプロジェクトにおける 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) { // レイアウトの更新なので省略 }
P2PNATDialog
と NATDocsButton
を擁していますが、NATDocsButton
に関しては、 Hide
されているため表示されていません。
また、AuthDialog
の SetSingleUserOnly(true)
が呼び出さており、アプリケーションごとに 1 ユーザーのみログイン可能に制限されているのが AuthAndFriends
サンプルとの相違点です。
では、ここから、新たに登場した FP2PNAT
と FP2PNATDialog
の初期化部分も見ていきます。
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
メソッドの内容がなんとなく読み取れます。
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 Status
がUnknown
から変化する(私の環境だとModerate
) - Friends 一覧に
CHAT
ボタンが表示される
ログイン時
ユーザーのログイン状態に変化がある場合には、OnGameEvent
に FAuthentication
からのイベント発火が行われます。ログイン関連のイベントは多く存在しますが、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"); } }
- EOS_Platform_GetP2PInterface | Epic Online Services ドキュメンテーション
Get a handle to the Peer-to-Peer Networking Interface.
- EOS_P2P_QueryNATType | Epic Online Services ドキュメンテーション
- EOS_ENATType | Epic Online Services ドキュメンテーション
EOS_NAT_Unknown
: NAT type either unknown (remote) or we are unable to determine it (local)EOS_NAT_Open
: All peers can directly-connect to youEOS_NAT_Moderate
: You can directly-connect to other Moderate and Open peersEOS_NAT_Strict
: You can only directly-connect to Open peers
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; }
- EOS_P2P_GetNATType | Epic Online Services ドキュメンテーション
Get our last-queried NAT-type, if it has been successfully queried.
- last-queried になっているので、 Reflesh 時に
EOS_P2P_QueryNATType
を呼び出しているため、その結果が使われると思われます
ドキュメントには以下の記述があります。よって、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 に関してみていければと思います。