Touched en Roblox: por qué se dispara catorce veces

Claim Your Loot

·

Portada del artículo «Detectar que un jugador toca algo», de la sección Scripting en Luau

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.

Las catorce partes de un personaje R15 disparando Touched, con y sin debounce

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 = false en las partes que no deben disparar eventos de contacto (adornos, cristales decorativos).
  • CanQuery = false en 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.

LA PLACA
Salida de la consola 0 líneas
Elige tu apuesta y cruza

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

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 *