플레이어 데이터 및 구매 시스템 구현

*이 콘텐츠는 AI(베타)를 사용해 번역되었으며, 오류가 있을 수 있습니다. 이 페이지를 영어로 보려면 여기를 클릭하세요.

배경

Roblox는 DataStoreService .이러한 API의 가장 일반적인 사용 사례는 저장, 로드 및 복제 플레이어 데이터입니다.즉, 플레이어의 진행 상황, 구매 및 기타 세션 특성과 관련된 데이터는 개별 플레이 세션 간에 지속됩니다.

Roblox의 대부분의 경험은 이러한 API를 사용하여 플레이어 데이터 시스템의 일종을 구현합니다.이러한 구현은 접근 방식이 다르지만 일반적으로 동일한 문제 집합을 해결하려고 합니다.

일반적인 문제

다음은 플레이어 데이터 시스템이 해결하려는 가장 일반적인 문제 중 일부입니다:

  • 메모리 액세스에서: DataStoreService 요청은 비동기적으로 작동하고 속도 제한이 적용되는 웹 요청을 만듭니다.이는 세션 시작 시의 초기 로드에 적합하지만, 게임 플레이일반적인 과정에서 높은 빈도의 읽기 및 쓰기 작업에는 적합하지 않습니다.대부분의 개발자의 플레이어 데이터 시스템은 Roblox 서버에서 메모리에 이 데이터를 저장하여 다음 시나리오의 요청을 제한합니다: DataStoreService

    • 세션 시작 시의 초기 읽기
    • 세션 끝에 최종 쓰기
    • 마지막 쓰기가 실패하는 시나리오를 완화하기 위해 간격을 두고 정기적으로 쓰기
    • 구매를 처리하는 동안 데이터가 저장되도록 쓰기 Writes to ensure data is saved while processing a purchase
  • 효율적인 저장소: 플레이어의 세션 데이터를 단일 테이블에 모두 저장하면 여러 값을 원자적으로 업데이트하고 더 적은 요청으로 동일한 양의 데이터를 처리할 수 있습니다.또한 상호 값 비동기화의 위험을 제거하고 롤백을 이해하기 쉽게 만듭니다.

    일부 개발자는 또한 대형 데이터 구조를 압축하기 위해 사용자 생성 콘텐츠(일반적으로 게임 내 저장)를 사용자 지정 직렬화로 구현합니다.

  • 복제: 클라이언트는 플레이어의 데이터에 정기적으로 액세스해야 합니다(예: UI 업데이트).클라이언트에 플레이어 데이터를 복제하는 일반적인 방법을 사용하면 데이터의 각 구성 요소에 맞춤형 복제 시스템을 만들지 않고도 이 정보를 전송할 수 있습니다.개발자들은 종종 클라이언트에 복제되는 것과 복제되지 않는 것에 대해 선택적으로 선택하길 원합니다.

  • 오류 처리: 데이터스토어에 액세스할 수 없을 때 대부분의 솔루션은 재시도 메커니즘과 '기본값' 데이터로의 백업을 구현합니다.백업 데이터가 나중에 '실제' 데이터를 덮어쓰지 않고 적절하게 플레이어에게 전달되도록 하려면 특별한 주의가 필요합니다.

  • 재시도: 데이터 저장소에 액세스할 수 없을 때 대부분의 솔루션은 재시도 메커니즘과 기본 데이터로의 백업을 구현합니다.대체 데이터가 "실제" 데이터를 덮어쓰지 않도록 특별히 주의하고, 플레이어에게 적절하게 상황을 전달하십시오.

  • 세션 잠금: 단일 플레이어의 데이터가 여러 서버에 로드되고 메모리에 있으면 한 서버에서 오래된 정보를 저장하는 문제가 발생할 수 있습니다.이로 인해 데이터 손실 및 일반 아이템 복제 취약점이 발생할 수 있습니다.

  • 원자성 구매 처리: 항목이 여러 번 손실되거나 수여되지 않도록 구매를 원자적으로 확인, 수여 및 기록합니다.

샘플 코드

Roblox에는 플레이어 데이터 시스템 설계 및 구축을 돕기 위한 참조 코드가 있습니다.이 페이지의 나머지 부분에서는 배경, 구현 세부 정보 및 일반적인 주의 사항을 살펴봅니다.


