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
- El guardado funcionando: DataStore sin perder datos.
- Idealmente, la tienda: cómo montar una tienda.
- Unos 40 minutos.
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.

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
falsey 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 = 0guardado, 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.
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





Deja una respuesta