Implementar sistemas de datos de jugadores y de compras

*Este contenido se traduce usando la IA (Beta) y puede contener errores. Para ver esta página en inglés, haz clic en aquí.

Antecedentes

Roblox proporciona un conjunto de APIs para interactuar con almacenes de datos a través de DataStoreService. El caso de uso más común para estas APIs es guardar, cargar y replicar datos de jugadores. Es decir, datos asociados con el progreso del jugador, compras y otras características de la sesión que persisten entre sesiones de juego individuales.

La mayoría de los juegos en Roblox utilizan estas APIs para implementar alguna forma de un sistema de datos de jugadores. Estas implementaciones difieren en su enfoque, pero generalmente buscan resolver el mismo conjunto de problemas.

Problemas comunes

A continuación se presentan algunos de los problemas más comunes que los sistemas de datos de jugadores intentan resolver:

  • Acceso en memoria: Las solicitudes de DataStoreService realizan solicitudes web que operan de manera asíncrona y están sujetas a límites de tasa. Esto es apropiado para una carga inicial al comienzo de la sesión, pero no para operaciones de lectura y escritura de alta frecuencia durante el curso normal del juego. La mayoría de los sistemas de datos de jugadores de los desarrolladores almacenan estos datos en memoria en el servidor de Roblox, limitando las solicitudes de DataStoreService a los siguientes escenarios:

    • Lectura inicial al comienzo de una sesión
    • Escritura final al final de la sesión
    • Escrituras periódicas a intervalos para mitigar el escenario en el que la escritura final falla
    • Escrituras para asegurar que los datos se guarden mientras se procesa una compra
  • Almacenamiento eficiente: Almacenar todos los datos de la sesión de un jugador en una sola tabla permite actualizar múltiples valores de manera atómica y manejar la misma cantidad de datos en menos solicitudes. También elimina el riesgo de desincronización entre valores y facilita las reversas.

    Algunos desarrolladores también implementan serialización personalizada para comprimir grandes estructuras de datos (típicamente para guardar contenido generado por el usuario en el juego).

  • Replicación: El cliente necesita acceso regular a los datos de un jugador (por ejemplo, para actualizar la interfaz de usuario). Un enfoque genérico para replicar datos de jugadores al cliente permite transmitir esta información sin tener que crear sistemas de replicación personalizados para cada componente de datos. Los desarrolladores a menudo quieren la opción de ser selectivos sobre qué se replica y qué no se replica al cliente.

  • Manejo de errores: Cuando no se pueden acceder a los DataStores, la mayoría de las soluciones implementarán un mecanismo de reintento y un respaldo a datos 'predeterminados'. Se necesita un cuidado especial para asegurar que los datos de respaldo no sobrescriban más tarde los datos 'reales', y que esto se comunique al jugador de manera apropiada.

  • Reintentos: Cuando los almacenes de datos son inaccesibles, la mayoría de las soluciones implementan un mecanismo de reintento y un respaldo a datos predeterminados. Tenga especial cuidado para asegurarse de que los datos de respaldo no sobrescriban más tarde los datos "reales", y comunique la situación al jugador de manera apropiada.

  • Bloqueo de sesión: Si los datos de un solo jugador se cargan y están en memoria en múltiples servidores, pueden ocurrir problemas en los que un servidor guarda información desactualizada. Esto puede llevar a la pérdida de datos y a agujeros comunes de duplicación de artículos.

  • Manejo atómico de compras: Verifique, otorgue y registre compras de manera atómica para evitar que los artículos se pierdan o se otorguen múltiples veces.

Código de ejemplo

Roblox tiene código de referencia para ayudarle a diseñar y construir sistemas de datos de jugadores. El resto de esta página examina antecedentes, detalles de implementación y advertencias generales.


Después de importar el modelo en Studio, debería ver la siguiente estructura de carpetas:

Ventana del explorador mostrando el modelo del sistema de compras.

Arquitectura

Este diagrama de alto nivel ilustra los sistemas clave en el ejemplo y cómo se interfazan con el código en el resto del juego.

Un diagrama de arquitectura para el código de ejemplo.

Reintentos

Clase: DataStoreWrapper

Antecedentes

Dado que DataStoreService realiza solicitudes web en segundo plano, sus solicitudes no están garantizadas para tener éxito. Cuando esto sucede, los métodos DataStore lanzan errores, lo que le permite manejarlos.

Un "problema" común puede ocurrir si intenta manejar fallos de almacén de datos de esta manera:

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

Reintente fallos transitorios con retroceso exponencial y jitter aleatorio para que los servidores no reintenten simultáneamente. Limite el retraso y el número de intentos.

Incluso con ese patrón de retraso, este mecanismo de reintento no es adecuado para solicitudes de DataStoreService porque no garantiza el orden en que se realizan las solicitudes. Preservar el orden de las solicitudes es importante para las solicitudes de DataStoreService porque interactúan con el estado. Considere el siguiente escenario:

  1. Se realiza la solicitud A para establecer el valor de la clave K en 1.
  2. La solicitud falla, por lo que se programa un reintento para ejecutarse después del retraso de retroceso.
  3. Antes de que ocurra el reintento, la solicitud B establece el valor de K en 2, pero el reintento de la solicitud A sobrescribe inmediatamente este valor y establece K en 1.

A pesar de que UpdateAsync opera sobre la última versión del valor de la clave, las solicitudes de UpdateAsync aún deben procesarse en orden para evitar estados transitorios inválidos (por ejemplo, una compra resta monedas antes de que se procese una adición de monedas, resultando en monedas negativas).

Nuestro sistema de datos de jugadores utiliza una nueva clase, DataStoreWrapper, que proporciona reintentos que se garantizan que se procesen en orden por clave.

Enfoque

Un diagrama de proceso que ilustra el sistema de reintentos

DataStoreWrapper proporciona métodos correspondientes a los métodos de DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() y DataStore:RemoveAsync().

Estos métodos, cuando se llaman:

  1. Agregan la solicitud a una cola. Cada clave tiene su propia cola, donde las solicitudes se procesan en orden y en serie. El hilo solicitante se cede hasta que la solicitud se ha completado.

    Esta funcionalidad se basa en la clase ThreadQueue, que es un programador de tareas basado en corutinas y limitador de tasa. En lugar de devolver una promesa, ThreadQueue cede el hilo actual hasta que la operación se complete y lanza un error si falla. Esto es más consistente con los patrones asíncronos idiomáticos de Luau.

  2. Si una solicitud falla, se reintenta con un retroceso exponencial configurable. Estos reintentos forman parte de la devolución de llamada enviada a la ThreadQueue, por lo que se garantiza que se completen antes de que comience la siguiente solicitud en la cola para esta clave.

  3. Cuando se completa una solicitud, el método de solicitud devuelve el patrón success, result.

DataStoreWrapper también expone métodos para obtener la longitud de la cola para una clave dada y limpiar solicitudes obsoletas. Esta última opción es particularmente útil en escenarios en los que el servidor se está apagando y no hay tiempo para procesar más que las solicitudes más recientes.

Advertencias

DataStoreWrapper sigue el principio de que, fuera de escenarios extremos, cada solicitud de almacén de datos debe permitirse completar (con éxito o no), incluso si una solicitud más reciente la hace redundante. Cuando se produce una nueva solicitud, las solicitudes obsoletas no se eliminan de la cola, sino que se les permite completar antes de que se inicie la nueva solicitud. La razón de esto se basa en la aplicabilidad de este módulo como una utilidad genérica de almacén de datos en lugar de una herramienta específica para datos de jugadores, y es la siguiente:

  1. Es difícil decidir un conjunto intuitivo de reglas para cuándo una solicitud es segura para eliminar de la cola. Considere la siguiente cola:

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

    El comportamiento esperado es que GetAsync() devolvería 1, pero si eliminamos la solicitud SetAsync() de la cola debido a que fue hecha redundante por la más reciente, devolvería 0.

    La progresión lógica es que cuando se agrega una nueva solicitud de escritura, solo se deben eliminar las solicitudes obsoletas hasta la más reciente solicitud de lectura. UpdateAsync, de lejos la operación más común (y la única utilizada por este sistema), puede leer y escribir, por lo que sería difícil reconciliar esto dentro de este diseño sin agregar complejidad adicional.

    DataStoreWrapper podría requerir que especifique si una solicitud de UpdateAsync() estaba permitida para leer y/o escribir, pero no tendría aplicabilidad a nuestro sistema de datos de jugadores, donde esto no se puede determinar de antemano debido al mecanismo de bloqueo de sesión (cubierto con más detalle más adelante).

  2. Una vez eliminadas de la cola, es difícil decidir una regla intuitiva para cómo debe manejarse esto. Cuando se realiza una solicitud de DataStoreWrapper, el hilo actual se cede hasta que se completa. Si elimináramos solicitudes obsoletas de la cola, tendríamos que decidir si devolver false, "Eliminado de la cola" o nunca devolver y descartar el hilo activo. Ambos enfoques tienen sus propias desventajas y transfieren complejidad adicional al consumidor.

En última instancia, nuestra opinión es que el enfoque simple (procesar cada solicitud) es preferible aquí y crea un entorno más claro para navegar en cuestiones complejas como el bloqueo de sesión. La única excepción a esto es durante DataModel:BindToClose(), donde limpiar la cola se vuelve necesario para guardar los datos de todos los usuarios a tiempo y el valor que devuelven las llamadas a funciones individuales ya no es una preocupación continua. Para tener en cuenta esto, exponemos un método skipAllQueuesToLastEnqueued. Para más contexto, consulte Datos de Jugadores.

Bloqueo de sesión

Clase: SessionLockedDataStoreWrapper

Antecedentes

Los datos de los jugadores se almacenan en memoria en el servidor y solo se leen y escriben en los almacenes de datos subyacentes cuando es necesario. Puede leer y actualizar los datos de los jugadores en memoria instantáneamente sin necesidad de solicitudes web y evitar exceder los límites de DataStoreService.

Para que este modelo funcione como se pretende, es imperativo que no más de un servidor pueda cargar los datos de un jugador en memoria desde el DataStore al mismo tiempo.

Por ejemplo, si el servidor A carga los datos de un jugador, el servidor B no puede cargar esos datos hasta que el servidor A libere su bloqueo durante una última guardado. Sin un mecanismo de bloqueo, el servidor B podría cargar datos de jugador desactualizados desde el almacén de datos antes de que el servidor A tenga la oportunidad de guardar la versión más reciente que tiene en memoria. Luego, si el servidor A guarda sus datos más nuevos después de que el servidor B carga los datos desactualizados, el servidor B sobrescribiría esos datos más nuevos durante su próximo guardado.

A pesar de que Roblox solo permite que un cliente esté conectado a un servidor a la vez, no se puede asumir que los datos de una sesión se guardan siempre antes de que comience la siguiente sesión. Considere los siguientes escenarios que pueden ocurrir cuando un jugador deja el servidor A:

  1. El servidor A realiza una solicitud de DataStore para guardar sus datos, pero la solicitud falla y requiere varios reintentos para completarse con éxito. Durante el período de reintento, el jugador se une al servidor B.
  2. El servidor A realiza demasiadas llamadas a UpdateAsync() a la misma clave y es limitado. La solicitud de guardado final se coloca en una cola. Mientras la solicitud está en la cola, el jugador se une al servidor B.
  3. En el servidor A, algún código conectado al evento PlayerRemoving cede antes de que se guarden los datos del jugador. Antes de que esta operación se complete, el jugador se une al servidor B.
  4. El rendimiento del servidor A se ha degradado hasta el punto en que el guardado final se retrasa hasta después de que el jugador se une al servidor B.

Estos escenarios deberían ser raros, pero ocurren, particularmente en situaciones donde un jugador se desconecta de un servidor y se conecta a otro en rápida sucesión (por ejemplo, mientras se teletransporta). Algunos usuarios malintencionados incluso podrían intentar abusar de este comportamiento para completar acciones sin que persistan. Esto puede ser particularmente impactante en juegos que permiten a los jugadores comerciar y es una fuente común de exploits de duplicación de artículos.

El bloqueo de sesión aborda esta vulnerabilidad asegurando que cuando la clave DataStore de un jugador se lee por primera vez por el servidor, el servidor escribe atómicamente un bloqueo en los metadatos de la clave dentro de la misma llamada a UpdateAsync(). Si este valor de bloqueo está presente cuando cualquier otro servidor intenta leer o escribir la clave, el servidor no procede.

Enfoque

Un diagrama de proceso que ilustra el sistema de bloqueo de sesión

SessionLockedDataStoreWrapper es un meta-envoltorio alrededor de la clase DataStoreWrapper. DataStoreWrapper proporciona funcionalidad de cola y reintento, que SessionLockedDataStoreWrapper complementa con bloqueo de sesión.

SessionLockedDataStoreWrapper pasa cada solicitud de DataStore, independientemente de si es GetAsync, SetAsync o UpdateAsync, a través de UpdateAsync. Esto se debe a que UpdateAsync permite que una clave se lea y escriba atómicamente. También es posible abandonar la escritura en función del valor leído devolviendo nil en la devolución de llamada de transformación.

La función de transformación pasada a UpdateAsync para cada solicitud realiza las siguientes operaciones:

  1. Verifica que la clave sea segura para acceder, abandonando la operación si no lo es. "Segura para acceder" significa:

    • El objeto de metadatos de la clave no incluye un valor LockId no reconocido que se actualizó por última vez hace menos de tiempo de expiración del bloqueo. Esto tiene en cuenta el respeto a un bloqueo colocado por otro servidor y para ignorar ese bloqueo si ha expirado.

    • Si este servidor ha colocado su propio valor LockId en los metadatos de la clave anteriormente, entonces este valor aún está en los metadatos de la clave. Esto tiene en cuenta la situación en la que otro servidor ha tomado el bloqueo de este servidor (por expiración o por fuerza) y luego lo ha liberado. Alternativamente expresado, incluso si LockId es nil, otro servidor podría haber reemplazado y eliminado un bloqueo en el tiempo desde que bloqueó la clave.

  2. UpdateAsync realiza la operación de DataStore que el consumidor de SessionLockedDataStoreWrapper solicitó. Por ejemplo, GetAsync() se traduce a function(value) return value end.

  3. Dependiendo de los parámetros pasados a la solicitud, UpdateAsync bloquea o desbloquea la clave:

    1. Si la clave debe ser bloqueada, UpdateAsync establece el LockId en los metadatos de la clave a un GUID. Este GUID se almacena en memoria en el servidor para que se pueda verificar la próxima vez que acceda a la clave. Si el servidor ya tiene un bloqueo en esta clave, no realiza cambios. También programa una tarea para advertirle si no accede a la clave nuevamente para mantener el bloqueo dentro del tiempo de expiración del bloqueo.

    2. Si la clave debe ser desbloqueada, UpdateAsync elimina el LockId en los metadatos de la clave.

Se pasa un controlador de reintento personalizado al DataStoreWrapper subyacente para que la operación se reintente si fue abortada en el paso 1 debido a que la sesión estaba bloqueada.

También se devuelve un mensaje de error personalizado al consumidor, lo que permite que el sistema de datos de jugadores informe un error alternativo en caso de bloqueo de sesión al cliente.

Advertencias

El régimen de bloqueo de sesión depende de que un servidor siempre libere su bloqueo en una clave cuando haya terminado con ella. Esto debería ocurrir siempre a través de una instrucción para desbloquear la clave como parte de la escritura final en PlayerRemoving o BindToClose().

Sin embargo, el desbloqueo puede fallar en ciertas situaciones. Por ejemplo:

  • El servidor se bloqueó o DataStoreService no estaba operativo para todos los intentos de acceder a la clave.
  • Debido a un error en la lógica o un error similar, no se realizó la instrucción para desbloquear la clave.

Para mantener el bloqueo en una clave, debe acceder regularmente a ella mientras esté cargada en memoria. Esto normalmente se haría como parte del bucle de guardado automático que se ejecuta en segundo plano en la mayoría de los sistemas de datos de jugadores, pero este sistema también expone un método refreshLockAsync si necesita hacerlo manualmente.

Si el tiempo de expiración del bloqueo ha sido excedido sin que se haya actualizado el bloqueo, entonces cualquier servidor es libre de tomar el bloqueo. Si un servidor diferente toma el bloqueo, los intentos del servidor actual de leer o escribir la clave fallan a menos que establezca un nuevo bloqueo.

Procesamiento de productos de desarrollador

Singleton: ReceiptHandler

Antecedentes

La devolución de llamada ProcessReceipt realiza la tarea crítica de determinar cuándo finalizar una compra. ProcessReceipt se llama en escenarios muy específicos. Para su conjunto de garantías, consulte MarketplaceService.ProcessReceipt.

Aunque la definición de "manejar" una compra puede diferir entre juegos, utilizamos los siguientes criterios:

  1. La compra no ha sido manejada previamente.

  2. La compra se refleja en la sesión actual.

  3. La compra se ha guardado en un DataStore.

    Cada compra, incluso los consumibles de una sola vez, debe reflejarse en el DataStore para que el historial de compras de los usuarios se incluya con sus datos de sesión.

Esto requiere realizar las siguientes operaciones antes de devolver PurchaseGranted:

  1. Verificar que el PurchaseId no se haya registrado previamente como manejado.
  2. Otorgar la compra en los datos de jugador en memoria.
  3. Registrar el PurchaseId como manejado en los datos de jugador en memoria.
  4. Escribir los datos de jugador en memoria en el DataStore.

El bloqueo de sesión simplifica este flujo, ya que ya no necesita preocuparse por los siguientes escenarios:

  • Los datos de jugador en memoria en el servidor actual pueden estar desactualizados, lo que requiere que obtenga el valor más reciente del DataStore antes de verificar el historial de PurchaseId.
  • La devolución de llamada para la misma compra que se ejecuta en otro servidor, lo que requiere que lea y escriba el historial de PurchaseId y guarde los datos de jugador actualizados con la compra reflejada atómicamente para prevenir condiciones de carrera.

El bloqueo de sesión garantiza que, si un intento de escribir en el DataStore del jugador tiene éxito, ningún otro servidor ha leído o escrito con éxito en el DataStore del jugador entre la carga y el guardado de los datos en este servidor. En resumen, los datos de jugador en memoria en este servidor son la versión más actualizada disponible. Hay algunas advertencias, pero no afectan este comportamiento.

Enfoque

Los comentarios en ReceiptProcessor describen el enfoque:

  1. Verifique que los datos del jugador estén actualmente cargados en este servidor y que se hayan cargado sin errores.

    Debido a que este sistema utiliza bloqueo de sesión, esta verificación también asegura que los datos en memoria sean la versión más actualizada.

    Si los datos del jugador no se han cargado aún (lo cual es esperado cuando un jugador se une a un juego), espere a que se carguen los datos del jugador. El sistema también escucha si el jugador sale del juego antes de que se carguen sus datos, ya que no debe ceder indefinidamente y bloquear esta devolución de llamada para que no se invoque nuevamente en este servidor para esta compra si el jugador vuelve a unirse.

  2. Verifique que el PurchaseId no esté ya registrado como procesado en los datos del jugador.

    Debido al bloqueo de sesión, el arreglo de PurchaseIds que el sistema tiene en memoria es la versión más actualizada. Si el PurchaseId está registrado como procesado y reflejado en un valor que se ha cargado o guardado en el DataStore, devuelva PurchaseGranted. Si está registrado como procesado, pero no reflejado en el DataStore, devuelva NotProcessedYet.

  3. Actualice los datos del jugador localmente en este servidor para "otorgar" la compra.

    ReceiptProcessor toma un enfoque de devolución de llamada genérico y asigna una devolución de llamada diferente para cada DeveloperProductId.

  4. Actualice los datos del jugador localmente en este servidor para almacenar el PurchaseId.

  5. Envíe una solicitud para guardar los datos en memoria en el DataStore, devolviendo PurchaseGranted si la solicitud tiene éxito. Si no, devuelva NotProcessedYet.

    Si esta solicitud de guardado no tiene éxito, una solicitud posterior para guardar los datos de sesión en memoria del jugador aún podría tener éxito. Durante la próxima llamada a ProcessReceipt, el paso 2 maneja esta situación y devuelve PurchaseGranted.

Datos de jugadores

Singletons: PlayerData.Server, PlayerData.Client

Antecedentes

Los módulos que proporcionan una interfaz para que el código lea y escriba datos de sesión de jugadores de manera sincrónica son comunes en los juegos de Roblox. Esta sección cubre PlayerData.Server y PlayerData.Client.

Enfoque

PlayerData.Server y PlayerData.Client manejan lo siguiente:

  1. Cargar los datos del jugador en memoria, incluyendo el manejo de casos en los que falla la carga.
  2. Proporcionar una interfaz para que el código del servidor consulte y cambie los datos del jugador.
  3. Replicar cambios en los datos del jugador al cliente para que el código del cliente pueda acceder a ellos.
  4. Replicar errores de carga y/o guardado al cliente para que pueda mostrar cuadros de diálogo de error.
  5. Guardar los datos del jugador periódicamente, cuando el jugador se va y cuando el servidor se apaga.

Cargar datos del jugador

Un diagrama de proceso que ilustra el sistema de carga
  1. SessionLockedDataStoreWrapper realiza una solicitud getAsync al almacén de datos.

    Si esta solicitud falla, se utilizan los datos predeterminados y el perfil se marca como "con error" para asegurar que no se escriba en el almacén de datos más tarde.

    Una opción alternativa es expulsar al jugador, pero recomendamos permitir que el jugador juegue con datos predeterminados y un mensaje claro sobre lo que ocurrió en lugar de sacarlo del juego.

  2. Se envía una carga inicial a PlayerDataClient que contiene los datos cargados y el estado de error (si lo hay).

  3. Se reanudan cualquier hilo cedido utilizando waitForDataLoadAsync para el jugador.

Proporcionar una interfaz para el código del servidor

  • PlayerDataServer es un singleton que puede ser requerido y accedido por cualquier código del servidor que se ejecute en el mismo entorno.
  • Los datos del jugador están organizados en un diccionario de claves y valores. Puede manipular estos valores en el servidor utilizando los métodos setValue, getValue, updateValue y removeValue. Todos estos métodos operan de manera sincrónica sin ceder.
  • Los métodos hasLoaded y waitForDataLoadAsync están disponibles para asegurar que los datos se hayan cargado antes de acceder a ellos. Recomendamos hacer esto una vez durante una pantalla de carga antes de que se inicien otros sistemas para evitar tener que verificar errores de carga antes de cada interacción con los datos en el cliente.
  • Un método hasErrored puede consultar si la carga inicial del jugador falló, lo que le obliga a usar datos predeterminados. Verifique este método antes de permitir que el jugador realice compras, ya que las compras no se pueden guardar en los datos sin una carga exitosa.
  • Una señal playerDataUpdated se activa con el player, key y value cada vez que se cambian los datos de un jugador. Los sistemas individuales pueden suscribirse a esto.

Replicar cambios al cliente

  • Cualquier cambio en los datos del jugador en PlayerDataServer se replica a PlayerDataClient, a menos que esa clave se haya marcado como privada utilizando setValueAsPrivate.
    • setValueAsPrivate se utiliza para denotar claves que no deben enviarse al cliente.
  • PlayerDataClient incluye un método para obtener el valor de una clave (get) y una señal que se activa cuando se actualiza (updated). También se incluye un método hasLoaded y una señal loaded, para que el cliente pueda esperar a que los datos se carguen y se repliquen antes de iniciar sus sistemas.
  • PlayerDataClient es un singleton que puede ser requerido y accedido por cualquier código del cliente que se ejecute en el mismo entorno.

Replicar errores al cliente

  • Los estados de error encontrados al guardar o cargar datos de jugadores se replican a PlayerDataClient.
  • Acceda a esta información con los métodos getLoadError y getSaveError, junto con las señales loaded y saved.
  • Hay dos tipos de errores: DataStoreError (la solicitud de DataStoreService falló) y SessionLocked (ver Bloqueo de Sesión).
  • Utilice estos eventos para deshabilitar los mensajes de compra del cliente e implementar cuadros de diálogo de advertencia. Esta imagen muestra un cuadro de diálogo de ejemplo:
Una captura de pantalla de un ejemplo de advertencia que podría mostrarse cuando los datos del jugador no se cargan

Guardar datos del jugador

Un diagrama de proceso que ilustra el sistema de guardado
  1. Cuando el jugador deja el juego, el sistema toma los siguientes pasos:

    1. Verifique si es seguro escribir los datos del jugador en el almacén de datos. Los escenarios en los que no sería seguro incluyen que los datos del jugador no se carguen o aún estén en proceso de carga.
    2. Realice una solicitud a través de SessionLockedDataStoreWrapper para escribir el valor actual de los datos en memoria en el almacén de datos y eliminar el bloqueo de sesión una vez completado.
    3. Limpie los datos del jugador (y otras variables como metadatos y estados de error) de la memoria del servidor.
  2. En un bucle periódico, el servidor escribe los datos de cada jugador en el almacén de datos (siempre que sea seguro guardar). Esta bienvenida redundancia mitiga la pérdida en caso de un fallo del servidor y también es necesaria para mantener el bloqueo de sesión.

    El ejemplo inicia un bucle compartido después de AUTO_SAVE_INTERVAL segundos (180 por defecto) y luego guarda a cada jugador cargado en paralelo. Ese bucle no compensa servidores o jugadores, por lo que los servidores que comienzan en momentos similares pueden vaciarse juntos.

    Compense el primer guardado de cada jugador por una duración aleatoria dentro del intervalo para que los servidores en vivo no escriban todos al mismo tiempo:

    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. Cuando se recibe una solicitud para apagar el servidor, ocurre lo siguiente en un callback de BindToClose:

    1. Se realiza una solicitud para guardar los datos de cada jugador en el servidor, siguiendo el proceso normalmente seguido cuando un jugador deja el servidor. Estas solicitudes se realizan en paralelo, ya que los callbacks de BindToClose solo tienen 30 segundos para completarse.
    2. Para acelerar los guardados, se eliminan todas las demás solicitudes en la cola de cada clave del DataStoreWrapper subyacente (ver Reintentos).
    3. El callback no devuelve hasta que todas las solicitudes se hayan completado.
©2026 Roblox Corporation. Roblox, el logotipo de Roblox y "Powering Imagination" son algunas de nuestras marcas registradas y no registradas en los Estados Unidos y otros países.