Roblox GuidebookBase de conocimiento
Español ⌄

Desarrollo / ROBLOX

UpdateAsync en Roblox: por qué dos guardados pueden perder un cambio

Dos servidores ficticios de un taller leen 100 y suman 5 cada uno, pero el total guardado queda en 105. Distinguimos reemplazar una copia antigua de transformar el estado actual, dibujamos el conflicto y comprobamos una función pura sin usar un almacén real.

Actualizado:

Investiga la secuencia antes de culpar al botón #

Imagina un contador compartido para entregas completadas en un ejercicio. Dos manejadores independientes deben sumar cinco cada uno. Ambos anuncian su trabajo terminado, pero el total solo aumenta una vez. Puede ocurrir si calcularon sus propuestas a partir de la misma copia antigua. El síntoma no demuestra la causa concreta en tu juego: primero registra acciones y condiciones de escritura.

Esta guía continúa el primer guardado, pero resuelve otra cuestión: el resultado depende de un estado que otro servidor puede cambiar. Usamos un contador aislado de aprendizaje, no una cartera, una compra ni el perfil completo de un jugador. Los números y las secuencias son inventados. No se iniciaron dos servidores reales de Roblox para producir estos ejemplos.

Dibuja la secuencia con copias antiguas #

Escribe cinco filas: el almacén contiene 100; A lee 100; B lee 100; A escribe 105; B escribe sus propios 105. B no necesita mala intención para perder el cambio de A. Reemplaza la entrada con un cálculo hecho antes de la primera escritura. Dos incrementos deberían dar 110, mientras dos reemplazos iguales dejan 105.

Conserva el dibujo junto a la tarea. Permite distinguir un manejador que no se ejecutó, una llamada fallida y escrituras realizadas sobre una base obsoleta. En el ejercicio local bastan el nombre de la operación, un identificador ficticio del pedido, su entrada y su propuesta. No necesitas datos privados ni credenciales del jugador para demostrar este conflicto.

Copias antiguas pierden un cambioAbrir imagen a tamaño completo ↗
Secuencia ficticia de reemplazos; no es un registro de servidores reales.
PasoAcciónValor guardado
1Inicio100
2A lee 100100
3B lee 100100
4A escribe 105105
5B escribe 105105

Reemplazar y transformar expresan intenciones distintas #

SetAsync establece el valor de una clave sin leer antes su estado dentro de ese método. Encaja con un reemplazo conocido que no depende del valor anterior. Para sumar al contador actual importa la base del cálculo. Roblox recomienda UpdateAsync cuando una escritura depende del estado actual o varios servidores pueden escribir la misma clave.

Cambiar únicamente el nombre del método y devolver los 105 calculados previamente no arregla la secuencia. La propuesta continúa basada en la copia antigua. Calcula desde current, el argumento recibido por el callback. Explica esa diferencia antes de trasladarla a un inventario: transformar un estado actual no equivale a reenviar tu antigua instantánea.

El callback puede ejecutarse otra vez #

Si otro servidor cambia la clave entre la lectura y la escritura, UpdateAsync puede descartar la propuesta anterior y volver a llamar la transformación. Una llamada al método no implica una única ejecución del callback. En la secuencia ficticia A propone 105, B guarda primero 105 y A recalcula desde 105 para proponer 110.

La transformación repetida no debería volver a otorgar un objeto, enviar un evento de recompensa ni modificar un objeto externo del juego. Su tarea es calcular la entrada propuesta. Los efectos del juego requieren otra decisión basada en un estado confirmado. Mover la entrega del objeto después de una llamada exitosa tampoco demuestra por sí solo que otro manejador no pueda solicitar independientemente la misma operación.

Recalcular desde el estado actualAbrir imagen a tamaño completo ↗
Recálculo ficticio tras otro escritor. El modelo local no verifica la red real de Roblox.

Una función educativa sin estado oculto #

El ModuleScript CounterTransform ofrece Add(current, delta). Trata una entrada inexistente como cero, acepta valores enteros no negativos y un incremento positivo, y limita el resultado a un millón. Son reglas elegidas para este contador. El estado inicial y el rango permitido pueden cambiar en otro juego; no son reglas universales para perfiles.

La función devuelve otro número sin modificar variables externas. Entradas iguales producen salidas iguales. Un formato incorrecto, un incremento negativo o fraccionario y superar el límite devuelven nil. No contiene red, esperas, recompensas ni acceso al jugador. Ejecutamos pruebas locales de Luau de esta función; no verifican el comportamiento del almacén real de Roblox.

-- Pure transform for an isolated teaching counter, not a player profile.
local Transform = {}
local LIMIT = 1000000

local function validInteger(value)
    return type(value) == "number" and value == math.floor(value)
        and value >= 0 and value <= LIMIT
end

function Transform.Add(current, delta)
    if not validInteger(delta) or delta == 0 then return nil end
    if current == nil then current = 0 end
    if not validInteger(current) then return nil end
    if delta > LIMIT - current then return nil end
    return current + delta
end

return Transform

Conecta la transformación en una prueba aislada #

Coloca CounterTransform en ServerScriptService de una experience de prueba separada. Un Script del servidor obtiene DataStoreService, elige el almacén del contador educativo y carga el módulo con require. Pasa a UpdateAsync un callback que devuelva CounterTransform.Add(current, 5). No incluyas task.wait, carga de recursos ni otra operación que suspenda la ejecución dentro de la transformación.