모델을 Studio에 가져온 후에는 다음 폴더 구조를 볼 수 있어야 합니다:

Explorer window showing the purchasing system model.

아키텍처

이 높은 수준의 다이어그램은 샘플의 주요 시스템과 경험의 나머지 부분에서 코드와 상호 작용하는 방법을 보여줍니다.

An architecture diagram for the code sample.

재시도

클래스: DataStoreWrapper >

배경

As DataStoreService 는 내부에서 웹 요청을 수행하므로, 요청이 성공하도록 보장되지 않습니다.이런 일이 발생하면 DataStore 메서드가 오류를 발생시키고, 처리할 수 있습니다.

데이터 저장소 오류를 처리하려고 시도하면 일반적인 "gotcha"가 발생할 수 있습니다.


local function retrySetAsync(dataStore, key, value)
for _ = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
break
end
task.wait(TIME_BETWEEN_ATTEMPTS)
end
end

이는 일반적인 함수에 대한 완벽하게 유효한 재시도 메커니즘이지만, 요청이 수행되는 순서를 보장하지 않기 때문에 DataStoreService 요청에는 적합하지 않습니다.요청의 순서를 유지하는 것은 상태와 상호작용하기 때문에 DataStoreService 요청에 중요합니다.다음 시나리오를 고려하십시오:

  1. 요청 A는 키 값 K를 1로 설정하도록 만듭니다.
  2. 요청이 실패하여 2초 후에 재시도가 예약됩니다.
  3. 재시도가 발생하기 전에, 요청 B는 K 값을 2로 설정하지만, 요청 A의 재시도는 즉시 이 값을 덮어쓰고 K 값을 1로 설정합니다.

비록 UpdateAsync 가 키의 값의 최신 버전에서 작동하더라도, UpdateAsync 요청은 무효한 임시 상태를 피하기 위해 여전히 처리되어야 하며(예를 들어, 코인 추가가 처리되기 전에 코인을 뺄 때 음의 코인이 발생함).

플레이어 데이터 시스템은 키별로 처리되는 보장된 재시도를 제공하는 새로운 클래스 DataStoreWrapper)를 사용합니다.

접근법

An process diagram illustrating the retry system

DataStoreWrapperDataStore 방법에 해당하는 메서드를 제공합니다: DataStore:GetAsync() , DataStore:SetAsync() , DataStore:UpdateAsync()DataStore:RemoveAsync() .

이 메서드는 호출될 때:

  1. 요청을 큐에 추가합니다.각 키에는 요청이 순차적으로 처리되고 시리즈로 처리되는 자체 큐가 있습니다.요청하는 스레드는 요청이 완료될 때까지 기다립니다.

    이 기능은 코루틴 기반 태스크 스케줄러 및 속도 제한기인 ThreadQueue에 기반합니다.약속을 반환하는 대신, ThreadQueue 작업이 완료될 때까지 현재 스레드를 생성하고 실패하면 오류를 발생시킵니다.이는 비동기적인 Luau 패턴의 표현에 더 일관적입니다.

  2. 요청이 실패하면 구성 가능한 지수 백오프로 다시 시도합니다.이러한 재시도는 ThreadQueue에 제출된 콜백의 일부이므로, 이 키의 다음 요청이 시작되기 전에 완료되도록 보장됩니다.

  3. 요청이 완료되면 요청 메서드는 success, result 패턴으로 반환됩니다

DataStoreWrapper 또한 지정된 키에 대한 대기열 길이를 가져오고 오래된 요청을 지우는 메서드를 노출합니다.후자의 옵션은 서버가 종료되고 가장 최근 요청만 처리할 시간이 없는 시나리오에서 특히 유용합니다.

주의 사항

