Implementare sistemi di dati dei giocatori e di acquisto

*Questo contenuto è tradotto usando AI (Beta) e potrebbe contenere errori. Per visualizzare questa pagina in inglese, clicca qui.

Contesto

Roblox fornisce un insieme di API per interfacciarsi con i data store tramite DataStoreService. Il caso d'uso più comune per queste API è il salvataggio, il caricamento e la replicazione dei dati dei giocatori. Cioè, dati associati ai progressi del giocatore, agli acquisti e ad altre caratteristiche della sessione che persistono tra le singole sessioni di gioco.

La maggior parte dei giochi su Roblox utilizza queste API per implementare una forma di sistema di dati dei giocatori. Queste implementazioni differiscono nel loro approccio, ma generalmente cercano di risolvere lo stesso insieme di problemi.

Problemi comuni

Di seguito sono riportati alcuni dei problemi più comuni che i sistemi di dati dei giocatori cercano di risolvere:

  • Accesso in memoria: Le richieste di DataStoreService effettuano richieste web che operano in modo asincrono e sono soggette a limiti di frequenza. Questo è appropriato per un caricamento iniziale all'inizio della sessione, ma non per operazioni di lettura e scrittura ad alta frequenza durante il normale corso del gioco. La maggior parte dei sistemi di dati dei giocatori degli sviluppatori memorizza questi dati in memoria sul server Roblox, limitando le richieste di DataStoreService ai seguenti scenari:

    • Lettura iniziale all'inizio di una sessione
    • Scrittura finale alla fine della sessione
    • Scritture periodiche a intervalli per mitigare lo scenario in cui la scrittura finale fallisce
    • Scritture per garantire che i dati siano salvati durante l'elaborazione di un acquisto
  • Memorizzazione efficiente: Memorizzare tutti i dati della sessione di un giocatore in una singola tabella consente di aggiornare più valori in modo atomico e gestire la stessa quantità di dati in meno richieste. Rimuove anche il rischio di desincronizzazione tra i valori e rende più facile ragionare sui rollback.

    Alcuni sviluppatori implementano anche una serializzazione personalizzata per comprimere grandi strutture di dati (tipicamente per salvare contenuti generati dagli utenti in-game).

  • Replicazione: Il client ha bisogno di accesso regolare ai dati di un giocatore (ad esempio, per aggiornare l'interfaccia utente). Un approccio generico per replicare i dati dei giocatori al client consente di trasmettere queste informazioni senza dover creare sistemi di replicazione su misura per ciascun componente di dati. Gli sviluppatori spesso vogliono avere la possibilità di essere selettivi su ciò che viene e non viene replicato al client.

  • Gestione degli errori: Quando i DataStore non possono essere accessibili, la maggior parte delle soluzioni implementerà un meccanismo di ripetizione e un fallback ai dati "predefiniti". È necessaria particolare attenzione per garantire che i dati di fallback non sovrascrivano successivamente i dati "reali" e che questo venga comunicato al giocatore in modo appropriato.

  • Ritenti: Quando i data store sono inaccessibili, la maggior parte delle soluzioni implementa un meccanismo di ripetizione e un fallback ai dati predefiniti. Fai particolare attenzione per garantire che i dati di fallback non sovrascrivano successivamente i dati "reali" e comunica la situazione al giocatore in modo appropriato.

  • Blocco della sessione: Se i dati di un singolo giocatore vengono caricati e memorizzati in memoria su più server, possono verificarsi problemi in cui un server salva informazioni obsolete. Questo può portare a perdita di dati e comuni falle di duplicazione degli oggetti.

  • Gestione atomica degli acquisti: Verifica, assegna e registra gli acquisti in modo atomico per prevenire la perdita di oggetti o l'assegnazione multipla.

Codice di esempio

Roblox ha codice di riferimento per assisterti nella progettazione e costruzione di sistemi di dati dei giocatori. Il resto di questa pagina esamina il contesto, i dettagli di implementazione e le avvertenze generali.


Dopo aver importato il modello in Studio, dovresti vedere la seguente struttura di cartelle:

Finestra di Explorer che mostra il modello del sistema di acquisto.

Architettura

Questo diagramma ad alto livello illustra i sistemi chiave nel campione e come si interfacciano con il codice nel resto del gioco.

Un diagramma di architettura per il campione di codice.

Ritenti

Classe: DataStoreWrapper

Contesto

Poiché DataStoreService effettua richieste web in background, le sue richieste non sono garantite di avere successo. Quando ciò accade, i metodi DataStore generano errori, consentendoti di gestirli.

Un comune "problema" può verificarsi se tenti di gestire i fallimenti del data store in questo modo:

local MAX_ATTEMPTS = 5
local BASE_DELAY = 2
local MAX_DELAY = 32
local function retrySetAsync(dataStore, key, value)
for attempt = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
return result
end
if attempt < MAX_ATTEMPTS then
local backoff = math.min(MAX_DELAY, BASE_DELAY * (2 ^ (attempt - 1)))
local jitter = math.random() * backoff
task.wait(math.min(MAX_DELAY, backoff + jitter))
end
end
end

Ritenta i fallimenti transitori con un backoff esponenziale e jitter casuale in modo che i server non ripetano simultaneamente. Limita il ritardo e il numero di tentativi.

Anche con quel modello di ritardo, questo meccanismo di ripetizione non è adatto per le richieste di DataStoreService perché non garantisce l'ordine in cui vengono effettuate le richieste. Preservare l'ordine delle richieste è importante per le richieste di DataStoreService perché interagiscono con lo stato. Considera il seguente scenario:

  1. La richiesta A viene effettuata per impostare il valore della chiave K a 1.
  2. La richiesta fallisce, quindi viene programmata una ripetizione dopo il ritardo di backoff.
  3. Prima che si verifichi la ripetizione, la richiesta B imposta il valore di K a 2, ma la ripetizione della richiesta A sovrascrive immediatamente questo valore e imposta K a 1.

Anche se UpdateAsync opera sull'ultima versione del valore della chiave, le richieste UpdateAsync devono comunque essere elaborate in ordine per evitare stati transitori non validi (ad esempio, un acquisto sottrae monete prima che un'aggiunta di monete venga elaborata, risultando in monete negative).