UpdateAsync en sí es una operación de red. Envuélvela en pcall e inspecciona el valor devuelto por separado. Que pcall termine correctamente indica que no capturó un error. Si el callback cancela con nil, eso no confirma haber guardado cinco. Un nombre que diga «prueba» no aísla automáticamente un almacén si la experience y las claves siguen usando datos de producción.

-- Server Script in an isolated test experience only.
-- This demonstrates one numeric teaching key, not a player profile.
local DataStoreService = game:GetService("DataStoreService")
local CounterTransform = require(
    game:GetService("ServerScriptService"):WaitForChild("CounterTransform")
)
local store = DataStoreService:GetDataStore("GuidebookCounterExercise_v1")

local ok, result = pcall(function()
    return store:UpdateAsync("CounterExercise", function(current)
        return CounterTransform.Add(current, 5)
    end)
end)

if not ok then
    warn("Write response failed; outcome is not established", result)
elseif result == nil then
    warn("Update cancelled; no new saved counter was returned")
else
    print("Updated teaching counter", result)
end

Cancelar no debe convertirse en reiniciar #

Devolver nil desde el callback cancela la actualización. En el ejercicio sirve cuando la entrada tiene un tipo inesperado o la propuesta supera el límite. No conviertas una cadena inválida en cero para continuar. Podrías ocultar un problema de datos y sobrescribir un registro que necesita investigación. Una entrada inexistente y una entrada existente mal formada son casos distintos.

Para diagnosticar, registra que falta el número guardado esperado y detén los pasos siguientes de esta ruta educativa. La función compacta no proporciona motivos detallados del rechazo. Si necesitas ese registro, diséñalo para que una repetición del callback no se convierta en otra acción del juego ni cambie la base del cálculo siguiente.

Comprueba por separado el modelo y la función #

Reproduce primero la secuencia incorrecta: dos copias de 100, dos propuestas de 105 y dos reemplazos. Después modela el recálculo: prepara la propuesta de A, aplica el cambio de B, descarta la propuesta antigua de A y calcula desde 105. El resultado final debe ser 110. Es un modelo determinista de dos secuencias, no una emulación de todas las reglas internas de Roblox.

Prueba también la entrada inexistente, un entero normal, una cadena, una fracción, un negativo, infinito y el límite superior. Repetir las mismas entradas no debe aumentar el resultado mediante estado oculto. Una tabla pasada en lugar de un número debe permanecer intacta. La prueba local verifica esas propiedades concretas, no la resistencia de un servidor de producción.

Prueba localResultado esperado
Sin entrada; sumar 55
Actual 100; sumar 5105
Actual 105; sumar 5110
Cadena en lugar de númeronil: cancelar
999995; sumar 51000000
999996; sumar 5nil: cancelar

Repetir el callback no equivale a repetir la operación #

Una repetición dentro de una actualización ajusta la propuesta al estado cambiado. Una segunda petición independiente de sumar cinco es otra operación y puede sumar cinco más aunque utilice UpdateAsync. El método no es una protección completa contra recompensas duplicadas, pedidos repetidos o compras procesadas varias veces.

Ese problema necesita una identidad definida de la operación y una regla para comprobar si ya terminó junto con el cambio de datos. Aquí no implementamos ese mecanismo. Un contador escalar no guarda un historial de operaciones. Entrega esta limitación al próximo chat de desarrollo junto con el código para que no confunda el ejercicio con una cartera terminada.

Una respuesta fallida deja otra pregunta abierta #

Un error de red no demuestra automáticamente que el backend no escribió nada. Roblox documenta escrituras con resultado desconocido: puede faltar una respuesta exitosa después de haberse guardado el cambio. Una nueva petición incondicional de sumar cinco podría repetir la operación independiente. Eso es distinto del recálculo interno del callback.

No añadas al ejemplo un bucle infinito de reintentos. Para un sistema real, define primero el tratamiento de resultados desconocidos y el orden de operaciones de cada clave. Después elige reintentos limitados para fallos temporales y retrasos adecuados. La prueba local no provoca un fallo de red de Roblox ni demuestra una solución a este problema; sigue siendo una tarea separada.

Un método no administra un perfil completo #

Un perfil puede contener campos relacionados, propiedad de la sesión y metadatos. El ejemplo numérico no muestra cómo conservarlos al escribir. Tampoco asigna un propietario de sesión ni evita que un servidor antiguo guarde su copia después de que el jugador pase a otro servidor. Afirmar que UpdateAsync resuelve esas tareas ocultaría trabajo pendiente.

No uses el contador educativo para compras con Robux o moneda real del juego. Una adaptación de producción necesita su propio esquema, comprobaciones de compatibilidad, tratamiento de fallos y reglas de recompensa. Empieza por la documentación de player data y purchasing de Roblox y verifica luego tu implementación concreta. El experimento explica una transformación, no proporciona una biblioteca de perfiles.

Prepara una entrega honesta al desarrollador #

Comparte ambos dibujos, el nombre de la clave aislada, las reglas numéricas y las pruebas locales completadas. Indica expresamente que no se probaron servidores reales ni conflictos de red. Enumera por separado los requisitos pendientes: resultado desconocido, operación de negocio repetida, propiedad de sesión y conservación de otros campos del perfil.

Al comparar soluciones, pregunta desde qué valor se calcula cada propuesta y qué efectos ocurren fuera del callback. La respuesta debe explicar una secuencia concreta. Si solo funciona sin un segundo escritor, el conflicto sigue sin resolver. Conserva esta guía con la del primer guardado para distinguir acceso inicial al almacén de coordinación de cambios posteriores.

Fuentes originales

Roblox Creator Hub — Data stores
Roblox Creator Hub — GlobalDataStore
Roblox Creator Hub — Data store best practices