Un sistema de botín sin tienda es una colección de números que no sirven para nada. La tienda es lo que convierte «llevo 4.300 monedas» en «me faltan 700 para la espada», y ahí es donde el jugador se engancha.
Montarla tiene una parte fácil (los botones) y una parte que hay que hacer bien a la primera: la compra se decide en el servidor. Si el cliente decide, cualquiera se regala el catálogo entero.
En la demo tienes una tienda funcionando con tres artículos: gasta, quédate sin monedas y prueba a pulsar dos veces seguidas.
Respuesta rápida
El catálogo vive en un ModuleScript que ve el servidor. El cliente solo pide comprar por un RemoteEvent. El servidor comprueba que el artículo existe, que el precio es el suyo, que al jugador le llega, resta y entrega. El cliente nunca manda el precio.
Lo que necesitas antes de empezar
- Monedas ya funcionando: cómo crear un leaderboard de puntuación monta el
IntValueque vamos a gastar. - Entender los RemoteEvents: cómo hablan el cliente y el servidor.
- Unos 40 minutos.
Paso 1: el catálogo en un ModuleScript
En ReplicatedStorage, un ModuleScript llamado Catalogo:
local Catalogo = {
espada_hierro = {
nombre = "Espada de hierro",
precio = 250,
descripcion = "Hace mas dano que la de madera.",
},
pocion_vida = {
nombre = "Pocion de vida",
precio = 75,
descripcion = "Recupera 50 puntos de vida.",
},
capa_rara = {
nombre = "Capa rara",
precio = 1200,
descripcion = "No hace nada. Es bonita.",
},
}
return Catalogo
Está en ReplicatedStorage porque el cliente necesita leerlo para dibujar la tienda: nombres, precios, descripciones. Que el cliente vea los precios no es un problema. El problema sería que los decidiera.
Paso 2: el RemoteEvent
En ReplicatedStorage, un RemoteEvent llamado ComprarObjeto.
Y aquí la regla que se salta media internet:
El cliente manda qué quiere comprar. Nunca cuánto cuesta.
Si el cliente manda el precio, un exploit manda 0 y se lo lleva gratis. Si manda solo el identificador, el servidor busca el precio en su copia del catálogo y no hay nada que falsear.
Paso 3: el servidor decide
Un Script en ServerScriptService:
local Replicado = game:GetService("ReplicatedStorage")
local Catalogo = require(Replicado:WaitForChild("Catalogo"))
local evento = Replicado:WaitForChild("ComprarObjeto")
local ocupado = {} -- para que no compre dos veces a la vez
local function darObjeto(jugador, id, articulo)
-- Aqui entregas de verdad: clonar una Tool, subir una stat, lo que sea
local mochila = jugador:FindFirstChild("Backpack")
local modelo = game.ServerStorage.Objetos:FindFirstChild(id)
if modelo then
if mochila then
modelo:Clone().Parent = mochila
end
end
end
evento.OnServerEvent:Connect(function(jugador, id)
-- 1. Que lo que llega sea lo que esperamos
if type(id) ~= "string" then return end
-- 2. Que el articulo exista
local articulo = Catalogo[id]
if not articulo then return end
-- 3. Que no este ya comprando
if ocupado[jugador] then return end
ocupado[jugador] = true
-- 4. Que le llegue
local stats = jugador:FindFirstChild("leaderstats")
local monedas = stats and stats:FindFirstChild("Monedas")
if monedas then
if monedas.Value >= articulo.precio then
monedas.Value = monedas.Value - articulo.precio
darObjeto(jugador, id, articulo)
evento:FireClient(jugador, id, true, "Comprado")
else
evento:FireClient(jugador, id, false, "No te llegan las monedas")
end
end
ocupado[jugador] = nil
end)
Las cuatro comprobaciones están numeradas a propósito. Ninguna sobra:
- El tipo. Un exploit puede mandar una tabla, un número o
nil. Si no compruebas, elCatalogo[id]puede petar el script. - Que exista. Un identificador inventado tiene que caerse solo, no crear objetos fantasma.
- La bandera
ocupado. Sin ella, pulsar dos veces muy rápido puede entrar dos veces antes de que se reste. Es el clásico duplicado. - El saldo. Y el orden importa: restar antes de entregar. Si entregas primero y falla el resto, has regalado el objeto.
Paso 4: la interfaz
Un LocalScript dentro del ScreenGui de la tienda:
local Replicado = game:GetService("ReplicatedStorage")
local Catalogo = require(Replicado:WaitForChild("Catalogo"))
local evento = Replicado:WaitForChild("ComprarObjeto")
local marco = script.Parent:WaitForChild("Marco")
local plantilla = marco:WaitForChild("Plantilla")
for id, articulo in pairs(Catalogo) do
local fila = plantilla:Clone()
fila.Name = id
fila.Visible = true
fila.Titulo.Text = articulo.nombre
fila.Precio.Text = articulo.precio .. " monedas"
fila.Parent = marco
fila.Boton.Activated:Connect(function()
fila.Boton.Text = "..."
evento:FireServer(id)
end)
end
evento.OnClientEvent:Connect(function(id, exito, mensaje)
local fila = marco:FindFirstChild(id)
if fila then
fila.Boton.Text = mensaje
task.wait(1.5)
fila.Boton.Text = "Comprar"
end
end)
Lo que hace bien este cliente: enseña que está pasando algo ("...") y espera la respuesta del servidor para decir si salió bien. Lo que no hace: restar monedas por su cuenta. Las monedas las baja el servidor y el cambio llega solo, porque un IntValue del jugador se replica.

Paso 5: que no se pueda comprar dos veces lo que es único
Para artículos de un solo uso (una capa, una mascota) hay que llevar la cuenta:
local function yaLoTiene(jugador, id)
local comprados = jugador:FindFirstChild("datos")
if not comprados then return false end
local marca = comprados:FindFirstChild(id)
if marca then return true end
return false
end
Y en el servidor, antes de cobrar:
if articulo.unico then
if yaLoTiene(jugador, id) then
evento:FireClient(jugador, id, false, "Ya lo tienes")
ocupado[jugador] = nil
return
end
end
Esas marcas hay que guardarlas en el DataStore junto con el resto del progreso, o al volver a entrar la capa se paga dos veces: cómo guardar los datos con DataStore.
Paso 6: poner precios que tengan sentido
Un catálogo con precios inventados aburre en diez minutos. La cuenta que hay que hacer es cuántos minutos de juego cuesta cada cosa:
minutos para comprarlo = precio ÷ monedas que gana por minuto
Si tu jugador gana unas 60 monedas por minuto:
| Artículo | Precio | Tiempo real |
|---|---|---|
| Poción | 75 | poco más de un minuto |
| Espada | 250 | unos 4 minutos |
| Capa | 1.200 | unos 20 minutos |
Ese escalón (algo cada minuto, algo cada cinco, algo cada veinte) es lo que hace que siempre haya un objetivo cerca y otro lejos. Si todo cuesta veinte minutos, el jugador se va antes de comprar nada.
Pruébalo aquí
La tienda, con el servidor decidiendo
Gana monedas, compra, quédate sin saldo. Y si quitas la bandera del paso 3, pulsa dos veces muy rápido a ver qué pasa.
Con la bandera quitada, pulsa dos veces seguidas muy rápido en un artículo caro.
Lo que hace el servidor
Errores habituales
El jugador se compra cosas gratis
El cliente está mandando el precio, o está restando él las monedas. El precio lo pone el servidor leyendo su catálogo, siempre.
Se duplica el objeto al pulsar rápido
Falta la bandera ocupado. Es exactamente ese síntoma.
Compra, se le resta, pero no recibe nada
darObjeto falló (el modelo no está donde crees) después de haber restado. Comprueba que la entrega funciona antes de cobrar, o devuelve las monedas si falla.
Las monedas bajan en su pantalla pero vuelven a subir
El cliente restó por su cuenta y luego llegó el valor real del servidor. Quita esa resta del LocalScript.
attempt to index nil with 'Monedas'
El jugador entró y la tienda se usó antes de que existiera leaderstats. En el servidor comprueba con FindFirstChild, como en el ejemplo.
El objeto único se puede comprar otra vez tras reconectar
No estás guardando las compras en el DataStore. La marca tiene que sobrevivir a la sesión.
Preguntas frecuentes
¿Puedo vender por Robux en la misma tienda?
Sí, pero es otro mecanismo: eso pasa por MarketplaceService y por la comisión de la plataforma. Lo tienes en cómo crear un gamepass y venderlo en tu juego.
¿Dónde pongo el catálogo si no quiero que el cliente lo vea entero?
En ServerStorage, y mandas al cliente solo lo que toque enseñar. Se hace cuando tienes artículos secretos o que se desbloquean; para una tienda normal no compensa la complicación.
¿Cómo hago rebajas o precios por tiempo limitado?
Un campo más en el catálogo (descuento) y que el servidor calcule el precio final. Nunca dejes que el cliente mande el precio con descuento aplicado.
¿Puedo dejar que vendan objetos de vuelta?
Sí, y devuelve menos de lo que costó (la mitad es lo habitual). Si devuelves el precio completo, comprar y vender en bucle no cuesta nada y cualquier bug de entrega se convierte en una máquina de monedas.
Última revisión: 1 de septiembre de 2026





Deja una respuesta