Inventario en Roblox que se guarda de verdad

Claim Your Loot

·

Portada del artículo «Un inventario que se guarda», de la sección Sistemas de botín

Ya sueltas objetos y ya tienes tienda. Ahora hace falta el sitio donde viven esos objetos entre partida y partida: el inventario. Y aquí es donde mucha gente se mete en un lío, porque intenta guardar en el DataStore lo que ve en pantalla en vez de guardar datos.

En la demo tienes un inventario de doce huecos: mete objetos, apílalos, tíralos, y mira abajo exactamente lo que se guardaría.

Respuesta rápida

El inventario es una tabla de identificadores cortos y cantidades que vive en el servidor. Ni Parts, ni Tools, ni colores: {esp_hierro = 1, pocion = 4}. Lo que se ve en pantalla se dibuja a partir de esa tabla, nunca al revés.

Lo que necesitas antes de empezar

Paso 1: separar el catálogo de lo que tiene el jugador

Dos cosas distintas que la gente mezcla:

  • El catálogo es fijo, igual para todos, y vive en un ModuleScript: qué es cada objeto, cómo se llama, qué icono tiene, si se apila.
  • El inventario es de cada jugador y solo guarda cuántos tiene de qué.
-- ReplicatedStorage > Catalogo (ModuleScript)
return {
	esp_hierro = {nombre = "Espada de hierro", apila = false, icono = "rbxassetid://0"},
	pocion     = {nombre = "Poción",           apila = true,  max = 20},
	gema_azul  = {nombre = "Gema azul",        apila = true,  max = 99},
}
-- lo que se guarda de un jugador
{
	esp_hierro = 1,
	pocion = 4,
}

Guardar solo eso tiene dos ventajas enormes: ocupa nada, y el día que cambies el nombre o el icono de un objeto, cambia para todo el mundo sin tocar ni un guardado.

Catálogo e inventario por separado: en el DataStore solo van identificadores y cantidades

Paso 2: el módulo de inventario

-- ServerScriptService > Inventario (ModuleScript)
local Catalogo = require(game.ReplicatedStorage.Catalogo)

local Inventario = {}
local datos = {}          -- datos[jugador] = {id = cantidad}
local HUECOS = 12

function Inventario.cargar(jugador, guardado)
	datos[jugador] = guardado or {}
end

function Inventario.leer(jugador)
	return datos[jugador] or {}
end

local function huecosUsados(inv)
	local n = 0
	for _, _ in pairs(inv) do n = n + 1 end
	return n
end

function Inventario.agregar(jugador, id, cantidad)
	local articulo = Catalogo[id]
	if not articulo then return false, "ese objeto no existe" end

	cantidad = cantidad or 1
	local inv = datos[jugador]
	if not inv then return false, "sin cargar" end

	if inv[id] then
		if not articulo.apila then
			return false, "ya lo tienes"
		end
		local tope = articulo.max or 99
		if inv[id] + cantidad > tope then
			return false, "no te caben más"
		end
		inv[id] = inv[id] + cantidad
	else
		if huecosUsados(inv) >= HUECOS then
			return false, "inventario lleno"
		end
		inv[id] = cantidad
	end

	return true
end

function Inventario.quitar(jugador, id, cantidad)
	local inv = datos[jugador]
	if not inv then return false end
	if not inv[id] then return false end

	cantidad = cantidad or 1
	inv[id] = inv[id] - cantidad
	if inv[id] <= 0 then
		inv[id] = nil          -- si llega a cero, se borra: no dejes ceros guardados
	end
	return true
end

return Inventario

Dos cosas que importan aquí:

  • Devuelve false y un motivo. Quien llame decide qué hacer con el fallo; el módulo no enseña avisos ni sabe de interfaces.
  • Los ceros se borran. Si dejas pocion = 0 guardado, con el tiempo el DataStore de un jugador veterano se llena de basura.

Paso 3: enseñárselo al jugador

El cliente necesita ver el inventario, pero no puede tocarlo. Se lo mandas cuando cambie:

-- servidor
local actualizar = game.ReplicatedStorage.ActualizarInventario

local function avisar(jugador)
	actualizar:FireClient(jugador, Inventario.leer(jugador))
end
-- cliente
actualizar.OnClientEvent:Connect(function(inv)
	pintarHuecos(inv)      -- solo dibuja
end)

Si el cliente cambia su copia, lo único que consigue es mentirse a sí mismo: al siguiente FireClient vuelve la buena.

Paso 4: usar un objeto

local usar = game.ReplicatedStorage.UsarObjeto

usar.OnServerEvent:Connect(function(jugador, id)
	if type(id) ~= "string" then return end

	local inv = Inventario.leer(jugador)
	if not inv[id] then return end            -- no lo tiene: fin

	if id == "pocion" then
		local personaje = jugador.Character
		local hum = personaje and personaje:FindFirstChildOfClass("Humanoid")
		if not hum then return end
		if hum.Health >= hum.MaxHealth then
			return                             -- lleno: no gastamos la poción
		end

		hum.Health = math.min(hum.MaxHealth, hum.Health + 50)
		Inventario.quitar(jugador, "pocion", 1)
		avisar(jugador)
	end
end)

