背景
Roblox 提供一套 API 可以通過 DataStoreService 與數據存取商店交互。這些 API 的使用案例最常見是儲存、載入和複製 玩家資料 。這就是,與玩家的進度、購買和其他遊戲特性相關的數據。
大多數 Roblox 體驗使用這些 API 來實現某些玩家數據系統的一些形式。這些實現方式不同,但一般來說尋求解決相同的問題。
常見問題
下面是一些最常見的玩家資料系統嘗試解決的問題:
在記憶體存取中: DataStoreService 要求使用者伺服器在線上時執行的網路請求,並且受到速率限制。這適合在開始遊遊玩時的初始載入,但不適合在正常遊戲中的高速讀取和寫入操作。大多數開發者
- 會在會話開始時初始読作業
- 在作業議結束時的最後寫入
- 期間寫入以間隔來控制最終寫入失敗的情況
- 寫入以確保購買過程中的資料已儲存
高效存取: 將所有玩家的遊戲資訊儲存在單一桌子中,讓您更能隨時更新多個值的原子值,並且在更少的請求中處理相同的數據。它還移除了異值的風險,並且使捲回更容易理解。
一些開發人員也實現自訂 serialization 來壓縮大型資料結構 (通常為了在遊戲中生成的用戶生成內容)。
複製: 客戶端需要常規存取玩家的資料 (例如更新 UI)。一般來說,複製玩家資料到客戶端的方法可以讓您傳輸此信息,而不需要為每個資料元素創建專用複製系統。開發人員通常想要選擇地將玩家資料複製到客戶端。
錯誤處理: 當無法存取資料儲存時,大多數解決方案會實現重試機制和恢復到預設資料。特別小心,以確保恢復資料不會寫滿「真實」資料,並且這一點正確傳達給玩家。
重試: 當資料儲存無法存取時,大多數解決方案會部署重試機制和預設數據。請特別小心,以確保預設數據不會隨後覆蓋「真實」數據,並且對玩家合理地通知情況。
會話鎖定: 如果單一玩家的資料在多個服務器上載入和在內存中,會發生問題,在哪一個服務器上儲存過時的資訊。這可能會導致資料損失和常見項目重複漏洞。
原子購買處理: 驗證、獎勵、和記錄原子購買以防止物品丟失或被多次獎勵。
範例代碼
Roblox 有參考代碼可以幫助您設計並建設玩家數據系統。這個頁面的剩下會考慮背景、實現細節和一般 caveats。
你在 Studio 匯入模型後,應該會看到以下資料樣樣:
建築
這個高層圖示範了樣本中的關鍵系統和它們如何與代碼在剩下的體驗中交互。
重試
背景
作為 DataStoreService 在 DataStore 下發生網路請求時,它的請求不保證成功。當發生這樣的時候, Class.GlobalDataStore|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 請求的需求,因為它們與狀態交互。考慮以下場景:
- 要求 A 設定鑰匙值 K 為 1。
- 請求失敗,因此預定在 2 秒後執行重試。
- 在重試發生之前,請求 B 設置 K 值為 2,但請求 A 的重試立即覆蓋此值並將 K 設置為 1。
雖然 UpdateAsync 在鑰鍵值的最新版本上運行,但 UpdateAsync 請求仍必須處理,以避免無效的暫時狀態 (例如, 一次購買會從購買子減去金幣,導致負金幣)。
我們的玩家資料系統使用新的類別, DataStoreWrapper ,這提供讓重試發生的機率,並保證它們是按鍵處理的。
方法
DataStoreWrapper 提供與 DataStore 方法相對應的方法:DataStore:GetAsync() 、0> Class.GlobalDataStore:SetAsync()|DataStore:
這些方法,當呼叫時:
將要求加到佇列。每個鑰匙都有自己的佇列,在要求完成時按照順序處理。請求完成後,請從佇列中移除。
此功能基於 ThreadQueue 類,是一個基於程式程序的任務程序表,它是一個程式程序的程度表和速度限制器。而不是返回一個促銷,ThreadQueue 會在操作完成後返回當前線程直到操作完成並且發生錯誤,如果錯誤發生錯誤。這與 idiomatic 非同步 Lua 模式更一致。
如果要求失敗,它會重試,配置可變的指數減去。這些重試形成在 ThreadQueue 提交的回調中,因此它們保證在這個鑰匙的下一個要求之前完成。
當要求完成時,請求方法會以 success, result 的格式返回
DataStoreWrapper 也會揭示方法來取得指定鍵的排隊長度和清除過時的請求。 後者選項對於涉及服務器關閉並且沒有時間處理任何但最新的請求的情況尤為有用。
洞穴
DataStoreWrapper 按照極端情況下,每個資存要求的完成 (成功或否則) 都應該允許,即使最近的要求使它過時。當新的請求發生時,舊的請求不會從排隊中移除,而是允許在新的請求開始前完成。這是因為此模組的申請為一個適
很難決定要從排隊中移除的請求安全時間。請考慮以下排隊:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
預期的行為是,GetAsync() 將返回 1,但如果我們移除最近一個 SetAsync() 請求,它將返回 1> 01>。
論理進度是,當新的寫入請求添加時,只有在最近的閱取請邀請添加時,才會審核過時的請求。 UpdateAsync() , 是使用此系統中最常見的操作(並且只有這個系統使用),可以閱取和寫入,因此在這個設計中添加額外的複雜性很難。
DataStoreWrapper 可能需要您指定是否允許 UpdateAsync() 請求閱取和/或寫入,但它對於我們的玩家資料系統來說不可能在時間到達上限時(由於會在更長時間後才會發生)。
一旦從排隊中移除,就很難決定有關如何這個應該被處理。當DataStoreWrapper 要求時,當前線程會被生成直到完成。如果我們從排隊中移除了閣下的請求,我們就必須決定是否 true, "
最終,我們的觀點是,簡單的方法(處理每個請邀請)在這裡更優惠,並且在處理複雜問題時提供更清晰的環境。唯一的例子是在 DataModel:BindToClose() 中,清除佇列
鎖定會話
類別: SessionLockedDataStoreWrapper >
背景
玩家資料存儲在服務器上的記憶中,並且僅在必要時從記憶中寫入和寫出基礎數據存儲。您可以立即在記憶中閱讀和更新玩家資料,而不需要網路請求,並且避免超出 DataStoreService 限制。
為此模型正常運行,必須要確保不超過一個服務器能夠在 DataStore 中同時載入玩家的資料。
舉例來說,如果伺服器 A 載入玩家的資料,伺服器 B 無法在伺服器 A 釋放鎖定時,直到伺服器 A 釋放鎖定,才能載入玩家的資料。沒有鎖定機制的話,伺服器 B 就會在伺服器 A 載入最新版本的資料後,就會覆�
雖然 Roblox 只允許一個客戶端連接到一個服務器,但你不能假設任何資料在一個會話結束時會被儲存,直到下一個會話開始。考慮到下面的場景,當玩家離開服務器 A 時發生:
- 服務器 A 發出 DataStore 請求儲存他們的資料,但請求失敗並需要幾次重試才能成功完成。在重試期間,玩家加入服務器 B。
- 服務器 A 發生了太多 Class.GlobalDataStore:UpdateAsync()|UpdateAsync() 的呼叫,而且會獲得限制。最後儲存請求會被放置在佇列中。當請求在佇列中時,玩家會加入伺服器 B。
- 在服務器 A 上,一些與 PlayerRemoving 事件相關的代碼會在玩家的資料儲存之前產生。在此操作完成之前,玩家將會加入服務器 B。
- 服務器 A 的性能已降低至程度,直到玩家加入服務器 B 為止。
這些場景應該很稀有,但它們會發生,例如在玩家從一個伺服器切斷並連接到另一個伺服器的情況下 (例如,當玩家傳送)。一些惡意的用戶可能會嘗試濫用此行為來完成無需持續的操作(例如,傳送時)。這可能會對遊戲造成特別嚴重的影響,例如滿級濫用
會議鎖定地址這個漏洞,確保當玩家的 DataStore 鍵首先由服務伺服器閱讀時,服務器原子性寫入鍵的內部資料庫內的鎖定。如果此鎖值在任何其他服務器嘗試閱讀或寫入鍵時,服務器將不會
方法
SessionLockedDataStoreWrapper 是 DataStoreWrapper 類的 meta 包。 DataStoreWrapper 提供疊加排隊和重試功能,而 0> SessionLockedDataStore0> 則提供會話鎖定。
SessionLockedDataStoreWrapper 傳送每個
變身功能傳入 UpdateAsync 為每個要求執行以下操作:
驗證鑰匙是否安全以使用權 通行權 存取,如果不是,則會放棄操作。「安全以使用權 通行權 存取」意味著:
鑰鍵的元數資料對象並不包含位於鑰匙發出時間少於鑰匙有效時間的後方的無認識的 LockId 值。這是因為尊重其他伺服器放置的鑰匙並且忽略鑰匙在過期後的時間。
如果此伺服器已以前將自己的 LockId 值設置在鍵鍵的鑰匙值中,則此值仍在鑰匙的鑰值中。這是因為另一個服務器已經將此服務器的鎖定 (由過期或由強制) 解鎖,然後稍後重新��
Class.GlobalDataStore:UpdateAsync()|UpdateAsync 執行 DataStore 操作要求 SessionLockedDataStoreWrapper 所要求的。例如,0> Class.GlobalDataStore:GetAsync()|GetAsync()0> 翻譯成 UpdateAsync3>。
依據傳入要邀請的參數,UpdateAsync 鎖定或解鎖鑰鍵:
如果鑰匙是鎖定的,UpdateAsync 將鑰鍵的 LockId 設置在鑰鍵的標籤中,以便在下次存取鑰匙時檢查。如果服務器已有此鎖定
如果鑰匙需要解鎖,UpdateAsync 會移除鑰匙的 LockId 在鑰鍵的元數據中。
將自訂重試處理器傳入底部的 DataStoreWrapper ,以便在步驟 1 由於階段被鎖定而導致操作的重試。
還會向消費者傳回自訂錯誤訊息,讓玩家資料系統在客戶端鎖定會話的情況下報告另一個錯誤。
洞穴
鎖定程式的執行,會依照鍵完成後將鍵釋放在 PlayerRemoving 或 BindToClose() 中。這會通過指令解鎖鍵作為最後寫入的一部分來完成。
但鎖定可能會在某些情況下失敗。例如:
- 服務器崩溃或 DataStoreService 無法對所有嘗試存取鑰鍵的嘗試進行操作。
- 因為論理錯誤或類似錯誤,沒有提供鑰匙的解鎖說明。
要維持鑰鍵的鎖定,您必須正常存取它,直到它在記憶體中載入為止。這通常會在玩家資料系統的背景中執行的自動儲存循環中執行,但此系統還會在需要手動執行的情況下顯示 refreshLockAsync 方法。
如果鎖的有效時間已過期,而鎖尚未更新,則任何服務器都可以佔領鎖。如果不同的服務器佔領鎖,現有服務器的嘗試無法閱取或寫入鑰匙,直到它建立新的鎖。
開發者產品處理
Singleton: ReceiptHandler
背景
ProcessReceipt 回歸器執行決定購買時間的關鍵工作。ProcessReceipt 在特定場景中稱為 MarketplaceService.ProcessReceipt 。
雖然「處理」購買體驗的方式可能會在體驗間不同,但我們會使用以下標準
購買尚未處理。
購買會在當前會作業中反映。
這需要執行以下操作,然後才能返回 PurchaseGranted :
- 確認 PurchaseId 尚未記錄為已處理。
- 在玩家的內存資料中獎勵購買。
- 將 PurchaseId 記錄在玩家的內存玩家資料中處理。
- 將玩家的內存玩家資料寫到 DataStore 中。
鎖定會議室,讓您不再需要擔心以下情況:
- 當前伺服器中的玩家資料可能會過時,需要從 DataStore 中取得最新值,然後才能確認 PurchaseId 歷史
- 在其他伺服器上執行的相同購買,需要您閱讀並寫入 PurchaseId 歷史,並儲存更新的玩家資料以反映原子的更新,以防止賽車條件
會議鎖定可以確保,如果嘗試寫入玩家的 DataStore 成功,沒有其他服務器在此伺服器務器載入和儲存的玩家的 DataStore 之間,在此服務器中的內存玩家資料是最新版本。 有一些
方法
ReceiptProcessor 中的評論概述了方法:
確認玩家的資料已載入此伺服器,並且沒有任何錯誤地載入。
因為此系統使用會話鎖定,這個檢查也會確認內存資料是否為最新版本。
如果玩家的資料尚未載入 (這是玩家加入遊戲時所期望的情況),請等待玩家的資料載入。 系統也會在玩家離開遊戲之前偵測到玩家是否離開遊戲,因為它不應該永久運行並且重新啟動此呼叫,因此在這個伺服器上重新啟動此購買。
確認 PurchaseId 已不是玩家資料中已處理的狀態。
由於會話鎖定,系統內存中的 PurchaseIds 列表是最新版本。如果 PurchaseId 被記錄為已處理並反射在值中,
在此伺服器中本地更新玩家資料,以「獎勵」購買。
ReceiptProcessor 使用通用回撥方法,並為每個 DeveloperProductId 分配不同的回撥。
在此伺服器中本地更新玩家資料,以存儲 PurchaseId 。
提交要求將內存資料儲存到DataStore,並且在要求成功的情況下返回PurchaseGranted。如果未成功,將返回NotProcessedYet。
如果此儲存請求無法成功,之後儲存玩家內存資料的請求仍然可能成功。在下一個 ProcessReceipt 呼叫中,步驟 2 處理此情況,並且返回 PurchaseGranted。
玩家資料
Singletons: PlayerData.Server PlayerData.Client
背景
提供遊戲代碼接口以同步閱取並寫入玩家會話數據的模組在 Roblox 體驗中常見。此部分包括 PlayerData.Server 和 PlayerData.Client。
方法
PlayerData.Server 和 PlayerData.Client 處理以下內容:
- 載入玩家的資料到記憶體,包括處理失敗的情況
- 提供一個可以查詢和變更玩家資料的服務器代碼介面
- 將玩家資料的變更複製到客戶端,以便客戶端程式碼可以存取
- 重複載入和/或儲存錯誤,以便客戶端能夠顯示錯誤對話框
- 在玩家離開時,以及伺服器關閉時,保存玩家的資料
載入玩家數據
SessionLockedDataStoreWrapper 會向資料存商店 商家發出一個 getAsync 請求。
如果此請求失敗,將使用預設資料,並且標記為 "錯誤" 以確保它不會寫入資料存儲。
另一個選擇是踢玩家,但我們推薦讓玩家使用預設資料並清除訊息,以便了解發生了什麼事情而不是將他們從體驗中移除。
初始載入發送到 PlayerDataClient ,包含載入的資料和狀態錯誤(如有).
使用 waitForDataLoadAsync 為玩家產生的任何線程都已重新啟動。
提供服務器代碼的介面
- PlayerDataServer 是可以需要並且由任何執行相同環境的伺服器代碼所需的單一。
- 玩家資料以鑰匙和值的典禮組織為一個基地。您可以使用 setValue、 getValue、 updateValue 和 2>RemoveValue2> 方法來操作這些值。這些方法都能夠同步運行,無需提供。
- hasLoaded 和 waitForDataLoadAsync 方法可用來確認資料已載入,然後才能啟用data。 我們建議您在載入屏幕上執行此操作,以避免在客戶端上每次使用資料時檢查載入錯誤。
- hasErrored 方法可以檢查玩家的初始載入是否失敗,導致他們使用預設資料。在允許玩家進行任何購買之前,請檢查此方法,因為購買無法儲存到資料無成功載入。
- playerDataUpdated 信號會在玩家和 player 、以及 key 的任何時間發射。個別系統可以訂閱此。
複製客戶端變更
- 在 PlayerDataServer 中對玩家資料進行任何變更,將會在 PlayerDataClient 中複製,除非使用 setValueAsPrivate 設定為私人
- setValueAsPrivate 用於指定不應傳送給客戶端的鑰匙
- PlayerDataClient 包含取得鑰匙值 (get) 和更新時發生的信號 (更新). 一個 hasLoaded 方法和一個 loaded 方法也包含在客戶端等待數據載入和複製之前啟動系統的信號。
- PlayerDataClient 是一個可以被任何執行在相同環境中的客戶端代碼所需並存取的單一
正在複製錯誤傳送至客戶
- 儲存或載入玩家資料時遇到錯誤狀態。PlayerDataClient 將會儲存在 PlayerDataClient 中。
- 使用 getLoadError 和 getSaveError 方法,以及 loaded 和 1>saved1> 信號來存取此資訊。
- 使用這些事件來禁用客戶端購買提示和實現警告對話框。這張圖像顯示了一個範例對話框:
儲存玩家資料
當玩家離開遊戲時,系統會執行以下動作:
- 檢查是否安全地將玩家的資料寫入資料存商店 商家。 詐騙、玩家資料失敗載入或仍在載入中的情況下。
- 通過 SessionLockedDataStoreWrapper 作出要求,寫入當前內存資料值到資料儲存,並在完成會話後移除會話鎖。
- 從服務器記憶體中清除玩家的資料 (和其他變量,例如標籤和錯誤狀態)。
在期間滯后的時間內,服務器會寫入每個玩家的資料到資料存放庫(提供資料存放是安全的)。這項歡迎的重複可以在服務器發生錯誤的情況下減少損失,並且也是必要的來維護會話鎖定。
當收到要關閉伺服器的請求時,發生以下情況在 BindToClose 回撥式碼:
- 要求將每個玩家的資料儲存在服務伺服器中,跟隨正常的程序在玩家離開服務伺服器時完成。這些請求是並行的,因為 BindToClose 回呼只有30秒的時間。
- 為了加速儲存,所有其他要求在每個鑰鍵的排隊中清除 DataStoreWrapper (請參閱 重試)。
- 回撥碼不會在所有請求完成後返回。