Il nostro sistema di dati dei giocatori utilizza una nuova classe, DataStoreWrapper, che fornisce ripetizioni che garantiscono di essere elaborate in ordine per chiave.

Approccio

Un diagramma di processo che illustra il sistema di ripetizione

DataStoreWrapper fornisce metodi corrispondenti ai metodi DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() e DataStore:RemoveAsync().

Questi metodi, quando chiamati:

  1. Aggiungono la richiesta a una coda. Ogni chiave ha la propria coda, dove le richieste vengono elaborate in ordine e in serie. Il thread richiedente si sospende fino al completamento della richiesta.

    Questa funzionalità si basa sulla classe ThreadQueue, che è un pianificatore di attività basato su coroutine e limitatore di frequenza. Piuttosto che restituire una promessa, ThreadQueue sospende il thread corrente fino al completamento dell'operazione e genera un errore se fallisce. Questo è più coerente con i modelli asincroni idiomatici di Luau.

  2. Se una richiesta fallisce, viene ripetuta con un backoff esponenziale configurabile. Queste ripetizioni fanno parte del callback inviato al ThreadQueue, quindi sono garantite di completarsi prima che inizi la prossima richiesta nella coda per questa chiave.

  3. Quando una richiesta è completata, il metodo di richiesta restituisce il pattern success, result.

DataStoreWrapper espone anche metodi per ottenere la lunghezza della coda per una chiave data e per eliminare richieste obsolete. Quest'ultima opzione è particolarmente utile in scenari in cui il server si sta spegnendo e non c'è tempo per elaborare altre richieste se non le più recenti.

Avvertenze

