Background
Roblox fornisce un insieme di API per interfacciarsi con i datastore tramite DataStoreService . Il caso d'uso più comune per queste API è per il salvataggio, il caricamento e la replicazione dei dati del Giocatore. Cioè, i dati associati al progresso, all'acquisto e alle altre caratteristiche della sessione che persiste tra le sessioni di gioco individuali.
La maggior parte delle esperienze su Roblox utilizza queste API per implementare una forma di sistema di dati del giocatore. Questi implementi differiscono nel loro approccio, ma generalmente cercano di risolvere lo stesso set di problemi.
Problemi comuni
Di seguito sono riportati alcuni dei problemi più comuni che i sistemi di dati del giocatore tentano di risolvere:
In Memory Access: DataStoreService richieste fanno le richieste web che operano in asincrono e sono soggette a limiti di rate. Questo è appropriato per un carico iniziale alla fine della Sessione, ma non per le operazioni di lettura e scrittura ad alta frequenza durante il normale corso di Partita. La maggior parte dei sistemi di dati del giocatore memorizza questo d
- Prima lettura all'inizio di una Sessione
- Scrittura finale alla fine della Sessione
- I periodici scrivono in un intervallo per mitigare la situazione in cui la scena finale non funziona
- Scrivi per garantire che i dati vengono salvati durante il processo di Acquista
Storage efficiente: Stoccando tutti i dati di sessione di un Giocatorein una singola tabella ti consente di aggiornare più valori in atomica e gestire la stessa quantità di dati in meno richieste. Rimuove anche il rischio di desincronizzazione tra i valori e rende più facile da ragionevole.
Alcuni sviluppatori implementano anche la serializzazione personalizzata per ridurre le grandi strutture di dati (in genere per salvare il contenuto generato dagli utenti in gioco).
Replication: Il client ha bisogno di un accesso regolare ai dati di un Giocatore(per esempio, per aggiornare l'interfaccia utente). Un approccio generico per replicare i dati del giocatore al client ti consente di trasmettere queste informazioni senza dover creare sistemi di replicazione personalizzati per ciascuna component di dati. Gli sviluppatori spesso vogliono l'opzione di essere selettivo su ciò che viene replicato al client.
Gestione degli errori: Quando i datastore non possono essere accessibili, la maggior parte delle soluzioni implementerà un meccanismo di riprova e un fallback per i dati 'predefiniti'. La cura speciale è necessaria per garantire che i dati fallback non sovrascrivano in seguito i dati 'reali', e che questo sia comunicato al giocatore in modo appropriato.
Ripristini: Quando i magazzini di dati sono inaccessibili, la maggior parte delle soluzioni implementa un meccanismo di riprova e un fallback per i dati predefiniti. Prendi particolare cura per assicurarti che i dati fallback non sovrascrivano i dati "reali", e comunica la situazione al giocatore in modo appropriato.
Blocco sessione: Se i dati di un singolo Giocatorevengono caricati e in memoria su più server, possono verificarsi problemi in cui un server salva informazioni non aggiornate. Ciò può comportare la perdita di dati e soluzioni comuni di duplicazione di oggetti.
Handling di acquisto atomico: Verifica, assegna e registra gli acquisti atomici per prevenire gli oggetti persi o assegnati più volte.
Codice di esempio
Roblox ha un codice di riferimento per aiutarti a progettare e costruire sistemi di dati del giocatore. Il resto di questa pagina esamina il background, i dettagli di implementazione e le generali cavezze.
Dopo aver importato il modello in Studio, dovresti vedere la seguente struttura cartelle:
Architettura
Questo diagramma di alto livello illustra i sistemi chiave nella campione e come interagiscono con il codice nel resto dell'esperienza.
Riprova
Classe: DataStoreWrapper >
Background
Poiché DataStoreService rende le richieste web sotto il cappuccio, le sue richieste non sono garantite per il successo. Quando questo accade, i metodi DataStore mostrano errori, consentendoti di gestirli.
Un "gotcha" comune può verificarsi se si tenta di gestire i guasti del magazzino di dati come questo:
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
Mentre questo è un perfettamente valido meccanismo di riprova per una funzione generica, non è adatto per le richieste di DataStoreService , poiché non garantisce l'ordine in cui le richieste vengono fatte. La conservazione dell'ordine delle richieste è importante per le richieste di DataStoreService , poiché interagiscono con lo stato. Considere il seguente scenario:
- La richiesta A viene eseguita per impostare il valore della chiave K a 1.
- La richiesta non riesce, quindi un tentativo di nuovo non riesce in 2 secondi.
- Prima che si verifichi il tentativo di nuovo, richiedi a B imposta il valore di K a 2, ma il tentativo di richiedere A immediatamente sovrascrive questo valore e imposta K a 1.
Anche se UpdateAsync opera sulla versione più recente della chiave, le richieste di UpdateAsync devono essere elaborate per evitare gli stati transienti non validi (per esempio, un acquisto sottrae monete prima che venga elaborato un'aggiunta di monete, il che risulta in monete negative).
Il nostro sistema di dati del giocatore utilizza una nuova classe, DataStoreWrapper , che fornisce tentativi di produzione che sono garantiti di essere elaborati in modo per chiave.
Avvicinamento
DataStoreWrapper fornisce metodi che corrispondono ai metodi DataStore : DataStore:GetAsync() , 0> Class.GlobalDataStore:SetAsync()|DataStore:SetAsync()
Questi metodi, quando chiamati:
Aggiungi la richiesta a una coda. Ogni chiave ha la sua coda, in cui le richieste vengono elaborate in modo ordinato e in serie. Il thread di richiesta rende fino a quando la richiesta non è completata.
Questa funzionalità è basata sulla classe ThreadQueue, che è un programmatore e un limitatore di thread basato sulla coroutine. Invece di restituire una promessa, ThreadQueue restituisce il thread corrente fino all'operazione completa e lancia un errore se non funziona. Ciò è più coerente con i modelli Lua asincroni idiomatici.
Se una richiesta fallisce, riprova con un backoff esponenziale configurabile. Questi tentativi di nuovo formano parte del callback inviato alla ThreadQueue , quindi sono garantiti di completare prima della prossima richiesta nella coda per questa chiave.
Quando una richiesta è completata, il metodo richiesta restituisce il success, result pattern
DataStoreWrapper inoltre espose metodi per ottenere la lunghezza della coda per una chiave specifica e cancellare le richieste scadute. L'opzione latter è particolarmente utile in casi in cui il server si sta spegnendo e non c'è tempo per elaborare qualsiasi richiesta ma la più recente.
Grotte
DataStoreWrapper segue il principio che, al di fuori di scenari estremi, ogni richiesta di archiviazione dei dati deve essere autorizzata a completarsi (con successo o altro), anche se una richiesta più recente la rende inutilizzabile. Quando si verifica una nuova richiesta, le richieste scadute non vengono rimosse dalla coda, ma vengono invece consentite per completare prima
È difficile decidere su un insieme intuitivo di regole per quando una richiesta è sicura da rimuovere dalla coda. Considera il seguente队:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Il comportamento previsto è che GetAsync() restituisca 1 , ma se rimuoviamo la richiesta SetAsync() dalla coda a causa della sua creazione in redundancia dalla più recente, restituirà 1> 01> .
La progressione logica è che quando viene aggiunto una nuova richiesta di scrittura, solo prune richieste vecchie lontane come l'ultima Richiestadi lettura. UpdateAsync() , di gran lunga l'operazione più comune (e l'unica utilizzata da questo sistema), può leggere e scrivere, quindi sarebbe difficile da conciliare in questo design senza aggiungere complessità extra.
DataStoreWrapper potrebbe richiedere che tu specifici se una richiesta UpdateAsync() è ammessa a leggere e/o scrivere, ma non avrebbe applicabilità al nostro sistema di dati del giocatore, dove questo non può essere determinato in anticipo a causa del meccanismo di blocco della sessione (覆盖在更新后).
Una volta rimosso dalla coda, è difficile decidere su una regola intuitiva per come questo dovrebbe essere gestito. Quando viene richiesta una richiesta DataStoreWrapper, il Filocorrente viene generato fino a quando non è completata. Se rimuoviamo le richieste stagne dalla coda, dovremo decidere se restituire
In ultima istanza, la nostra opinione è che l'approccio semplice (elaborare ogni Richiesta) sia preferibile qui e crei un ambiente più chiaro per navigare in quando si approccia a problemi complessi come il blocco della sessione. L'unica eccezione a questo è durante DataModel:BindToClose()
Blocco sessione
Classe: SessionLockedDataStoreWrapper >
Background
I dati del giocatore vengono memorizzati nella memoria sul server e vengono letti e scritti solo quando necessario. Puoi leggere e aggiornare i dati del giocatore in memoria istantaneamente senza richiedere richieste web e evitare di superare i limiti di DataStoreService .
Per questo modello per funzionare come previsto, è imperativo che non più di un server sia in grado di caricare i dati di un Giocatorenella memoria dal DataStore allo stesso tempo.
Ad esempio, se il server A carica i dati di un Giocatore, il server B non può caricare tali dati fino a quando il server A non rilascia il suo blocco su di esso durante un salvataggio finale. Senza un meccanismo di blocco, il server B potrebbe caricare i dati del giocatore più recenti dal magazzino dei dati prima che il server A abbia la possibilità di salvare la versione più recente che ha in memoria. Quindi se
Anche se Roblox consente solo a un client di essere connesso a un server alla volta, non puoi assumere che i dati da una sessione vengano sempre salvati prima che la prossima sessione inizi. Considera i seguenti scenari che possono verificarsi quando un giocatore lascia il server A:
- Il server A esegue una richiesta DataStore per salvare i loro dati, ma la richiesta non si esegue e richiede più tentativi per completare con successo. Durante il periodo di riprova, il giocatore si unisce al server B.
- Il server A esegue troppi UpdateAsync() chiami alla stessa chiave e ottiene accelerato. La richiesta di salvataggio finale viene posizionata in una coda. Mentre la richiesta è in coda, il giocatore si unisce al server B.
- Su server A, alcun codice connesso all'evento PlayerRemoving rende i risultati prima che i dati del Giocatorevengano salvati. Prima che questo completi l'operazione, il giocatore si unisce al server B.
- Le prestazioni di server A sono state degradate al punto che il salvataggio finale è ritardato fino a quando il giocatore non si unisce a server B.
Questi scenari dovrebbero essere rari, ma Si verificano, in particolare in situazioni in cui un giocatore si disconnette da un server e si connette a un altro in rapida successione (per esempio, mentre si teletrasporta). Alcuni utenti malvagi potrebbero persino tentare di abusare di questo comportamento per completare azioni senza che persistano. Questo può essere particolarmente dannoso in giochi che consentono ai giocatori di scambiare e è una fonte comune di duplicazioni di oggetti.
Il blocco della sessione affronta questa vulnerabilità consentendo che quando la chiave di un Giocatoreviene prima letta dal Server, l'atomico del server scrive un lock sul metadato della chiave all'interno della stessa DataStore chiamata. Se questo valore di blocco è presente quando qualsiasi altro server tenta di leggere o scrivere la chiave, il server non procede.
Avvicinamento
SessionLockedDataStoreWrapper è un meta- wrapper attorno alla classe DataStoreWrapper . DataStoreWrapper fornisce la funzione diQueue e Riprova, che 0> SessionLockedDataStore0> integra con la funzione di session locking.
SessionLockedDataStoreWrapper passa ogni richiesta
La funzione di trasformazione passata in UpdateAsync per ogni richiesta esegue le seguenti operazioni:
Verifica che la chiave sia sicura da Accesso, abbandonando l'operazione se non lo è. "Sicuro da Accesso" significa:
L'oggetto dei metadati della chiave non include un valore non riconosciuto LockId che è stato aggiornato meno del tempo di scadenza della chiave. Ciò rende conto di rispettare un lucchetto posizionato da un altro server e di ignorare quel lucchetto se è scaduto.
Se questo server ha precedentemente posizionato il proprio valore LockId nel metadato della chiave, allora questo valore è ancora nel metadato della chiave. Ciò account per la situazione in cui un altro server ha preso il lucchetto di questo Server(per scadenza o per forza) e poi rilasciato. In alternativa, anche se LockId è stato nil
Class.GlobalDataStore:UpdateAsync()|UpdateAsync esegue l'operazione DataStore richiesta dal cliente di SessionLockedDataStoreWrapper . Ad esempio, 0> Class.GlobalDataStore:GetAsync()|GetAsync()0> traduce in UpdateAsync3> .
A seconda dei parametri passati nella Richiesta, UpdateAsync blocca o sblocca la chiave:
Se la chiave è bloccata, UpdateAsync imposta il LockId nel metadato della chiave su un GUID. Questo GUID viene memorizzato in memoria sul server in modo che possa essere verificato la prossima volta che accede alla chiave. Se il server ha già un blocco su questa chiave,
Se la chiave deve essere sbloccata, UpdateAsync rimuove il LockId nella descrizione della chiave.
Un gestore di tentativi personalizzato viene passato all'interno di DataStoreWrapper in modo che l'operazione venga riproposta se viene interrotta al passo 1 a causa della sessione che è stata chiusa.
Un messaggio di errore personalizzato viene anche restituito al consumatore, consentendo al sistema dei dati del giocatore di segnalare un errore alternativo nel caso di blocco della sessione al client.
Grotte
Il regime di blocco della sessione si basa su un server che sempre rilascia il suo blocco su una chiave quando lo ha finito con esso. Questo dovrebbe sempre avvenire attraverso un'istruzione per sbloccare la chiave come parte della fine di PlayerRemoving o BindToClose() .
Tuttavia, l'unlock può fallire in alcune situazioni. Ad esempio:
- Il server è stato crashato o DataStoreService non era operativo per tutti gli attentati per accedere alla chiave.
- A causa di un errore nella logica o di un bug simile, l'istruzione per sbloccare la chiave non è stata fornita.
Per mantenere la chiave bloccata, devi accedervi regolarmente finché è caricata in memoria. Questo avviene normalmente come parte del flusso di salvataggio automatico in esecuzione nel sistema di dati del giocatore, ma questo sistema espose anche un metodo refreshLockAsync se devi farlo manualmente.
Se il tempo di scadenza della chiave è stato superato senza che la chiave venga aggiornata, allora qualsiasi server è gratuito per prendere il blocco. Se un altro server prende il blocco, gli tentativi del server corrente per leggere o scrivere la chiave non funzionano a meno che non stabilisca un nuovo blocco.
Processo di prodotto per sviluppatori
Singleton: ReceiptHandler>
Background
Il ProcessReceipt callback esegue il lavoro critico di determinare quando finalizzare un Acquista. ProcessReceipt viene chiamato in scenari molto specifici. Per il suo set di garanzie, vedi MarketplaceService.ProcessReceipt.
Anche se la definizione di "maneggiamento" di un acquisto può differire tra le esperienze, utilizziamo i seguenti criteri
L'acquisto non è stato precedentemente gestito.
L'acquisto è riflettuto nella Sessioneattuale.
Questo richiede di eseguire le seguenti operazioni prima di restituire PurchaseGranted :
- Verifica che PurchaseId non sia già stato registrato come maneggiato.
- Assegna l'acquisto nei dati del Giocatorein memoria.
- Registra il PurchaseId come gestito nei dati del Giocatorein memoria.
- Scrivi i dati del Giocatorein memoria del giocatore del DataStore .
La sessione di blocco rende questo flusso più semplice, poiché non devi più preoccuparti dei seguenti scenari:
- I dati del giocatore in memoria nel server attuale potrebbero essere non aggiornati, richiedendo che tu abbia il valore più recente dal DataStore prima di verificare la cronologia degli PurchaseId
- Il callback per lo stesso acquisto in un altro Server, richiedendo che tu legga e scriva la cronologia PurchaseId e salvi i dati del giocatore aggiornati con l'acquisto riflettente per evitare le condizioni di gara
Blocco della sessione che garantisce che, se un tentativo di scrivere nel GiocatoreDataStore è di successo, nessun altro server ha letto o scritto con successo nel GiocatoreDataStore tra i dati caricati e salvati in questo Server. In breve, i dati del giocatore in memoria in questo server sono
Avvicinamento
I commenti in ReceiptProcessor contorno l'approccio:
Verifica che i dati del Giocatoresiano attualmente caricati su questo server e che siano stati caricati senza errori.
Poiché questo sistema utilizza la crittografia della sessione, questa verifica anche verifica che i dati in memoria sono la versione più aggiornata.
Se i dati del Giocatorenon sono ancora caricati (che è ciò che si aspetta quando un giocatore si unisce a un Gioco), aspetta che i dati del Giocatoresiano Caricare. Il sistema ascolta anche il giocatore che lascia il gioco prima che i suoi dati vengono caricati, poiché non dovrebbe essere indefinitamente e bloccare questo callback per essere chiamato nuovamente su questo server per questo acquisto se il giocatore si unisce.
Verifica che PurchaseId non sia già registrato come elaborato nei dati del giocatore.
A causa della sessione di blocco, l' array di PurchaseIds che il sistema ha in memoria è la versione più aggiornata. Se il PurchaseId è registrato come elaborato e riflesso in
Aggiorna i dati del giocatore localmente in questo server per "ricompensare" l'Acquista.
ReceiptProcessor adotta un approccio generico a callback e assegna un diverso approccio a callback per ciascun DeveloperProductId .
Aggiorna i dati del giocatore localmente in questo server per memorizzare il PurchaseId .
Invia una richiesta per salvare i dati in memoria nel DataStore, restituendo PurchaseGranted se la richiesta è un successo. Se non è il caso, restituisci NotProcessedYet.
Se questa richiesta di salvataggio non è riuscita, un'ulteriore richiesta di salvataggio dei dati della sessione in memoria del Giocatorepotrebbe ancora avere successo. Durante la prossima chiamata ProcessReceipt, il passo 2 gestisce questa situazione e restituisce PurchaseGranted.
Dati del giocatore
Singletons: PlayerData.Server > , PlayerData.Client >
Background
I moduli che forniscono un'interfaccia per il codice di gioco per essere letti e scritti in modo sincrono i dati della sessione del giocatore sono comuni nelle esperienze Roblox. Questa sezione copre PlayerData.Server e PlayerData.Client .
Avvicinamento
PlayerData.Server e PlayerData.Client gestiscono i Seguendo:
- Caricamento dei dati del Giocatorenella memoria, tra cui casi in cui non riesce a Caricare
- Fornire un'interfaccia per il codice del server per interrogare e modificare i dati del giocatore
- Replicare le modifiche nei dati del Giocatoreal client in modo che il codice client possa accedervi
- Replicazione degli errori di caricamento e/o salvataggio al client in modo che possa mostrare dialoghi di errore
- Salvataggio dei dati del Giocatoreperiodico, quando il giocatore lascia e quando il server si spegne
Caricamento dei dati del giocatore
SessionLockedDataStoreWrapper fa una richiesta getAsync al data Negozio.
Se questa richiesta fallisce, i dati predefiniti vengono utilizzati e il profilo è contrassegnato come "errored" per assicurarsi che non venga scritto nel magazzino dei dati in seguito.
Un'opzione alternativa è quella di espellere il Giocatore, ma raccomandiamo di lasciare che il giocatore gioci con dati predefiniti e cancellare i messaggi come ciò che è accaduto piuttosto che rimuoverli dall'esperienza.
Un primo carico viene inviato a PlayerDataClient contenente i dati caricati e lo stato di errore (se applicabile).
I thread generati utilizzando waitForDataLoadAsync per il giocatore sono ripresi.
Fornire un'interfaccia per il codice del server
- PlayerDataServer è un singolo che può essere richiesto e accessibile da qualsiasi codice server in esecuzione nello stesso Ambiente.
- I dati del giocatore sono organizzati in un dizionario di chiavi e valori. Puoi manipolare questi valori sul server utilizzando i metodi setValue, getValue, updateValue e 1>RemoveValue1>. Questi metodi operano tutti in modo sincrono senza rendere.
- I metodi hasLoaded e waitForDataLoadAsync sono disponibili per garantire che i dati siano caricati prima di poter accedervi. Consigliamo di fare questo una volta durante uno schermo di caricamento prima che altri sistemi vengono avviati per evitare di dover controllare gli errori di caricamento prima di ogni interazione con i dati sul client.
- Un metodo hasErrored può chiedere se il carico iniziale del Giocatoreè fallito, causando loro l'utilizzo di dati predefiniti. Controlla questo metodo prima di consentire al giocatore di effettuare qualsiasi acquisto, poiché gli acquisti non possono essere salvati ai dati senza un Caricaredi successo.
- Un segnale playerDataUpdated con il player , key e 1> value1> viene attivato ogni volta che i dati di un Giocatorevengono modificati. I sistemi individuali possono sottoscriversi a questo.
Replicazione delle modifiche al client
- Qualsiasi cambiamento dei dati del giocatore in PlayerDataServer viene replicato in PlayerDataClient , a meno che non sia stato contrassegnato come privato utilizzando setValueAsPrivate
- setValueAsPrivate è usato per rappresentare le chiavi che non dovrebbero essere inviate al client
- PlayerDataClient include un metodo per ottenere il valore di una chiave (ottenere) e un segnale che si attiva quando viene aggiornata (aggiornata). Un metodo hasLoaded e un segnale loaded sono inclusi, in modo che il client possa aspettare che i dati vengono caricati e replicati prima di avviare i suoi sistemi
- PlayerDataClient è un singolo che può essere richiesto e accessibile da qualsiasi codice client in esecuzione nello stesso Ambiente
Replicazione degli errori al client
- Status di errore incontrati quando si replica il salvataggio o il caricamento dei dati del giocatore in PlayerDataClient .
- Accedi a queste informazioni con i metodi getLoadError e getSaveError, oltre a segnali loaded e 1>saved1>.
- Ci sono due tipi di errori: DataStoreError (la richiesta DataStoreService è fallita) e SessionLocked (vedi 1> Locking sessione1>).
- Usa questi eventi per disabilitare le richieste di acquisto del client e implementare i dialoghi di avviso. Questa immagine mostra un esempio di dialogo:
Salvataggio dei dati del giocatore
Quando il giocatore lascia il Gioco, il sistema fa i seguenti passi:
- Controlla se è sicuro scrivere i dati del Giocatorenel data Negozio. I scenari in cui non sarebbe sicuro includono i dati del Giocatoreche non si carica o è ancora in carico.
- Fai una richiesta attraverso il SessionLockedDataStoreWrapper per scrivere il valore dei dati in memoria corrente e rimuovere il session lock una volta completato.
- Rimuove i dati del Giocatore(e altre variabili come metadati e stati di errore) dalla memoria del server.
Su un ciclo periodico, il server memorizza i dati di ciascun Giocatorenel data store (a patto che sia sicuro salvarli). Questa integrazione di benvenuto riduce il perdita in caso di un crash del server e è anche necessaria per mantenere il blocco della sessione.
Quando viene ricevuta una richiesta di spegnimento del server, segue ciò in un BindToClose Richiama:
- Un richiamo viene eseguito per salvare i dati di ciascun Giocatoresul Server, seguendo il processo di solito completato quando un giocatore lascia il Server. Questi richiami vengono eseguiti in parallelo, poiché BindToClose chiamate solo 30 secondi per completare.
- Per velocizzare le esportazioni, tutte le altre richieste nella coda di ciascuna chiave vengono cancellate dall'DataStoreWrapper sottostante (vedi Riprova).
- Il callback non viene restituito fino a quando tutte le richieste non sono completate.