DataStoreWrapper 극단적인 시나리오 외부에서는 모든 데이터 저장소 요청이 (성공적으로 또는 그렇지 않으면) 중복되더라도 완료될 수 있도록 허용해야 한다는 원칙을 따릅니다.새 요청이 발생하면 새로운 요청이 시작되기 전에 오래된 요청이 대기열에서 제거되지 않고 대신 새 요청이 완료될 수 있습니다.이 논리는 플레이어 데이터에 대한 특정 도구가 아닌 일반적인 데이터 저장소 유틸리티로서 이 모듈의 적용성에 근거하며 다음과 같습니다: The rationale for this is rooted in this module's applicability as a generic data store utility rather than a specific tool for player data, and is as follows:

  1. 요청을 큐에서 제거할 수 있는 적절한 규칙 집합을 결정하기가 어렵습니다. 다음 큐를 고려하십시오:

    Value=0, SetAsync(1), GetAsync(), SetAsync(2)

    예상되는 동작은 가 반환하는 것이 이지만, 가장 최근의 요청으로 인해 줄어들어 큐에서 제거되면 반환되는 것은 입니다.

    논리적 진행은 새로운 쓰기 요청이 추가될 때, 가장 최근의 읽기 요청까지만 오래된 요청을 잘라내는 것입니다.UpdateAsync() , 가장 일반적인 작업(그리고 이 시스템에서 사용되는 유일한 작업)은 읽기와 쓰기 모두를 수행할 수 있으므로 여기에 불필요한 복잡성을 추가하지 않고도 디자인 내에서 화해하기가 어려울 것입니다.

    DataStoreWrapper 은 요청 UpdateAsync() 이 읽고/쓰기에 허용되었는지 여부를 지정해야 할 수 있지만, 세션 잠금 메커니즘(나중에 자세히 설명)으로 인해 시간 앞서 결정할 수 없는 우리의 플레이어 데이터 시스템에는 적용할 수 없습니다.

  2. 큐에서 제거되면 어떻게 처리해야 하는지에 대한 직관적인 규칙을 결정하기가 어렵습니다.DataStoreWrapper 요청이 만들어지면 현재 스레드가 완료될 때까지 릴리스됩니다.대기열에서 만료된 요청을 제거하면 false, "Removed from queue" 를 반환할지 아니면 활성 스레드를 결코 반환하고 삭제하지 않을지 결정해야 합니다.두 접근법 모두 자체 단점이 있으며 소비자에게 추가 복잡성을 전가합니다.

궁극적으로 우리의 견해는 간단한 접근 방식(모든 요청 처리)이 여기에서 선호되고 세션 잠금과 같은 복잡한 문제에 접근할 때 더 명확한 환경을 만들어 준다는 것입니다.이 예외는 동안 발생합니다 DataModel:BindToClose() , 대기열을 지우는 것이 모든 사용자의 데이터를 실시간으로 저장하는 데 필수가 되었고 값 개별 함수 호출의 반환이 더 이상 진행 중인 문제가 아닙니다.이것을 고려하여 우리는 skipAllQueuesToLastEnqueued 메서드를 노출합니다.자세한 내용은 플레이어 데이터를 참조하십시오.

세션 잠금

클래스: SessionLockedDataStoreWrapper >

배경

플레이어 데이터는 서버의 메모리에 저장되며 필요한 경우 기본 데이터 저장소에서만 읽고 쓰입니다.웹 요청 없이 메모리 내 플레이어 데이터를 즉시 읽고 업데이트할 수 있으며 DataStoreService 제한을 초과하지 않습니다.

이 모델이 의도한 대로 작동하려면 한 번에 하나 이상의 서버가 플레이어의 데이터를 메모리에 로드할 수 없어야 합니다.For this model to work as intended, it is imperative that no more than one server is able to load a player's data into memory from the DataStore at the same time.

예를 들어, 서버 A가 플레이어의 데이터를 로드하면 서버 B는 최종 저장 중에 서버 A가 그 데이터에 대한 잠금을 해제할 때까지 해당 데이터를 로드할 수 없습니다.잠금 메커니즘이 없으면 서버 B는 서버 A가 메모리에 있는 최신 버전을 저장할 기회가 있기 전에 데이터 저장소에서 만료된 플레이어 데이터를 로드할 수 있습니다.그런 다음 서버 A가 서버 B가 오래된 데이터를 로드한 후 새로운 데이터를 저장하면 서버 B는 다음 저장 중에 그 새로운 데이터를 덮어쓸 것입니다.