DataStoreWrapper segue il principio che, al di fuori di scenari estremi, ogni richiesta al data store dovrebbe essere consentita a completarsi (con successo o meno), anche se una richiesta più recente la rende ridondante. Quando si verifica una nuova richiesta, le richieste obsolete non vengono rimosse dalla coda, ma vengono invece consentite a completarsi prima che venga avviata la nuova richiesta. La razionalità di questo è radicata nell'applicabilità di questo modulo come utility generica per i data store piuttosto che come strumento specifico per i dati dei giocatori, ed è la seguente:

  1. È difficile decidere un insieme intuitivo di regole per quando una richiesta è sicura da rimuovere dalla coda. Considera la seguente coda:

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

    Il comportamento atteso è che GetAsync() restituisca 1, ma se rimuoviamo la richiesta SetAsync() dalla coda a causa della sua ridondanza rispetto all'ultima, restituirebbe 0.

    La progressione logica è che quando viene aggiunta una nuova richiesta di scrittura, si devono potare le richieste obsolete solo fino alla richiesta di lettura più recente. UpdateAsync, di gran lunga l'operazione più comune (e l'unica utilizzata da questo sistema), può sia leggere che scrivere, quindi sarebbe difficile riconciliare questo design senza aggiungere ulteriore complessità.

    DataStoreWrapper potrebbe richiederti di specificare se una richiesta UpdateAsync() era autorizzata a leggere e/o scrivere, ma non avrebbe alcuna applicabilità al nostro sistema di dati dei giocatori, dove questo non può essere determinato in anticipo a causa del meccanismo di blocco della sessione (trattato in modo più dettagliato in seguito).

  2. Una volta rimossa dalla coda, è difficile decidere una regola intuitiva per come questo dovrebbe essere gestito. Quando viene effettuata una richiesta DataStoreWrapper, il thread corrente viene sospeso fino al completamento. Se rimuovessimo le richieste obsolete dalla coda, dovremmo decidere se restituire false, "Rimosso dalla coda" o non restituire mai e scartare il thread attivo. Entrambi gli approcci presentano i propri svantaggi e scaricano ulteriore complessità sul consumatore.

In definitiva, la nostra opinione è che l'approccio semplice (elaborare ogni richiesta) sia preferibile qui e crei un ambiente più chiaro da navigare quando si affrontano problemi complessi come il blocco della sessione. L'unica eccezione a questo è durante DataModel:BindToClose(), dove la pulizia della coda diventa necessaria per salvare i dati di tutti gli utenti in tempo e il valore restituito dalle singole chiamate di funzione non è più una preoccupazione continua. Per ulteriori dettagli, vedere Dati dei giocatori.

Blocco della sessione

Classe: SessionLockedDataStoreWrapper

Contesto

I dati dei giocatori sono memorizzati in memoria sul server e vengono letti e scritti negli underlying data store solo quando necessario. Puoi leggere e aggiornare i dati dei giocatori in memoria istantaneamente senza necessità di richieste web ed evitare di superare i limiti di DataStoreService.

Affinché questo modello funzioni come previsto, è imperativo che non più di un server possa caricare i dati di un giocatore in memoria dalla DataStore contemporaneamente.

Ad esempio, se il server A carica i dati di un giocatore, il server B non può caricare quei dati fino a quando il server A non rilascia il suo blocco durante un salvataggio finale. Senza un meccanismo di blocco, il server B potrebbe caricare dati obsoleti dal data store prima che il server A abbia la possibilità di salvare la versione più recente che ha in memoria. Poi, se il server A salva i suoi dati più recenti dopo che il server B ha caricato i dati obsoleti, il server B sovrascriverebbe quei dati più recenti durante il suo prossimo salvataggio.

Anche se Roblox consente solo a un client di essere connesso a un server alla volta, non puoi assumere che i dati di una sessione siano sempre salvati prima che inizi la sessione successiva. Considera i seguenti scenari che possono verificarsi quando un giocatore lascia il server A:

  1. Il server A effettua una richiesta DataStore per salvare i propri dati, ma la richiesta fallisce e richiede diversi tentativi per completarsi con successo. Durante il periodo di ripetizione, il giocatore si unisce al server B.
  2. Il server A effettua troppe chiamate UpdateAsync() alla stessa chiave e viene limitato. L'ultima richiesta di salvataggio viene inserita in una coda. Mentre la richiesta è in coda, il giocatore si unisce al server B.
  3. Sul server A, del codice collegato all'evento PlayerRemoving si sospende prima che i dati del giocatore vengano salvati. Prima che questa operazione venga completata, il giocatore si unisce al server B.
  4. Le prestazioni del server A sono degradate al punto che il salvataggio finale è ritardato fino a dopo che il giocatore si unisce al 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 (ad esempio, durante il teletrasporto). Alcuni utenti malintenzionati potrebbero persino tentare di abusare di questo comportamento per completare azioni senza che esse persistano. Questo può essere particolarmente impattante nei giochi che consentono ai giocatori di scambiare e rappresenta una comune fonte di exploit di duplicazione degli oggetti.

Il blocco della sessione affronta questa vulnerabilità garantendo che quando la chiave DataStore di un giocatore viene letta per la prima volta dal server, il server scrive in modo atomico un blocco nei metadati della chiave all'interno della stessa chiamata UpdateAsync(). Se questo valore di blocco è presente quando un altro server tenta di leggere o scrivere la chiave, il server non procede.

Approccio

Un diagramma di processo che illustra il sistema di blocco della sessione

SessionLockedDataStoreWrapper è un meta-wrapper attorno alla classe DataStoreWrapper. DataStoreWrapper fornisce funzionalità di coda e ripetizione, che SessionLockedDataStoreWrapper integra con il blocco della sessione.

SessionLockedDataStoreWrapper passa ogni richiesta DataStore—indipendentemente dal fatto che sia GetAsync, SetAsync o UpdateAsync—attraverso UpdateAsync. Questo perché UpdateAsync consente di leggere e scrivere una chiave in modo atomico. È anche possibile abbandonare la scrittura in base al valore letto restituendo nil nel callback di trasformazione.

La funzione di trasformazione passata a UpdateAsync per ogni richiesta esegue le seguenti operazioni:

  1. Verifica che la chiave sia sicura da accedere, abbandonando l'operazione se non lo è. "Sicura da accedere" significa:

    • L'oggetto metadati della chiave non include un valore LockId non riconosciuto che è stato aggiornato meno di tanto tempo fa rispetto al tempo di scadenza del blocco. Questo tiene conto del rispetto di un blocco posto da un altro server e dell'ignorare quel blocco se è scaduto.

    • Se questo server ha precedentemente inserito il proprio valore LockId nei metadati della chiave, allora questo valore è ancora nei metadati della chiave. Questo tiene conto della situazione in cui un altro server ha preso il blocco di questo server (per scadenza o per forza) e successivamente lo ha rilasciato. In altre parole, anche se LockId è nil, un altro server potrebbe comunque aver sostituito e rimosso un blocco nel tempo intercorso da quando hai bloccato la chiave.

  2. UpdateAsync esegue l'operazione DataStore richiesta dal consumatore di SessionLockedDataStoreWrapper. Ad esempio, GetAsync() si traduce in function(value) return value end.

  3. A seconda dei parametri passati nella richiesta, UpdateAsync blocca o sblocca la chiave:

    1. Se la chiave deve essere bloccata, UpdateAsync imposta il LockId nei metadati della chiave a un GUID. Questo GUID è 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, non apporta modifiche. Pianifica anche un'attività per avvisarti se non accedi di nuovo alla chiave per mantenere il blocco entro il tempo di scadenza del blocco.

    2. Se la chiave deve essere sbloccata, UpdateAsync rimuove il LockId nei metadati della chiave.

Un gestore di ripetizione personalizzato viene passato al sottostante DataStoreWrapper in modo che l'operazione venga ripetuta se è stata abortita al passo 1 a causa del blocco della sessione.

Un messaggio di errore personalizzato viene anche restituito al consumatore, consentendo al sistema di dati dei giocatori di segnalare un errore alternativo nel caso di blocco della sessione al client.

Avvertenze

Il regime di blocco della sessione si basa sul fatto che un server rilasci sempre il proprio blocco su una chiave quando ha finito di usarla. Questo dovrebbe sempre avvenire tramite un'istruzione per sbloccare la chiave come parte della scrittura finale in PlayerRemoving o BindToClose().

Tuttavia, lo sblocco può fallire in determinate situazioni. Ad esempio:

  • Il server è andato in crash o DataStoreService era inoperabile per tutti i tentativi di accesso alla chiave.
  • A causa di un errore nella logica o di un bug simile, l'istruzione per sbloccare la chiave non è stata effettuata.

Per mantenere il blocco su una chiave, devi accedervi regolarmente finché è caricata in memoria. Questo di solito viene fatto come parte del ciclo di salvataggio automatico che gira in background nella maggior parte dei sistemi di dati dei giocatori, ma questo sistema espone anche un metodo refreshLockAsync se hai bisogno di farlo manualmente.

Se il tempo di scadenza del blocco è stato superato senza che il blocco sia stato aggiornato, allora qualsiasi server è libero di prendere il blocco. Se un server diverso prende il blocco, i tentativi del server corrente di leggere o scrivere la chiave falliscono a meno che non stabilisca un nuovo blocco.

Elaborazione dei prodotti per sviluppatori

Singleton: ReceiptHandler

Contesto

Il callback ProcessReceipt svolge il compito critico di determinare quando finalizzare un acquisto. ProcessReceipt viene chiamato in scenari molto specifici. Per il suo insieme di garanzie, vedere MarketplaceService.ProcessReceipt.

Sebbene la definizione di "gestire" un acquisto possa differire tra i giochi, utilizziamo i seguenti criteri:

  1. L'acquisto non è stato precedentemente gestito.

  2. L'acquisto è riflesso nella sessione corrente.

  3. L'acquisto è stato salvato in un DataStore.

    Ogni acquisto, anche quelli consumabili una tantum, dovrebbe essere riflesso nel DataStore in modo che la cronologia degli acquisti degli utenti sia inclusa nei dati della loro sessione.

Questo richiede di condurre le seguenti operazioni prima di restituire PurchaseGranted:

  1. Verificare che il PurchaseId non sia già stato registrato come gestito.
  2. Assegnare l'acquisto nei dati del giocatore in memoria.
  3. Registrare il PurchaseId come gestito nei dati del giocatore in memoria.
  4. Scrivere i dati del giocatore in memoria nel DataStore.

Il blocco della sessione semplifica questo flusso, poiché non devi più preoccuparti dei seguenti scenari:

  • I dati del giocatore in memoria nel server corrente potrebbero essere obsoleti, richiedendo di recuperare il valore più recente dal DataStore prima di verificare la cronologia del PurchaseId.
  • Il callback per lo stesso acquisto che viene eseguito in un altro server, richiedendo di leggere e scrivere sia la cronologia del PurchaseId che salvare i dati del giocatore aggiornati con l'acquisto riflesso in modo atomico per prevenire condizioni di gara.

Il blocco della sessione garantisce che, se un tentativo di scrivere nel DataStore del giocatore ha successo, nessun altro server ha letto o scritto con successo nel DataStore del giocatore tra il caricamento e il salvataggio dei dati in questo server. In breve, i dati del giocatore in memoria in questo server sono la versione più aggiornata disponibile. Ci sono alcune avvertenze, ma non influenzano questo comportamento.

Approccio

