Touched es el primer evento que aprende cualquiera y el que más sustos da. Pones un print dentro, cruzas la plataforma andando y en la consola aparecen catorce líneas. Con una moneda, catorce monedas. Con una trampa, catorce veces de daño y estás muerto.
No está roto: está haciendo justo lo que dice. El problema es lo que uno se imagina que dice.
En la demo puedes cruzar una placa y ver, parte por parte, cuántas veces se dispara al cruzar. Y activar el debounce para verlo bajar a una.
Respuesta rápida
Touched se dispara una vez por cada parte que toca, y un personaje de Roblox tiene quince. Se arregla con una bandera (debounce) y comprobando que quien toca es un jugador, no cualquier cosa que pase por ahí.
Lo que necesitas antes de empezar
- Un Part en el mapa y un Script dentro.
- Unos 20 minutos.
Paso 1: lo que pasa en realidad
local placa = script.Parent
placa.Touched:Connect(function(parte)
print("me ha tocado: " .. parte.Name)
end)
Cruza andando y mira la consola:
me ha tocado: LeftFoot
me ha tocado: RightFoot
me ha tocado: LeftLowerLeg
me ha tocado: RightLowerLeg
me ha tocado: HumanoidRootPart
me ha tocado: LeftUpperLeg
...
Un personaje R15 son quince partes. Si andas despacio, algunas tocan y dejan de tocar varias veces. De ahí las catorce, veinte o treinta líneas.

Paso 2: quedarte solo con el jugador
La parte que te llega no es el jugador: es un trozo suyo. El jugador está dos niveles más arriba:
local Jugadores = game:GetService("Players")
placa.Touched:Connect(function(parte)
local personaje = parte.Parent
local jugador = Jugadores:GetPlayerFromCharacter(personaje)
if not jugador then return end -- era una caja, un NPC o el suelo
print(jugador.Name .. " ha pisado la placa")
end)
GetPlayerFromCharacter devuelve nil si eso no es un jugador, y esa comprobación te ahorra la mitad de los problemas: sin ella, cualquier caja que ruede encima te dispara la trampa.
Si además quieres asegurarte de que está vivo:
local humanoide = personaje:FindFirstChildOfClass("Humanoid")
if not humanoide then return end
if humanoide.Health <= 0 then return end
Paso 3: el debounce
Aun filtrando por jugador, siguen entrando quince partes. La bandera:
local activa = false
placa.Touched:Connect(function(parte)
if activa then return end
local jugador = Jugadores:GetPlayerFromCharacter(parte.Parent)
if not jugador then return end
activa = true
dar(jugador)
task.wait(1) -- durante un segundo, no vuelve a contar
activa = false
end)
Cuidado con dónde pones la bandera. Si la activas antes de comprobar que es un jugador, una caja rodando te bloquea la placa un segundo entero para todo el mundo. Primero filtras, luego bloqueas.
Paso 4: un debounce por jugador
Con la bandera de arriba, si dos jugadores pisan a la vez, uno se queda sin premio. Para eso, una tabla:
local ultimo = {}
local ESPERA = 1
placa.Touched:Connect(function(parte)
local jugador = Jugadores:GetPlayerFromCharacter(parte.Parent)
if not jugador then return end
local ahora = os.clock()
if ultimo[jugador] then
if ahora - ultimo[jugador] < ESPERA then return end
end
ultimo[jugador] = ahora
dar(jugador)
end)
game:GetService("Players").PlayerRemoving:Connect(function(j)
ultimo[j] = nil
end)
Es el mismo patrón que el de los cooldowns, y por el mismo motivo: una marca de tiempo por jugador es más barata y más justa que una bandera global.
Paso 5: cuando el objeto desaparece al tocarlo
Para una moneda, ni siquiera hace falta debounce por tiempo: basta con marcar que ya se cogió y destruirla.
local moneda = script.Parent
local cogida = false
moneda.Touched:Connect(function(parte)
if cogida then return end
local jugador = Jugadores:GetPlayerFromCharacter(parte.Parent)
if not jugador then return end
cogida = true
sumarMoneda(jugador)
moneda:Destroy()
end)
La bandera sigue haciendo falta aunque destruyas la moneda: entre que llamas a Destroy y el objeto desaparece, pueden haber entrado ya tres eventos más. Es el origen del clásico «una moneda me dio tres monedas».
Paso 6: TouchEnded y las zonas
Para saber si alguien sigue dentro de una zona (una zona segura, una que hace daño mientras estás dentro), Touched y TouchEnded son un mal negocio: TouchEnded se dispara también cuando el pie se levanta, así que entra y sale sin parar.
Para eso hay algo mejor:
local piezas = workspace:GetPartBoundsInBox(zona.CFrame, zona.Size)
Lo llamas cada cierto tiempo (dos o tres veces por segundo sobra) y miras quién hay dentro. Es más estable y más barato que llevar la cuenta con eventos.
while true do
task.wait(0.4)
local dentro = {}
for _, p in ipairs(workspace:GetPartBoundsInBox(zona.CFrame, zona.Size)) do
local jug = Jugadores:GetPlayerFromCharacter(p.Parent)
if jug then dentro[jug] = true end
end
for jugador, _ in pairs(dentro) do
aplicarEfecto(jugador)
end
end
Paso 7: que no toque lo que no debe
Dos propiedades que ahorran mucho trabajo:
CanTouch = falseen las partes que no deben disparar eventos de contacto (adornos, cristales decorativos).CanQuery = falseen las que no deben aparecer en búsquedas ni en rayos.
Y si necesitas control fino de qué choca con qué, mira los grupos de colisión (PhysicsService): puedes hacer que los drops del suelo no toquen al jugador pero sí al suelo, por ejemplo.
Adivina la salida
¿Cuántas veces se dispara Touched al cruzar?
Antes de pulsar, di un número. Un personaje R15 cruzando una placa andando, con un print dentro del evento.
Errores habituales
Una moneda da tres monedas
Falta la bandera, o está puesta después de Destroy. Ponla lo primero que se ejecute tras comprobar que es un jugador.
La trampa mata a quien pasa por debajo
Touched no distingue por dónde te tocan. Si solo quiere contar pisar la cara de arriba, comprueba la posición: if parte.Position.Y > placa.Position.Y then.
Salta con cajas, con el suelo o con nada
Falta el filtro GetPlayerFromCharacter. Sin él te llega todo lo que tenga físicas.
En el servidor no se entera de nada
Si el Part lo crea o lo mueve el cliente, el servidor puede no ver el contacto. Los eventos de contacto que importan (daño, premios) se escuchan en el servidor y sobre partes que existen en el servidor.
attempt to index nil with 'Parent'
Te ha llegado una parte que ya se destruyó en el mismo fotograma. Comprueba if not parte.Parent then return end al principio.
Funciona andando pero no corriendo
Si el jugador va muy rápido, puede atravesar una placa fina sin registrar contacto. Haz la placa más gruesa o usa una zona con GetPartBoundsInBox.
Preguntas frecuentes
¿Cuántas veces se dispara exactamente?
Depende del personaje (R6 tiene 6 partes, R15 tiene 15), de la velocidad y de la forma. No hay un número fijo: por eso se programa como si fuera a dispararse muchas veces, siempre.
¿Touched gasta rendimiento?
Cada Part con Touched conectado obliga al motor a llevar la cuenta de contactos. Con cien monedas no pasa nada; con cinco mil partes escuchando, sí, y ahí entra la guía de optimización. Ahí es donde compensa una zona única con GetPartBoundsInBox.
¿Puedo saber con qué parte del cuerpo me tocaron?
Sí, es el argumento que te llega. parte.Name te dice LeftFoot, HumanoidRootPart, lo que sea.
¿Y para detectar que un objeto toca el suelo?
El mismo evento, pero filtra por lo contrario: comprueba que la parte que toca no pertenezca a un personaje.
Última revisión: 1 de septiembre de 2026





Deja una respuesta