Roblox는 클라이언트가 한 번에 하나의 서버에만 연결할 수 있도록 허용하지만, 한 세션의 데이터가 항상 다음 세션이 시작되기 전에 저장된다고 가정할 수는 없습니다.플레이어가 서버 A를 떠날 때 발생할 수 있는 다음 시나리오를 고려하십시오:

  1. 서버 A는 데이터를 저장하기 위한 DataStore을 만들지만 요청이 실패하고 성공적으로 완료하려면 여러 번 재시도해야 합니다.재시도 기간 동안 플레이어는 서버 B에 합류합니다.
  2. 서버 A가 동일한 키에 너무 많은 UpdateAsync() 호출을 하고 제한을 받습니다.최종 저장 요청이 큐에 배치됩니다.요청이 대기열에 있는 동안 플레이어가 서버 B에 합류합니다.
  3. 서버 A에서, 플레이어의 데이터가 저장되기 전에 PlayerRemoving 이벤트에 연결된 일부 코드가 생성됩니다.이 작업이 완료되기 전에 플레이어가 서버 B에 합류합니다.
  4. 서버 A의 성능이 저하되어 플레이어가 서버 B에 참여할 때까지 최종 저장이 지연됩니다.

이러한 시나리오는 드문 일이어야 하지만, 특히 플레이어가 한 서버에서 연결을 끊고 빠르게 다른 서버에 연결(예를 들어, 순간이동하는 동안)하는 상황에서 발생합니다(예: 순간이동 중).일부 악성 사용자는 지속되지 않고 작업을 완료하기 위해 이 동작을 남용하려고 시도할 수도 있습니다.이는 플레이어가 거래할 수 있는 게임에서 특히 영향력이 있으며 아이템 복제 취약점의 일반적인 원천입니다.

세션 잠금은 플레이어의 DataStore 키가 서버에서 처음 읽혀지도록 하여 서버가 동시에 키의 메타데이터 내에 잠금을 기록하도록 합니다. 동일한 UpdateAsync() 호출에서.이 잠금 값이 다른 서버가 키를 읽거나 쓰려고 할 때 존재하면 서버가 진행되지 않습니다.

접근법

An process diagram illustrating the session locking system

SessionLockedDataStoreWrapperDataStoreWrapper 클래스 주위의 메타 래퍼입니다. 는 대기열 및 재시도 기능을 제공하며, 세션 잠금으로 보완됩니다.

모든 요청( 또는 또는 여부에 관계없이)을 통해 을 통과합니다.이는 UpdateAsync 가 원자적으로 읽고 쓰기 가능한 키를 허용하기 때문입니다.변환 콜백에서 nil를 반환하여 읽은 값에 따라 쓰기를 포기할 수도 있습니다.

각 요청에 대해 변환 함수가 UpdateAsync로 전달되면 다음 작업이 수행됩니다:

  1. 키가 액세스할 수 있는지 확인하고, 그렇지 않은 경우 작업을 포기합니다."액세스할 수 있음"은 다음을 의미합니다:"안전한 액세스"는:

    • 키의 메타데이터 개체에는 잠금 만료 시간 이전에 업데이트된 인식되지 않은 LockId 값이 포함되지 않습니다.이는 다른 서버에서 배치한 잠금을 존중하고 만료된 경우 해당 잠금을 무시하는 데 사용됩니다.

    • 이 서버가 키의 메타데이터에 자체 LockId 값을 이전에 배치했다면 이 값은 여전히 키의 메타데이터에 있습니다.이는 다른 서버가 이 서버의 잠금(만료 또는 강제로)을 넘겨받아 나중에 해제한 상황을 설명합니다.대체로 표현하면, 비록 LockIdnil 이더라도 다른 서버는 여전히 키를 잠긴 시간 이후에 잠금을 교체하고 제거할 수 있었습니다.

  2. 는 요청한 소비자의 작업을 수행합니다. 예를 들어, 는 로 번역됩니다.

  3. 요청에 전달된 매개변수에 따라 UpdateAsync 키를 잠그거나 잠금 해제합니다:

    1. 키가 잠겨야 하는 경우, UpdateAsync는 키의 메타데이터에서 LockId를 GUID로 설정합니다.이 GUID는 서버에서 메모리에 저장되어 다음에 키에 액세스할 때 확인할 수 있습니다.서버에 이 키에 대한 잠금이 이미 있으면 변경하지 않습니다.또한 키를 다시 액세스하지 않으면 잠금 만료 시간 내에 잠금을 유지하도록 경고하는 작업을 일정에 추가합니다.

    2. 키가 잠금 해제되어야 하는 경우, UpdateAsync 키의 메타데이터에서 LockId를 제거합니다.

사용자 지정 재시도 처리기가 기본 DataStoreWrapper에 전달되어 세션이 잠겨 작업이 중단되었기 때문에 1단계에서 작업이 다시 시도됩니다.

사용자 지정 오류 메시지도 소비자에게 반환되어 플레이어 데이터 시스템이 클라이언트에 대한 세션 잠금 경우 대체 오류를 보고할 수 있습니다.

주의 사항

세션 잠금 모드는 항상 키에 대한 잠금을 해제하는 서버에 의존하며, 작업이 완료되면 해당 키에 대한 잠금을 항상 해제합니다.이는 항상 PlayerRemoving 또는 BindToClose()에서 마지막 쓰기의 일부로 키를 잠금 해제하는 명령을 통해 발생해야 합니다.

그러나 특정 상황에서 잠금 해제가 실패할 수 있습니다. 예를 들어:

  • 서버가 충돌했거나 DataStoreService 키에 액세스하려는 모든 시도가 불가능했습니다.
  • 논리 오류 또는 유사한 버그로 인해 키를 잠금 해제하는 명령이 생성되지 않았습니다.

키에 대한 잠금을 유지하려면 메모리에 로드된 동안 정기적으로 액세스해야 합니다.이는 일반적으로 대부분의 플레이어 데이터 시스템에서 배경에서 실행되는 자동 저장 루프의 일부로 수행되지만, 수동으로 수행해야 하는 경우에도 이 시스템은 refreshLockAsync 메서드를 노출합니다.

잠금 만료 시간이 업데이트되지 않고 초과되면 모든 서버가 잠금을 인수할 수 있습니다.다른 서버가 잠금을 차지하면 현재 서버가 키를 읽거나 쓰려고 시도하더라도 새 잠금을 설정하지 않으면 실패합니다.

개발자 제품 처리

단일: ReceiptHandler

배경

콜백은 구매 완료 시기를 결정하는 중요한 작업을 수행합니다.ProcessReceipt는 매우 특정한 시나리오에서 호출됩니다.보장 집합에 대해서는 MarketplaceService.ProcessReceipt을 참조하십시오.

구매를 "처리"하는 정의가 경험 간에 달라도 우리는 다음 기준을 사용합니다

  1. 구매는 이전에 처리되지 않았습니다.

  2. 구매는 현재 세션에 반영됩니다.

  3. 구매가 DataStore 저장되었습니다.

    모든 구매, 심지어 일회성 소모품도, 모두 DataStore에 반영되어 사용자의 구매 기록이 세션 데이터와 함께 포함됩니다.

반환하기 전에 다음 작업을 수행해야 합니다 PurchaseGranted :

  1. PurchaseId가 처리된 것으로 이미 기록되지 않았는지 확인하십시오.
  2. 플레이어의 메모리 내 플레이어 데이터에서 구매를 수여합니다.
  3. 플레이어의 메모리 내 플레이어 데이터에서 처리된 PurchaseId를 기록합니다.
  4. 플레이어의 메모리 내 플레이어 데이터를 DataStore 쓰십시오.

세션 잠금은 더 이상 다음 시나리오에 대해 걱정할 필요가 없으므로 이 흐름이 간소화됩니다.

  • 현재 서버의 메모리 내 플레이어 데이터가 오래되어 검증하기 전에 DataStore에서 최신 값을 가져와야 하는 경우 역사 PurchaseId의 최신 값
  • 다른 서버에서 실행되는 동일한 구매에 대한 콜백, 두 가지 모두 읽고 쓰는 PurchaseId 기록 및 업데이트된 플레이어 데이터를 구매로 반영하여 레이스 조건을 방지하기 위해 저장해야 하는

세션 잠금은 플레이어의 DataStore에 쓰기 시도가 성공하면 이 서버에서 데이터를 로드하고 저장하는 동안 다른 서버에서 플레이어의 DataStore에 성공적으로 읽거나 쓰지 않는다는 것을 보장합니다.간단히 말해, 이 서버의 메모리 플레이어 데이터는 사용 가능한 최신 버전입니다.몇 가지 제한 사항이 있지만 이 동작에는 영향을 주지 않습니다.

접근법