I commenti in ReceiptProcessor delineano l'approccio:

  1. Verifica che i dati del giocatore siano attualmente caricati su questo server e che siano stati caricati senza errori.

    Poiché questo sistema utilizza il blocco della sessione, questo controllo verifica anche che i dati in memoria siano la versione più aggiornata.

    Se i dati del giocatore non sono ancora stati caricati (cosa che è prevista quando un giocatore entra in un gioco), attendi che i dati del giocatore vengano caricati. Il sistema ascolta anche per il giocatore che lascia il gioco prima che i suoi dati vengano caricati, poiché non dovrebbe sospendersi indefinitamente e bloccare nuovamente l'invocazione di questo callback su questo server per questo acquisto se il giocatore si riunisce.

  2. Verifica che il PurchaseId non sia già registrato come elaborato nei dati del giocatore.

    Grazie al blocco della sessione, l'array di PurchaseIds che il sistema ha in memoria è la versione più aggiornata. Se il PurchaseId è registrato come elaborato e riflesso in un valore che è stato caricato o salvato nel DataStore, restituisci PurchaseGranted. Se è registrato come elaborato, ma non riflesso nel DataStore, restituisci NotProcessedYet.

  3. Aggiorna i dati del giocatore localmente in questo server per "assegnare" l'acquisto.

    ReceiptProcessor adotta un approccio generico ai callback e assegna un callback diverso per ciascun DeveloperProductId.

  4. Aggiorna i dati del giocatore localmente in questo server per memorizzare il PurchaseId.

  5. Invia una richiesta per salvare i dati in memoria nel DataStore, restituendo PurchaseGranted se la richiesta ha successo. In caso contrario, restituisci NotProcessedYet.

    Se questa richiesta di salvataggio non ha successo, una richiesta successiva per salvare i dati della sessione in memoria del giocatore potrebbe comunque avere successo. Durante la successiva chiamata a ProcessReceipt, il passaggio 2 gestisce questa situazione e restituisce PurchaseGranted.

Dati dei giocatori

Singletons: PlayerData.Server, PlayerData.Client

Contesto

I moduli che forniscono un'interfaccia per il codice per leggere e scrivere in modo sincrono i dati della sessione dei giocatori sono comuni nei giochi Roblox. Questa sezione tratta di PlayerData.Server e PlayerData.Client.

Approccio

PlayerData.Server e PlayerData.Client gestiscono quanto segue:

  1. Caricamento dei dati del giocatore in memoria, inclusa la gestione dei casi in cui il caricamento fallisce
  2. Fornire un'interfaccia per il codice del server per interrogare e modificare i dati del giocatore
  3. Replicare le modifiche nei dati del giocatore al client in modo che il codice client possa accedervi
  4. Replicare gli errori di caricamento e/o salvataggio al client in modo che possa mostrare dialoghi di errore
  5. Salvare i dati del giocatore periodicamente, quando il giocatore lascia e quando il server si spegne

Carica i dati del giocatore

Un diagramma di processo che illustra il sistema di caricamento
  1. SessionLockedDataStoreWrapper effettua una richiesta getAsync al data store.

    Se questa richiesta fallisce, vengono utilizzati i dati predefiniti e il profilo viene contrassegnato come "errore" per garantire che non venga scritto nel data store in seguito.

    Un'opzione alternativa è espellere il giocatore, ma raccomandiamo di lasciare che il giocatore giochi con dati predefiniti e una comunicazione chiara su quanto accaduto piuttosto che rimuoverlo dal gioco.

  2. Un payload iniziale viene inviato a PlayerDataClient contenente i dati caricati e lo stato di errore (se presente).

  3. Qualsiasi thread sospeso utilizzando waitForDataLoadAsync per il giocatore viene ripreso.

Fornire un'interfaccia per il codice del server

  • PlayerDataServer è un singleton 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 removeValue. Questi metodi operano tutti in modo sincrono senza sospensioni.
  • I metodi hasLoaded e waitForDataLoadAsync sono disponibili per garantire che i dati siano stati caricati prima di accedervi. Raccomandiamo di farlo una volta durante una schermata di caricamento prima che altri sistemi vengano avviati per evitare di dover controllare gli errori di caricamento prima di ogni interazione con i dati sul client.
  • Un metodo hasErrored può interrogare se il caricamento iniziale del giocatore è fallito, costringendolo a utilizzare dati predefiniti. Controlla questo metodo prima di consentire al giocatore di effettuare acquisti, poiché gli acquisti non possono essere salvati nei dati senza un caricamento riuscito.
  • Un segnale playerDataUpdated viene attivato con il player, key e value ogni volta che i dati di un giocatore vengono modificati. I singoli sistemi possono iscriversi a questo.

Replicare le modifiche al client

  • Qualsiasi modifica ai dati del giocatore in PlayerDataServer viene replicata in PlayerDataClient, a meno che quella chiave non sia stata contrassegnata come privata utilizzando setValueAsPrivate
    • setValueAsPrivate viene utilizzato per denotare chiavi che non devono essere inviate al client
  • PlayerDataClient include un metodo per ottenere il valore di una chiave (get) e un segnale che viene attivato quando viene aggiornato (updated). È anche incluso un metodo hasLoaded e un segnale loaded, in modo che il client possa attendere il caricamento e la replicazione dei dati prima di avviare i propri sistemi
  • PlayerDataClient è un singleton che può essere richiesto e accessibile da qualsiasi codice client in esecuzione nello stesso ambiente

Replicare errori al client

  • Gli stati di errore riscontrati durante il salvataggio o il caricamento dei dati del giocatore vengono replicati in PlayerDataClient.
  • Accedi a queste informazioni con i metodi getLoadError e getSaveError, insieme ai segnali loaded e saved.
  • Ci sono due tipi di errori: DataStoreError (la richiesta DataStoreService è fallita) e SessionLocked (vedi Blocco della sessione).
  • Utilizza questi eventi per disabilitare i prompt di acquisto del client e implementare dialoghi di avviso. Questa immagine mostra un esempio di dialogo:
Uno screenshot di un esempio di avviso che potrebbe essere mostrato quando i dati del giocatore non riescono a caricarsi

Salva i dati del giocatore

Un diagramma di processo che illustra il sistema di salvataggio
  1. Quando il giocatore lascia il gioco, il sistema esegue i seguenti passaggi:

    1. Controlla se è sicuro scrivere i dati del giocatore nel data store. Gli scenari in cui non sarebbe sicuro includono il fallimento del caricamento dei dati del giocatore o il caricamento ancora in corso.
    2. Effettua una richiesta tramite SessionLockedDataStoreWrapper per scrivere il valore corrente dei dati in memoria nel data store e rimuovere il blocco della sessione una volta completato.
    3. Pulisce i dati del giocatore (e altre variabili come metadati e stati di errore) dalla memoria del server.
  2. In un ciclo periodico, il server scrive i dati di ciascun giocatore nel data store (a condizione che sia sicuro salvare). Questa benvenuta ridondanza mitiga la perdita in caso di crash del server ed è anche necessaria per mantenere il blocco della sessione.

    Il campione avvia un ciclo condiviso dopo AUTO_SAVE_INTERVAL secondi (180 per impostazione predefinita) e poi salva ogni giocatore caricato in parallelo. Quel ciclo non offsetta server o giocatori, quindi i server che iniziano a tempi simili possono svuotarsi insieme.

    Offsetta il primo salvataggio di ciascun giocatore di una durata casuale all'interno dell'intervallo in modo che i server attivi non scrivano tutti contemporaneamente:

    local AUTO_SAVE_INTERVAL = 180
    local function startAutoSave(player)
    task.spawn(function()
    task.wait(math.random() * AUTO_SAVE_INTERVAL)
    while player.Parent do
    if canSave(player) then
    savePlayerData(player)
    end
    task.wait(AUTO_SAVE_INTERVAL)
    end
    end)
    end
  3. Quando viene ricevuta una richiesta di spegnere il server, si verifica quanto segue in un callback BindToClose:

    1. Viene effettuata una richiesta per salvare i dati di ciascun giocatore nel server, seguendo il processo normalmente seguito quando un giocatore lascia il server. Queste richieste vengono effettuate in parallelo, poiché i callback BindToClose hanno solo 30 secondi per completarsi.
    2. Per accelerare i salvataggi, tutte le altre richieste nella coda di ciascuna chiave vengono eliminate dal sottostante DataStoreWrapper (vedi Ritenti).
    3. Il callback non restituisce fino a quando tutte le richieste non sono state completate.
© 2026 Roblox Corporation. Roblox, il logo Roblox e Powering Imagination sono tra i nostri marchi registrati e non registrati negli Stati Uniti. e altri paesi.