Implementar datos del jugador y sistemas de compra

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

Fondo

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

La mayoría de las experiencias en Roblox usan estas API para implementar algún tipo de 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 muestran algunos de los problemas más comunes que los sistemas de datos de jugadores intentan resolver:

  • En el acceso a la memoria: DataStoreService solicitudes hacen solicitudes web que operan de forma asíncrona y están sujetas a límites de velocidad.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.Los sistemas de almacenamiento de datos del jugador de la mayoría de los desarrolladores almacenan estos datos en memoria en el servidor de Roblox, limitando las solicitudes DataStoreService a los siguientes escenarios:

    • Lectura inicial al comenzar una sesión
    • Escritura final al final de la sesión
    • Escrituras periódicas a intervalos para mitigar el escenario en el que el último escrito falla
    • Escribe para asegurarse de que los datos se guarden mientras se procesa una comprar
  • Almacenamiento eficiente: Almacenar todos los datos de sesión de un jugador en una sola tabla le permite actualizar múltiples valores atómicamente y manejar la misma cantidad de datos en menos solicitudes.También elimina el riesgo de dessincronización entre valores y facilita las devoluciones más fácilmente razonables.

    Algunos desarrolladores también implementan serialización personalizada para comprimir grandes estructuras de datos (típicamente para guardar el 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 los datos del jugador al cliente te permite transmitir esta información sin tener que crear sistemas de replicación personalizados para cada componente de los datos.Los desarrolladores a menudo quieren que la opción sea selectiva sobre lo que se replica al cliente y lo que no.

  • Manejo de errores: Cuando los almacenes de datos no se pueden acceder, la mayoría de las soluciones implementarán un mecanismo de reintento y un fallo a los datos 'predeterminados'.Se necesita cuidado especial para garantizar que los datos de reemplazo no reemplacen más tarde los datos "reales", y que esto se comunique al jugador apropiadamente.

  • Reintentos: Cuando los almacenes de datos no son accesibles, la mayoría de las soluciones implementan un mecanismo de reintento y un fallback a los datos predeterminados.Tome especial cuidado para asegurarse de que los datos de reemplazo no reemplacen más tarde los datos "reales", y comunique la situación al jugador apropiadamente.

  • 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 guarde información obsoleta.Esto puede conducir a la pérdida de datos y a las lagunas de duplicación de elementos comunes.

  • Procesamiento de compras atómicas: Verifique, otorgue y registre las compras atómicamente para evitar que los artículos se pierdan o se otorguen varias veces.

códigode muestra

Roblox tiene un código de referencia para ayudarlo con el diseño y la construcción de sistemas de datos de jugadores.El resto de esta página examina los detalles de fondo, la implementación y las advertencias generales.


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

Explorer window showing the purchasing system model.

Arquitectura

Este diagrama de alto nivel ilustra los sistemas clave en la muestra y cómo se interfieren con el código en el resto de la experiencia.

An architecture diagram for the code sample.

Reintentos

Clase: DataStoreWrapper

Fondo

Como DataStoreService hace solicitudes web bajo el capó, sus solicitudes no están garantizadas de tener éxito.Cuando esto sucede, los métodos DataStore lanzan errores, lo que te permite manejarlos.

Un "gotcha" común puede ocurrir si intentas manejar fallos de almacenamiento de datos como este:


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

Aunque este es un mecanismo de reintento perfectamente válido para una función genérica, no es adecuado para solicitudes DataStoreService porque no garantiza el orden en que se hacen las solicitudes.Preservar el orden de las solicitudes es importante para las solicitudes DataStoreService porque interactúan con el estado.Considera el siguiente escenario:

  1. La solicitud A se hace para establecer el valor de la clave K a 1.
  2. La solicitud falla, por lo que se programó una nueva ejecución en 2 segundos.
  3. Antes de que ocurra el nuevo intento, solicite B establece el valor de K a 2, pero el nuevo intento de solicitud A anula inmediatamente este valor y establece K a 1.

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

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

Aproximación

An process diagram illustrating the retry system

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

Estos métodos, cuando se llaman:

  1. Añade 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 rinde hasta que la solicitud se haya completado.

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

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

  3. Cuando una solicitud se completa, 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 determinada y despejar las solicitudes caducas.La última opción es particularmente útil en escenarios en los que el servidor se está cerrando y no hay tiempo para procesar ninguna solicitud más reciente.

Caveatas

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

  1. Es difícil decidir sobre un conjunto intuitivo de reglas para cuando una solicitud es segura de 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 se hizo 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 eliminan las solicitudes viejas tan lejos como la solicitud de lectura más reciente. UpdateAsync() , por mucho 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 dentro de este diseño esto sin agregar complejidad adicional.

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

  2. Una vez eliminado de la cola, es difícil decidir sobre una regla intuitiva para cómo debería manejarse esto.Cuando se hace una solicitud DataStoreWrapper se renuncia al hilo actual hasta que se complete.Si eliminamos las solicitudes estancadas de la cola, tendríamos que decidir si devolver false, "Removed from queue" o nunca devolver y descartar el subprocesoactivo.Ambos enfoques vienen con sus propias desventajas y transfieren una mayor complejidad al consumidor.

En última instancia, nuestra opinión es que el enfoque simple (el procesamiento de cada solicitud) es preferible aquí y crea un entorno más claro para navegar cuando se abordan problemas complejos 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 de las llamadas de función individual ya no es una preocupación en curso.Para cubrir esto, exponemos un método skipAllQueuesToLastEnqueued .Para más contexto, vea Datos del jugador.

Bloqueo de sesión

Clase: SessionLockedDataStoreWrapper

Fondo

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

Para que este modelo funcione como se pretendía, es imperativo que no más de un servidor sea capaz de cargar los datos de un jugador en la 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 sobre ellos durante un guardado final.Sin un mecanismo de bloqueo, el servidor B podría cargar datos desactualados del 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.Entonces, si el servidor A guarda sus datos más recientes después de que el servidor B cargue los datos obsoletos, el servidor B reemplazaría esos datos más recientes durante su próximo guardado.

Aunque Roblox solo permite que un cliente se conecte a un servidor a la vez, no puedes asumir que los datos de una sesión siempre se guardan antes de que comience la siguiente sesión.Considera los siguientes escenarios que pueden ocurrir cuando un jugador abandona el servidor A:

  1. El servidor A hace una solicitud DataStore para guardar sus datos, pero la solicitud falla y requiere varios intentos para completarse con éxito.Durante el período de reintento, el jugador se une al servidor B.
  2. El servidor A hace demasiadas llamadas UpdateAsync() a la misma clave y se ve 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, parte del código conectado al evento PlayerRemoving se muestra 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 al punto de que el guardado final se retrasa hasta que el jugador se una al servidor B.

Estos escenarios deberían ser raros, pero producirse, particularmente en situaciones en las que un jugador se desconecta de un servidor y se conecta a otro en rápida sucesión (por ejemplo, mientras se teletransporta).Algunos usuarios maliciosos incluso pueden 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.

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

Aproximación

An process diagram illustrating the session locking system

SessionLockedDataStoreWrapper es un envoltorio meta alrededor de la clase DataStoreWrapper. proporciona funcionalidad de cola y de reintentar, que se complementa con el 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 sea leída y escrita a nivel atómico.También es posible abandonar la escritura basada en el valor leído devolviendo nil en la devolución de llamadade transformación.

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

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

    • El objeto de metadatos de la clave no incluye un valor no reconocido LockId que se actualizó por última vez menos de un tiempo de expiración de bloqueo.Esto se debe al respeto de un bloqueo colocado por otro servidor y a la ignorancia de ese bloqueo si expiró.

    • Si este servidor ha colocado su propio valor LockId en el metadato de la clave anteriormente, entonces este valor aún está en el metadato de la clave.Esto explica la situación en la que otro servidor ha asumido el bloqueo de este servidor (por expiración o por fuerza) y lo ha liberado más tarde.Alternativamente expresado, incluso si LockId es nil , otro servidor aún podría haber reemplazado y eliminado una cerradura en el tiempo desde que bloqueaste la clave.

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

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

    1. Si la clave debe ser bloqueada, UpdateAsync establece el LockId en el metadato 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 una cerradura en esta clave, no realiza cambios.También programar una tarea para advertirte si no accedes nuevamente a la clave para mantener la bloqueo dentro del tiempo de expiración de la clave.

    2. Si la clave debe desbloquearse, UpdateAsync elimina el LockId en el metadato de la clave.

Se pasa un manejador de reintentos personalizado al subyacente DataStoreWrapper para que se vuelva a intentar la operación si se abortó en el paso 1 debido a que la sesión estaba bloqueada.

Se devuelve un mensaje de error personalizado al consumidor, permitiendo que el sistema de datos del jugador informe de un error alternativo en el caso de bloqueo de sesión al cliente.

Caveatas

El régimen de bloqueo de sesión se basa en un servidor que siempre libera su bloqueo sobre una clave cuando termina con él.Esto siempre debe ocurrir 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 fue operable para todos los intentos de acceder a la clave.
  • Debido a un error en la lógica o a un bug similar, no se hizo la instrucción para desbloquear la clave.

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

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

Procesamiento de productos para desarrolladores

Singleton: ReceiptHandler

Fondo

La llamada de devolución ProcessReceipt realiza el trabajo crítico de determinar cuándo finalizar una comprar.ProcessReceipt se llama en escenarios muy específicos.Para su conjunto de garantías, vea MarketplaceService.ProcessReceipt .

Aunque la definición de "manejo" de una compra puede diferir entre experiencias, usamos los siguientes criterios

  1. La compra no se ha manejado previamente.

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

  3. La compra se ha guardado en un DataStore .

    Cada comprar, incluso consumibles de un solo uso, 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. Verifique que el PurchaseId no se haya registrado ya como manejado.
  2. Recompensar la compra en los datos del jugador en memoria del jugador.
  3. Registre el PurchaseId como se maneja en los datos del jugador en memoria del jugador.
  4. Escribe los datos del jugador en memoria del jugador al DataStore.

La bloqueación de sesión simplifica este flujo, ya que ya no necesitas preocuparte por los siguientes escenarios:

  • Los datos del jugador en memoria en el servidor actual potencialmente desactualizados, requiriendo que obtenga el último valor de la DataStore antes de verificar el historial de PurchaseId
  • La llamada de devolución para la misma compra que se ejecuta en otro servidor, que requiere que ambos lean y escriban el historial de PurchaseId y guarden los datos actualizados del reproductor con la compra reflejada atómicamente para evitar las condiciones de carrera

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

Aproximación

Los comentarios en ReceiptProcessor resumen el enfoque:

  1. Verifica que los datos del jugador se carguen actualmente en este servidor y que se carguen sin errores.

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

    Si los datos del jugador aún no se han cargado (lo que se espera cuando un jugador se une a un juego), espere a que se cargarlos datos del jugador.El sistema también escucha al jugador que deja el juego antes de que se carguen sus datos, ya que no debería rendirse indefinidamente y bloquear esta llamada de nuevo en este servidor para esta compra si el jugador se vuelve a unir.

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

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

  3. Actualice los datos del reproductor localmente en este servidor para "otorgar" la comprar.

    ReceiptProcessor toma un enfoque de llamada genérica y asigna un llamado diferente para cada DeveloperProductId.

  4. Actualice los datos del reproductor 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 siguiente llamada ProcessReceipt, el paso 2 maneja esta situación y devuelve PurchaseGranted.

Datos del jugador

Singletons: PlayerData.Server , PlayerData.Client

Fondo

Los módulos que proporcionan una interfaz para que el código del juego se lea y escriba sincronizadamente los datos de la sesión del jugador son comunes en las experiencias de Roblox.Esta sección cubre PlayerData.Server y PlayerData.Client.

Aproximación

PlayerData.Server y PlayerData.Client manejan lo siguiendo:

  1. Cargar los datos del jugador en la memoria, incluida la gestión de casos en los que no se cargar
  2. Proporcionar una interfaz para el código del servidor para consultar y cambiar 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 diálogos de error
  5. Guardar los datos del jugador periódicamente, cuando el jugador se va, y cuando el servidor se cierra

Cargar datos del jugador

An process diagram illustrating the loading system
  1. SessionLockedDataStoreWrapper hace una solicitud getAsync al tiendade datos.

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

    Una opción alternativa es expulsar al jugador, pero recomendamos dejar que el jugador juegue con datos predeterminados y despejar la comunicación sobre lo que ocurrió en lugar de eliminarlo de la experiencia.

  2. Se envía un pago inicial a PlayerDataClient que contiene los datos cargados y el estado de error (si es necesario).

  3. Cualquier hilo obtenido usando waitForDataLoadAsync para el jugador se reanuda.

Proporcionar una interfaz para el código del servidor

  • PlayerDataServer es un singleton que se puede requerir y acceder por cualquier código de servidor que se ejecute en el mismo entorno, ambiente.
  • Los datos del jugador se organizan en un diccionario de claves y valores.Puedes manipular estos valores en el servidor usando los métodos setValue, getValue, updateValue y removeValue.Estos métodos todos operan de forma sincronizada sin rendirse.
  • Los métodos hasLoaded y waitForDataLoadAsync están disponibles para garantizar que los datos se hayan cargado antes de que los accedas.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 datos en el cliente.
  • Un método hasErrored puede consultar si la carga inicial del jugador falló, lo que les hace usar datos predeterminados.Compruebe este método antes de permitir que el jugador realice cualquier compra, ya que las compras no se pueden guardar en datos sin una cargarexitosa.
  • Una señal A 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 usando setValueAsPrivate
    • setValueAsPrivate se usa para denotar las claves que no deben enviarse al cliente
  • PlayerDataClient incluye un método para obtener el valor de una clave (obtener) y una señal que se activa cuando se actualiza (actualizada).También se incluyen un método hasLoaded y una señal loaded, por lo que el cliente puede esperar a que se carguen y se replicen los datos antes de iniciar sus sistemas
  • PlayerDataClient es un singleton que se puede requerir y acceder por cualquier código de cliente que se ejecute en el mismo entorno, ambiente

Replicar errores al cliente

  • Los estados de error que se encuentran al guardar o cargar datos del jugador 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 DataStoreService falló) y SessionLocked (ver Bloqueo de sesión).
  • Utilice estos eventos para deshabilitar los mensajes de compra del cliente y implementar diálogos de advertencia. Esta imagen muestra un diálogo de ejemplo:
A screenshot of an example warning that could be shown when player data fails to load

Guardar datos del jugador

A process diagram illustrating the saving system
  1. Cuando el jugador abandona el juego, el sistema realiza los siguientes pasos:

    1. Compruebe si es seguro escribir los datos del jugador en el tiendade datos.Los escenarios en los que podría ser inseguro incluyen los datos del jugador que no se cargan o aún están cargando.
    2. Haz una solicitud a través del SessionLockedDataStoreWrapper para escribir el valor actual de los datos en memoria en el almacén de datos y eliminar la bloqueo de sesión una vez completado.
    3. Limpia 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 guardarlos).Esta redundancia de bienvenida mitiga la pérdida en caso de un error del servidor y también es necesaria para mantener el bloqueo de sesión.

  3. Cuando se recibe una solicitud para apagar el servidor, ocurre lo siguiente en una devolución de llamadade devolución BindToClose

    1. Se solicita guardar los datos de cada jugador en el servidor, siguiendo el proceso que normalmente se pasa cuando un jugador deja el servidor.Estas solicitudes se hacen en paralelo, ya que los BindToClose llamados de devolución solo tienen 30 segundos para completarse.
    2. Para agilizar los guardados, todas las demás solicitudes en la cola de cada clave se eliminan del subyacente DataStoreWrapper (ver Reintentos).
    3. La llamada de devolución no regresa hasta que todas las solicitudes se hayan completado.