ReceiptProcessor 의 주석은 접근 방식을 설명합니다:

  1. 플레이어의 데이터가 현재 이 서버에 로드되고 오류 없이 로드되었는지 확인합니다.

    이 시스템은 세션 잠금을 사용하므로 이 검사는 메모리에 있는 데이터가 최신 버전인지 확인합니다.

    플레이어의 데이터가 아직 로드되지 않았다면(플레이어가 게임에 참여할 때 예상되는 일) 플레이어의 데이터가 불러오다때까지 기다립니다.시스템은 데이터가 로드되기 전에 플레이어가 게임을 떠나는 것을 감지하여 무기한 양보하지 않고 이 서버에서 이 구매를 위해 다시 이 콜백이 호출되지 않도록 차단합니다.

  2. PurchaseId가 플레이어 데이터에 이미 처리된 것으로 기록되지 않았는지 확인하십시오.

    세션 잠금으로 인해 시스템의 메모리에 있는 PurchaseIds 배열이 가장 최신 버전입니다.PurchaseId 가 처리되어 로드되거나 저장된 값에 반영되면 DataStore 에 반환합니다.If the is recorded as processed and reflected in a value that has been loaded to or saved to the , return PurchaseGranted .처리된 것으로 기록되지만 반영되지 않은 경우, 에 반환합니다.

  3. 이 서버에서 플레이어 데이터를 로컬로 업데이트하여 구매를 "보상"합니다.

    ReceiptProcessor는 일반적인 콜백 접근법을 취하고 각 DeveloperProductId에 대해 다른 콜백을 할당합니다.

  4. 이 서버에서 플레이어 데이터를 로컬로 업데이트하여 PurchaseId 저장합니다.

  5. 요청을 제출하여 메모리에 저장된 데이터를 DataStore에 저장하고 요청이 성공하면 PurchaseGranted를 반환합니다. 그렇지 않으면 NotProcessedYet을 반환합니다.

    이 저장 요청이 성공하지 않으면 플레이어의 메모리 세션 데이터를 저장하는 나중의 요청이 여전히 성공할 수 있습니다.다음 ProcessReceipt 호출 중에 단계 2는 이 상황을 처리하고 PurchaseGranted 로 반환합니다.

플레이어 데이터

싱글턴: PlayerData.Server , PlayerData.Client

배경

게임 코드에 대한 인터페이스를 제공하여 플레이어 세션 데이터를 동기적으로 읽고 쓰는 모듈은 Roblox 경험에서 일반적입니다.이 섹션에서는 PlayerData.ServerPlayerData.Client를 다룹니다.

접근법

PlayerData.ServerPlayerData.Client 팔로잉처리합니다:

  1. 플레이어의 데이터를 메모리에 로드하여 불러오다수 없는 경우를 처리하고, 이에 대한 예외 처리
  2. 서버 코드가 플레이어 데이터를 쿼리하고 변경할 수 있는 인터페이스 제공
  3. 클라이언트 코드가 액세스할 수 있도록 플레이어의 데이터를 클라이언트에 복제
  4. 오류 대화 표시를 위해 클라이언트에 로딩 및/또는 저장 오류 재현
  5. 플레이어의 데이터를 정기적으로 저장하여, 플레이어가 떠날 때와 서버가 종료될 때

플레이어 데이터 로드

An process diagram illustrating the loading system
  1. SessionLockedDataStoreWrapper 데이터 상점getAsync 요청을 합니다.

    이 요청이 실패하면 기본 데이터가 사용되고 프로필이 "오류"로 표시되어 나중에 데이터 저장소에 쓰지 않도록 합니다.

    대안 옵션은 플레이어를 킥하는 것이지만, 플레이어가 기본 데이터로 플레이하고 발생한 내용에 대한 메시징을 지우는 것이 아니라 경험에서 제거하는 것을 권장합니다.

  2. 초기 페이로드가 로드된 데이터와 오류 상태(있는 경우)를 포함하는 PlayerDataClient로 전송됩니다.

  3. 플레이어에 대해 waitForDataLoadAsync를 사용하여 생성된 모든 스레드가 재시작됩니다.

서버 코드에 대한 인터페이스 제공

  • PlayerDataServer는 동일한 환경에서 실행되는 모든 서버 코드에서 요구하고 액세스할 수 있는 싱글턴입니다.
  • 플레이어 데이터는 키와 값의 사전으로 구성됩니다.서버에서는 setValue, getValue, updateValueremoveValue 메서드를 사용하여 이러한 값을 조작할 수 있습니다.이러한 메서드는 모두 양보 없이 동기적으로 작동합니다.
  • hasLoadedwaitForDataLoadAsync 메서드는 액세스하기 전에 데이터가 로드되도록 하기 위해 사용할 수 있습니다.다른 시스템이 시작되기 전에 로딩 화면에서 한 번 이 작업을 수행하여 클라이언트에서 데이터와 상호작용하기 전에 로드 오류를 확인할 필요가 없도록 하는 것이 좋습니다.
  • A hasErrored 메서드는 플레이어의 초기 로드가 실패하여 기본 데이터를 사용하는지 쿼리할 수 있습니다.플레이어가 구매를 할 수 있도록 허용하기 전에 이 메서드를 확인하십시오, 구매는 성공적으로 불러오다않으면 데이터에 저장할 수 없습니다.
  • A playerDataUpdated 신호는 플레이어의 데이터가 변경될 때마다 player , keyvalue 발생합니다.개별 시스템은 이것에 구독할 수 있습니다.

클라이언트에 변경 사항 복제

  • 플레이어 데이터의 변경 내용은 PlayerDataServer 에서 PlayerDataClient 로 복제되지만, 해당 키가 setValueAsPrivate를 사용하여 비공개로 표시되지 않은 한
    • setValueAsPrivate는 클라이언트에 보내지면 안 되는 키를 나타내는 데 사용됩니다
  • PlayerDataClient 은 키 값을 가져오는 메서드(가져오기)와 업데이트될 때 발생하는 신호(업데이트)를 포함합니다.A hasLoaded 메서드와 A loaded 신호도 포함되어 클라이언트가 시스템을 시작하기 전에 데이터를 로드하고 복제할 수 있습니다
  • PlayerDataClient는 동일한 환경에서 실행되는 모든 클라이언트 코드에서 요구하고 액세스할 수 있는 싱글턴입니다

클라이언트에 오류 복제

  • 플레이어 데이터를 저장하거나 로드할 때 발생하는 오류 상태가 PlayerDataClient로 복제됩니다.
  • 이 정보에는 getLoadErrorgetSaveError 메서드와 loadedsaved 신호가 함께 사용됩니다.
  • 오류는 두 가지 유형이 있습니다: (요청 실패) 및 (세션 잠금 참조 ).
  • 이 이벤트를 사용하여 클라이언트 구매 프롬프트를 비활성화하고 경고 대화 상자를 구현합니다. 이 이미지에는 예제 대화가 표시됩니다:
A screenshot of an example warning that could be shown when player data fails to load

플레이어 데이터 저장

A process diagram illustrating the saving system
  1. 플레이어가 게임을 떠나면 시스템은 다음 단계를 수행합니다:

    1. 플레이어의 데이터를 데이터 상점쓸 수 있는지 확인합니다.안전하지 않은 시나리오에는 플레이어의 데이터를 불러올 수 없거나 아직 불러오는 중인 경우가 포함됩니다.
    2. 현재 메모리 데이터 값을 데이터 저장소에 기록하고 세션 잠금을 제거하기 위해 SessionLockedDataStoreWrapper를 통해 요청을 만들고 완료되면 세션 잠금을 제거합니다.
    3. 플레이어의 데이터(메타데이터 및 오류 상태 등 기타 변수)를 서버 메모리에서 지우고
  2. 정기적인 루프에서 서버는 각 플레이어의 데이터를 데이터 저장소에 씁니다(저장할 수 있는 경우).이 환영 중복은 서버 오류 발생 시 손실을 완화하고 세션 잠금을 유지하는 데도 필요합니다.

  3. 서버 종료 요청이 수신되면 다음이 BindToClose 콜백에서 발생합니다:

    1. 플레이어의 데이터를 서버에 저장하도록 요청이 수행되며, 플레이어가 서버를 떠날 때 일반적으로 진행되는 프로세스를 따릅니다.이러한 요청은 병렬로 수행되며, BindToClose 콜백은 완료하기까지 30초밖에 걸리지 않습니다.
    2. 저장을 가속화하기 위해 각 키의 큐에 있는 모든 다른 요청이 기본 DataStoreWrapper(참조 재시도)에서 지워집니다.
    3. 콜백은 모든 요청이 완료될 때까지 반환되지 않습니다.