El orden vuelve a ser el de siempre: comprobar que lo tiene, comprobar que tiene sentido usarlo, aplicar el efecto y solo entonces restar.

Paso 5: guardarlo

Como el inventario ya es una tabla de textos y números, va directo:

local perfil = {
	monedas = 1240,
	inventario = Inventario.leer(jugador),
}
almacen:SetAsync("jug_" .. jugador.UserId, perfil)

Y al cargar:

Inventario.cargar(jugador, perfil.inventario)

Eso es todo. Si hubieras guardado Tools o Parts, aquí estarías peleándote con 104: Cannot store Instance in DataStore.

Paso 6: de la tabla a las herramientas de verdad

Si quieres que el jugador lleve la espada en la mano, la Tool se crea desde la tabla, no al revés:

local function equipar(jugador, id)
	local inv = Inventario.leer(jugador)
	if not inv[id] then return end

	local plantilla = game.ServerStorage.Objetos:FindFirstChild(id)
	if not plantilla then return end

	local mochila = jugador:FindFirstChild("Backpack")
	if not mochila then return end
	if mochila:FindFirstChild(plantilla.Name) then return end   -- ya la lleva

	plantilla:Clone().Parent = mochila
end

La Tool es una consecuencia del inventario. Si un día se pierde, se vuelve a crear; el dato bueno sigue en la tabla.

El día que cambies la forma del inventario

Va a pasar. Empiezas con id = cantidad y a los tres meses necesitas guardar también el nivel de mejora de cada objeto. Si sueltas el formato nuevo sin más, los jugadores que guardaron con el viejo cargan una tabla que tu código no sabe leer, y ahí es donde se pierden inventarios.

La solución es guardar la versión dentro del propio dato y convertir al cargar:

local VERSION_ACTUAL = 2

local function migrar(datos)
	local v = datos.version or 1

	if v == 1 then
		-- v1: pocion = 4   ->   v2: pocion = {n = 4, mejora = 0}
		local nuevo = {}
		for id, cantidad in pairs(datos.inventario or {}) do
			nuevo[id] = {n = cantidad, mejora = 0}
		end
		datos.inventario = nuevo
		datos.version = 2
	end

	return datos
end

Tres cosas que hacen que esto funcione a largo plazo:

  • Las migraciones se encadenan. De la 1 a la 2, y luego de la 2 a la 3 en el mismo if. Un jugador que vuelve después de un año pasa por todas seguidas.
  • El código de conversión no se borra nunca. Ocupa quince líneas y es lo único que sostiene a la gente que no entra desde hace meses.
  • Migrar no es guardar. Convierte al cargar y deja que el guardado normal escriba la versión nueva cuando toque; si escribes nada más migrar, multiplicas las escrituras al soltar la actualización.

Es lo mismo que hace el _v1 del nombre del almacén, pero sin tirar el progreso de nadie.

Pruébalo aquí

Doce huecos y lo que se guarda de verdad

Coge objetos, apílalos, llénalo. Abajo, en directo, la tabla que se manda al DataStore.

Lo que se guarda


  

Ni Parts, ni Tools, ni colores: identificadores y cantidades. Por eso cabe, se lee rápido y no da el error 104.

0 de 12 huecos

Errores habituales

104: Cannot store Instance in DataStore

Estás intentando guardar una Tool, un Part o un Color3. Guarda identificadores; el objeto se reconstruye al cargar.

El inventario se duplica al reconectar

Estás añadiendo los objetos otra vez en PlayerAdded además de cargar la tabla. Cargar y dar son cosas distintas.

Se llena de objeto = 0

Falta borrar la clave cuando la cantidad llega a cero.

El jugador se da objetos desde el cliente

El módulo de inventario tiene que estar en ServerScriptService, no en ReplicatedStorage. Si el cliente puede require, el cliente puede llamar.

Al cambiar el nombre de un objeto, los guardados viejos se rompen

Porque estás usando el nombre como clave. Usa un identificador corto y feo (esp_hierro) que no vayas a cambiar nunca, y guarda el nombre bonito en el catálogo.

El inventario lleno no avisa

agregar devuelve false y un motivo: pásalo al cliente y enséñalo. Un objeto que desaparece en silencio porque no cabía es una queja segura.

Preguntas frecuentes

¿Cuántos huecos debería tener?

Los que hagan que la tienda tenga sentido. Un inventario infinito quita toda decisión; uno de doce hace que el jugador elija, y de paso te da algo que vender (ampliar huecos es un gamepass clásico: cómo crear un gamepass).

¿Y si quiero que los objetos tengan estadísticas propias?

Entonces cada objeto es único y la tabla cambia de forma: en vez de id = cantidad, una lista de objetos con sus propiedades. Ocupa más, y ahí sí conviene vigilar el tamaño de lo que guardas.

¿Puedo dejar que los jugadores se intercambien objetos?

Sí, pero es la función más delicada de todo el juego: hay que quitar de uno y dar a otro de forma que no pueda fallar a medias. Hazlo con UpdateAsync, con confirmación por las dos partes y registrando cada intercambio.

¿Cómo ordeno el inventario?

Como pairs no garantiza orden, guarda también una lista de identificadores si quieres un orden estable, o ordena por nombre al pintar.


Última revisión: 1 de septiembre de 2026

Claim Your Loot

Explora otras secciones

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *