다음을 통해 공유


비동기 PlayFab 파티 작업 및 알림

속도가 느리거나 계산 비용이 많이 드는 작업의 경우 PlayFab 파티는 비동기 API를 노출합니다. 비동기 API는 메인 스레드에서 비용이 많이 들거나 느린 작업을 시작하고 선택한 스레드에서 해당 작업이 완료될 때까지 폴링할 수 있는 기능을 타이틀에 제공합니다. 타이틀에 PlayFab 파티 라이브러리 업데이트의 비동기 알림을 제공할 때에도 동일한 폴링 메커니즘이 사용됩니다. 이 페이지에서는 PlayFab 파티의 비동기 API 패턴과 이에 대한 프로그래밍 모범 사례를 간략하게 설명합니다.

기본 API 패턴

PlayFab 파티에서 인식하는 비동기 API 패턴에는 다음 두 가지 유형이 있습니다.

  1. 비동기 작업
  2. 비동기 알림

비동기 작업

PlayFab 파티의 비동기 API는 간단하게 사용할 수 있습니다. 비동기 작업을 시작하고 완료하는 일반적인 패턴은 다음과 같습니다.

  1. 선택한 적절한 비동기 API에 대한 일반 메서드 호출을 만듭니다. 사용할 수 있는 일반적인 비동기 파티 작업에는 다음이 포함됩니다.

  2. PARTY_SUCCEEDED() 또는 PARTY_FAILED() 매크로를 사용하여 API의 PartyError 반환 값을 확인합니다. 동기적으로 반환된 이 값은 작업이 성공적으로 시작되었는지 여부를 알려줍니다.

Warning

비동기 파티 API 호출의 동기 반환 값은 해당 작업이 성공적으로 완료되었는지의 여부를 알려주지 않습니다. 동기 오류 대 비동기 오류에 대한 자세한 내용은 이후 섹션을 통해 확인할 수 있습니다.

  1. PartyManager::StartProcessingStateChanges()에서 제공하는 관련 작업의 “완료 상태 변경”을 찾아 비동기 작업의 완료를 폴링합니다. PartyManager::CreateNewNetwork()에 연결된 “완료 상태 변경”의 예시는 PartyCreateNewNetworkCompletedStateChange입니다. “상태 변경”이 무엇인지와 그 작동 원리가 어떻게 되는지에 대한 자세한 내용은 아래의 상태 변경 섹션에서 확인할 수 있습니다.

  2. 완료 상태 변경 resulterrorDetail 값을 확인하여 작업의 성공 또는 실패 여부를 확인합니다. 이러한 오류 값에 대한 자세한 내용은 아래의 동기 및 비동기 오류 섹션에서 확인할 수 있습니다.

비동기 알림

일부 기능은 타이틀에 PlayFab 파티 라이브러리의 변경 내용에 대한 비동기 알림을 보냅니다.

일반적인 알림은 다음과 같습니다.

  1. 파티 네트워크 상에 새 엔드포인트가 만들어질 경우 EndpointCreated입니다.
  2. 채팅 컨트롤이 파티 네트워크에 참가하는 경우 ChatControlJoinedNetwork입니다.
  3. 파티가 로컬 채팅 컨트롤의 오디오 입력이 어떤 식으로든 변경되었음을 등록할 경우 LocalChatAudioInputChanged입니다.

이러한 비동기 알림은 PartyManager::StartProcessingStateChanges()를 통해 PlayFab 파티가 “상태 변경”으로 타이틀에 제공합니다.

“상태 변경”이 무엇인지와 그 작동 원리가 어떻게 되는지에 대한 자세한 내용은 아래의 상태 변경 섹션에서 확인할 수 있습니다.

상태 변경

PlayFab 파티의 비동기 API 모델은 PartyStateChange 구조체를 중심으로 빌드됩니다.

이러한 “상태 변경”은 PlayFab 파티 라이브러리의 이벤트에 대한 비동기 알림입니다. 이러한 알림은 내부적으로 대기열에 추가되며 타이틀에서 PartyManager::StartProcessingStateChanges()를 호출해 해당 알림을 처리합니다. StartProcessingStateChanges()는 대기 중인 모든 상태 변경을 타이틀이 반복하고 개별적으로 처리할 수 있는 목록으로 반환합니다. 각 상태 변경에는 PartyStateChange::stateChangeType 필드가 있어 타이틀이 어떤 특정 상태 변경에 대해 알림을 받는지 확인하기 위해 검사할 수 있습니다. 타이틀이 전달받을 상태 변경이 무엇인지 확인한 뒤에는 일반 PartyStateChange 구조체를 더욱 구체적인 유형의 상태 변경 구조체로 캐스트하여 해당 이벤트의 특정 데이터를 조사할 수 있습니다.

일반적으로는 상태 변경 처리는 각각의 상태 변화를 처리기에 위임하는 단순한 스위치 문으로 구현됩니다.

상태 변경 목록은 처리된 후에 반드시 PartyManager::FinishProcessingStateChanges()에 반환되어야 합니다.

uint32_t stateChangeCount;
Party::PartyStateChangeArray stateChanges;
PartyError error = Party::PartyManager::GetSingleton().StartProcessingStateChanges(&stateChangeCount, &stateChanges);
if (PARTY_FAILED(error))
{
    return error;
}

for (uin32_t i = 0; i < stateChangeCount; ++i)
{
    const Party::PartyStateChange* stateChange = stateChanges[i];
    switch (stateChange->stateChangeType)
    {
        case Party::PartyStateChangeType::CreateNewNetworkCompleted:
        {
            auto createNewNetworkStateChange = static_cast<const Party::PartyCreateNewNetworkCompletedStateChange*>(stateChange);
            HandlePartyCreateNewNetworkCompleted(createNewNetworkStateChange);
            break;
        }
        // add other state change handlers here.
    }
}

error = Party::PartyManager::GetSingleton().FinishProcessingStateChanges(stateChangeCount, stateChanges);
if (PARTY_FAILED(error))
{
    return error;
}

참고 항목

대부분의 타이틀은 모든 상태 변경을 즉시 처리하는 동시에 이들을 FinishProcessingStateChanges()에 반환하는 것이 좋습니다. 고급 시나리오의 경우 일부 상태 변경은 조금 더 기다렸다가 나중에 반환하는 것이 적절한 경우도 있습니다. 이것에 대해서는 아래의 상태 변경 내용 유지에서 자세히 논의됩니다.

동기 오류 대 비동기 오류

비동기 PlayFab 파티 API를 사용하는 경우 처리해야 하는 오류가 두 가지 유형이라는 사실을 알아야 합니다.

  1. 동기 오류
  2. 비동기 오류

"동기 오류"란 비동기 API 호출의 반환 값으로 제공되는 오류이며 비동기 작업을 시작하지 못했음을 나타냅니다. 이러한 유형의 오류는 일반적으로 잘못된 매개 변수를 통해 API를 호출하거나 라이브러리가 잘못된 상태에서 API를 호출한 경우 또는 내부 라이브러리에서 메모리 할당에 실패한 경우에 발생합니다.

"비동기 오류"란 개별 작업의 비동기 완료와 관련된 상태 변경 내부의 데이터로 제공됩니다. 상태 변경 구조체에는 오류 처리와 관련된 필드가 두 가지 있습니다.

  1. result - 모든 완료 상태 변경에 사용할 수 있는 PartyStateChangeResult enum 값입니다. 해당 값의 목적은 타이틀에 작업 실패 이유에 대한 광범위한 설명을 전달하는 것입니다. 해당 값은 타이틀이 프로그래밍 방식으로 비동기 오류에 대응하고 이를 처리하는 데 사용될 수 있습니다.
  2. errorDetail - 모든 완료 상태 변경 및 일부 알림 상태 변경에 사용할 수 있는 PartyError 값입니다. 이러한 오류 세부 정보 값은 대개 모호하여 해당 값에 대해 프로그래밍하면 안됩니다. 해당 값들은 주로 관련 작업의 실패 원인을 보다 자세히 확인하기 위해 원격 분석이나 오류 로그 등의 진단에서 작성됩니다. 이러한 오류 세부 정보는 PartyManager::GetErrorMessage()를 호출해 사람이 읽을 수 있는 형식으로 변환할 수 있습니다. 해당 오류 메시지는 개발자들만 확인할 수 있게 되어 있습니다. 해당 메시지들은 최종 사용자의 사용을 위해 현지화하거나 처리하지 않습니다. 또한 오류 메시지와 함께 PlayFab 파티 SDK의 오류 코드 목록은 PlayFab 파티 오류 코드 문서에서 사용할 수 있습니다.

Important

개발자 로그, 원격 분석, PlayFab 개발자 커뮤니티 오류 보고서 등의 상태 변경 실패를 캡처하는 경우 관련된 모든 상태 변경의 resulterrorDetail 값을 진단의 일부로서 캡처하는 것이 강력히 권장됩니다. 이들 두 종류의 값은 각각 있을 때보다 함께 캡처되면 더욱 많은 정보를 캡처합니다.

비동기 작업 식별자

각각의 비동기 API에는 void* asyncIdentifer 매개 변수가 포함됩니다. 이 값은 StartProcessingStateChanges()에서 제공되면 이 API 호출의 관련 완료 상태 변경에 설정되는 통과 매개 변수입니다.

타이틀에게 포인터 규격의 임의적 컨텍스트를 해당 비동기 API 호출에 연결할 메커니즘을 제공하는 것이 이 값의 목적입니다. 이러한 컨텍스트는 다음을 비롯한 여러 시나리오에서 사용할 수 있습니다.

  1. PlayFab 파티 API 호출에 특정 타이틀 데이터를 연결
  2. 공유 식별자로 다양한 비동기 작업을 한꺼번에 연결

이러한 비동기 식별자는 PlayFab 파티 사용에 필수적인 부분은 아니지만 일부 타이틀 논리를 작성하기 쉽게 해 줄 수는 있습니다.

작업 대기열

비동기 API로 작업할 때 여러 비동기 작업이 더 큰 비동기 흐름의 일부로 차례로 실행되어야 하는 경우가 많습니다.

PlayFab 파티의 일례로서 파티 네트워크에 가입해 채팅 컨트롤을 거기에 연결하는 사례가 있을 수 있습니다. 직렬화하면 이러한 흐름은 다음과 같은 모습입니다.

  1. PartyManager::ConnectToNetwork()를 호출해 장치를 파티 네트워크에 연결합니다.
  2. PartyConnectToNetworkCompletedStateChange를 기다려 연결이 성공했음을 리플렉션합니다.
  3. PartyNetwork::AuthenticateLocalUser()를 호출해 인증하고 사용자를 파티 네트워크에 가입하도록 합니다.
  4. PartyAuthenticateLocalUserCompletedStateChange를 기다려 인증이 성공했음을 리플렉션합니다.
  5. PartyNetwork::ConnectChatControl()을 호출해 채팅 컨트롤을 파티 네트워크에 연결합니다.
  6. PartyConnectChatControlCompletedStateChange를 기다려 채팅 컨트롤 연결이 성공했음을 리플렉션합니다.

보다 복잡한 흐름과 타이틀 논리에도 이러한 직렬화 패턴이 적절할 수 있습니다. 그러나 더 간단한 흐름을 위해 PlayFab 파티는 타이틀 코드를 단순화하려는 대안을 제공합니다.

이전 작업이 완료되기 전에 종속된 작업을 대기열에 등록하는 비동기 지원인 여러 PlayFab 파티 API가 그것입니다. 이전 예에서 연결을 성공적으로 완료하기 전에 로컬 사용자를 파티 네트워크에 인증하기 시작할 수 있고 연결된 로컬 사용자가 인증을 완료하기 전에 채팅 컨트롤 연결을 시작할 수 있습니다.

사실상 큐를 사용하면 비동기 작업 모음을 함께 묶고 한 번에 시작하고 오류 처리를 단일 실패 지점으로 통합할 수 있습니다.

Party::PartyNetwork* newPartyNetwork;
PartyError err = PartyManager::GetSingleton().ConnectToNetwork(networkDescriptor, nullptr, &newPartyNetwork);
if (PARTY_SUCCEEDED(err))
{
    err = newPartyNetwork->AuthenticateLocalUser(m_localUser, networkInvitation, nullptr);
    if (PARTY_SUCCEEDED(err))
    {
        err = newPartyNetwork->ConnectChatControl(m_chatControl, nullptr);
        if (PARTY_SUCCEEDED(err))
        {
            Log("Connecting chat control to new party network!");
            m_network = newPartyNetwork;

            // After this point, we should log any failures reported in PartyConnectToNetworkCompletedStateChange,
            // PartyAuthenticateLocalUserCompletedStateChange, and PartyConnectChatControlCompletedStateChange for
            // diagnostic purposes, but all final failure logic and any retry logic can be coalesced into
            // PartyConnectChatControlCompletedStateChange.
        }
    }

    // If we experienced any unexpected failures to start the authentice or connect-chat-control operations, queue
    // a leave operation so we don't join the network in a half-state.
    if (PARTY_FAILED(err))
    {
        (void) newPartyNetwork->LeaveNetwork(nullptr);
    }
}

상태 변경 내용 유지

일반적으로 StartProcessingStateChanges()에서 제공하는 상태 변경은 즉시 처리되어 FinishProcessingStateChanges()에 바로 반환됩니다.

uint32_t stateChangeCount;
Party::PartyStateChangeArray stateChanges;
PartyError error = Party::PartyManager::GetSingleton().StartProcessingStateChanges(&stateChangeCount, &stateChanges);
if (PARTY_FAILED(error))
{
    return error;
}


// process the state changes
// ...

error = Party::PartyManager::GetSingleton().FinishProcessingStateChanges(stateChangeCount, stateChanges);
if (PARTY_FAILED(error))
{
    return error;
}

이러한 흐름은 프로그래밍이 간단해지며 대부분의 타이틀 시나리오를 지원한다는 점에서 바람직한 흐름입니다. 그러나 일부 고급 시나리오에서는 상태 변경의 하위 집합을 유지해 이들의 원래 순서를 벗어나 나중에 반환하는 것이 적절할 수 있습니다. 이러한 작업은 일반적으로 해당 상태 변경과 관련된 리소스의 수명을 연장하기 위해 수행합니다.

예를 들면 타이틀이 주요 스레드에서 상태 변경을 처리하지만 수신된 엔드포인트 메시지는 네트워크 업데이트에 따라 타이틀 상태를 업데이트하는 백그라운드 스레드에서 처리할 수 있습니다. 전형적인 상태 변경 처리의 흐름이라면 여기에서 PartyEndpointMessageReceivedStateChange의 엔드포인트 메시지를 수명이 더 긴 버퍼로 복사해야 합니다. 해당 상태 변경 버퍼는 상태 변경이 FinishProcessingStateChanges()로 반환될 때 회수되기 때문입니다. PartyEndpointMessageReceivedStateChange를 제외해 그 반환을 메시지를 처리한 뒤로 미루면 복사를 자주 수행할 필요가 없어집니다.

uint32_t endpointMessageCount = 0;
for (uint32_t i = 0; i < stateChangeCount; ++i)
{
    switch (stateChanges[i]->stateChangeType)
    {
        //
        // ... Process all state change types except for EndpointMessageReceived here...
        //
    }
}

// if there were any endpoint messages in the queue, separate them out, queue them, and return the remaining state
// changes in a separate buffer

// NOTE: this sample uses local std::vectors for brevity, but avoiding heap allocations by reusing vectors or
// using a fixed sized buffer tuned for your game may be appropriate.
std::vector<const Party::PartyStateChange*> nonEndpointStateChanges;
std::vector<const Party::PartyStateChange*> endpointStateChanges;
for (uint32_t i = 0; i < stateChangeCount; ++i)
{
    if (stateChanges[i]->stateChangeType == Party::PartyStateChangeType::EndpointMessageReceived)
    {
        endpointStateChanges.push_back(stateChanges[i]);
    }
    else
    {
        nonEndpointStateChanges.push_back(stateChanges[i]);
    }
}

// Return all non-endpoint message state changes
err = PartyManager::GetSingleton().FinishProcessingStateChanges(
    static_cast<uint32_t>(nonEndpointStates.size()),
    nonEndpointStateChanges.data());

// Title-defined function to queue all endpoint message state changes for later processing.
// QueueEndpointMessagesForLaterProcessing will hold any locks necessary to write to the endpoint message queue
MyGame::QueueEndpointMessagesForLaterProcessing(endpointStateChanges.size(), endpointStateChanges.data());

//
// Elsewhere in another thread/execution context...
//
std::vector<const Party::PartyStateChange*> endpointStateChanges;
// Title-defined function to copy all endpoint message state change pointers that we've so far queued.
// TakeEndpointMessagesOutOfQueue will hold any locks necessary to drain the endpoint message queue.
MyGame::TakeEndpointMessagesOutOfQueue(&endpointStateChanges);

for (const Party::PartyStateChange* stateChange : endpointStateChanges)
{
    auto endpointMessage = static_cast<const Party::PartyEndpointMessageReceivedStateChange*>(stateChange);
    // process the endpoint message
}

err = PartyManager::GetSingleton().FinishProcessingStateChanges(
    static_cast<uint32_t>(endpointStateChanges.size()),
    endpointStateChanges.data());

상태 변경 내용을 유지하는 것은 보다 복잡한 타이틀 논리를 댓가로 타이틀에 PlayFab 파티의 일부 메모리와 리소스를 더 엄격하게 제어할 수 있도록 합니다.

비동기 작업 제어

PlayFab Party와 같은 라이브러리와 해당 타이틀의 핵심 CPU 워크로드 간의 CPU 경합을 피하기 위해 타이틀에서 비동기 작업이 수행되는 시기와 장소를 제어해야 하는 경우가 있습니다.

PlayFab 파티는 비동기 PlayFab 파티 작업을 어떻게 실행할지에 대한 제어 옵션 두 가지를 제시합니다.

  1. 스레드 선호도 제어
  2. 수동으로 비동기 작업 발송

스레드 선호도 제어

기본적으로 비동기 PlayFab 파티 작업은 주의 깊게 제어된 백그라운드 스레드에서 수행됩니다. 일부 타이틀은 CPU 경합을 피하기 위해 이러한 백그라운드 스레드를 어디에 예약할지에 대해 정밀하지 못한 수준의 컨트롤만 있어도 충분합니다.

PlayFab 파티는 이러한 타이틀에 대해 PartyManager::SetThreadAffinityMask()를 제공합니다. 이를 통해 타이틀은 지원되는 플랫폼에서 PlayFab 파티의 백그라운드 스레드에 어떤 CPU 코어를 사용할 지 제어할 수 있습니다. 이러한 방식을 사용하면 타이틀은 경합 없이도 자체 CPU 워크로드를 위해 특정 코어를 전용으로 확보할 수 있습니다.

수동으로 비동기 작업 발송

일부 타이틀의 경우에는 단순히 PlayFab 파티가 어디에서 비동기 작업을 수행하는지를 제어하는 것만으로는 불충분합니다. 이러한 타이틀은 PlayFab 파티가 비동기 작업을 수행하는 시점을 제어하는 것을 함께 요구합니다. PlayFab 파티는 이러한 타이틀에 대해 PartyManager::SetWorkMode()PartyManager::DoWork()를 제공합니다. PartyWorkMode::Manual은 타이틀이일체의 백그라운드 스레드를 사용하지 않고도 PlayFab 파티 라이브러리를 사용할 수 있도록 합니다. 해당 작업의 발생 시점에 대한 직접적인 컨트롤을 통해 타이틀은 선택한 실행 컨텍스트에서 수동으로 파티의 비동기 백그라운드 작업을 실행시킵니다.

Warning

수동으로 비동기 파티 작업을 발송하는 것은 고급 기능입니다. PartyWorkMode::Manual에서도 비동기 파티 작업은 주기적, 정기적으로 발송되어야 합니다. 시간에 맞춰 비동기 작업을 발송하지 않은 경우 PlayFab 파티가 제대로 작동하지 않을 수도 있습니다.

이 작업을 올바르게 발송하는 방법에 대한 안내는 PartyManager::DoWork() 설명서를 주의 깊게 읽어 확인하세요.

수동 발송이 불필요한 경우에는 파티의 기본 비동기 작업 구성이나 스레드 선호도 컨트롤을 대신 사용하는 것이 좋습니다.