Detectar impactos es el proceso de identificar cuándo los disparos colisionan con los jugadores, reduciendo su salud en consecuencia. A un alto nivel, puedes pensar en este trabajo como:
- Un chequeo simulado físicamente para saber si un proyectil golpeó el objetivo.
- Un chequeo instantáneo para saber si el bláster estaba apuntando al objetivo.
El tipo de detección de impactos que uses depende de los requisitos de juego de tu experiencia. Por ejemplo, un chequeo simulado físicamente es apropiado para una experiencia de dodgeball donde las pelotas deben salir de la mano a una cierta velocidad, caer mientras se mueven por el aire o cambiar de dirección debido a las condiciones meteorológicas. Sin embargo, un chequeo instantáneo es una mejor coincidencia para una experiencia de láser tag donde los rayos deben tener una velocidad casi infinita e ignorar factores ambientales como la gravedad y la velocidad del viento.
Usando la experiencia de láser tag de muestra como referencia, esta sección del tutorial te enseña sobre los scripts detrás de la detección de impactos en el espacio 3D, incluyendo orientación sobre:
- Obtener la dirección del disparo a partir de los valores actuales de la cámara y el tipo de bláster del jugador.
- Lanzar rayos en una trayectoria recta desde el bláster mientras dispara.
- Validar el disparo para prevenir la explotación de los datos del bláster.
- Reducir la salud del jugador de acuerdo al daño del disparo de cada tipo de bláster y cuántos rayos impactaron al jugador.
Después de completar esta sección, puedes explorar temas de desarrollo adicionales para mejorar tu experiencia de juego, como audio, iluminación y efectos especiales.
Obtener dirección del disparo
Después de que un jugador dispare su bláster, ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData llama a dos funciones para iniciar el proceso de detección de impactos: rayDirections() y rayResults().
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
Las entradas para rayDirections son sencillas: la posición actual de la cámara y los valores de rotación, y el tipo de bláster del jugador. Si la experiencia de láser tag de muestra solo diera a los jugadores blásters que producen un solo rayo láser, ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast sería innecesario porque podrías usar currentCamera.CFrame.LookVector para calcular la dirección del disparo.
Sin embargo, dado que la muestra proporciona un tipo de bláster adicional que produce varios rayos láser con una amplia dispersión horizontal, getDirectionsForBlast debe calcular la dirección para cada rayo láser de la dispersión según sus ángulos dentro de la configuración del bláster:
getDirectionsForBlastif numLasers == 1 then-- Para láseres individuales, apuntan rectotable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 then-- Para múltiples láseres, distribúyelos uniformemente de manera horizontal-- sobre un intervalo laserSpreadDegrees alrededor del centrolocal leftAngleBound = laserSpreadDegrees / 2local rightAngleBound = -leftAngleBoundlocal degreeInterval = laserSpreadDegrees / (numLasers - 1)for angle = rightAngleBound, leftAngleBound, degreeInterval dolocal direction = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVectortable.insert(directions, direction)endend
Para demostrar aún más este concepto, si quisieras incluir un tercer tipo de bláster con una amplia dispersión vertical, podrías crear un nuevo atributo de bláster, como spreadDirection, y luego ajustar el cálculo de CFrame para usar un eje diferente. Por ejemplo, nota la diferencia en los cálculos de direction en el siguiente script para este hipotético tercer tipo de bláster.
if numLasers == 1 thentable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 thenlocal leftAngleBound = laserSpreadDegrees / 2local rightAngleBound = -leftAngleBoundlocal degreeInterval = laserSpreadDegrees / (numLasers - 1)for angle = rightAngleBound, leftAngleBound, degreeInterval dolocal directionif spreadDirection == "vertical" thendirection = (originCFrame * CFrame.Angles(math.rad(angle), 0, 0)).LookVectorelsedirection = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVectorendtable.insert(directions, direction)endendreturn directions
En última instancia, la función rayDirections() devuelve una tabla de Vectors que representan la dirección de cada rayo láser. Si es útil, puedes agregar algunos registros para tener una idea de cómo se ve esta información.
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)for _, direction in rayDirections do -- nueva líneaprint(direction) -- nueva líneaend -- nueva línealocal rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
Lanzar rayos
castLaserRay(), la segunda función en ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData, realiza las operaciones más complejas dentro del script. Comienza especificando parámetros para que pueda hacer llamadas a Workspace:Raycast() con fines de raycasting. El raycasting es el proceso de enviar un rayo invisible desde un punto Vector3 en una dirección específica con una longitud definida, y luego comprobar su trayectoria para ver dónde intersecta con otros objetos.
Esta información es particularmente útil para experiencias de tiradores en primera persona porque te permite ver cuándo y dónde los disparos intersectan con jugadores o el entorno. Por ejemplo, la siguiente imagen demuestra dos rayos que se lanzan en paralelo. Según su punto de origen y dirección, el Rayo A falla en golpear la pared y continúa hasta que alcanza su distancia máxima, mientras que el Rayo B colisiona con la pared. Para obtener más información sobre este proceso, consulta Raycasting.

Los parámetros de castLaserRay() especifican que las llamadas a Raycast() deben considerar cada parte en el espacio de trabajo excepto el personaje que disparó. Luego, el script lanza un rayo para cada dirección en la tabla directions. Si un rayo golpea algo, genera un RaycastResult, que tiene cinco propiedades:
- Distance – La distancia entre el origen del rayo y el punto de intersección.
- Material – El Enum.Material en el punto de intersección.
El valor Instance es el más crítico de estas propiedades para la jugabilidad de la experiencia de láser tag de muestra porque comunica cuándo los rayos colisionan con otros jugadores. Para recuperar esta información, la experiencia usa la función auxiliar ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant. Si devuelve nil, la instancia no es parte de un jugador, lo que significa que el rayo golpeó un objeto inanimado dentro del entorno.
castLaserRay() luego utiliza Position y Normal para crear un nuevo CFrame que llama destination del rayo. Cada rayo tiene un destino, que es donde el rayo impactó en el espacio 3D, o el punto al final de su distancia máxima. Dependiendo de lo bien que apunten tus jugadores, muchos o la mayoría de los valores taggedPlayer son nil.
castLaserRayif result then-- El disparo impactó algo, verifica si fue un jugador.destination = CFrame.lookAt(result.Position, result.Position + result.Normal)taggedPlayer = getPlayerFromDescendant(result.Instance)else-- El disparo no impactó nada, por lo que su destino es-- el punto en su distancia máxima.local distantPosition = origin + rayDirection * MAX_DISTANCEdestination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)taggedPlayer = nilend
Validar el disparo
Para prevenir trampas, el capítulo anterior Implementación de Blásters explica cómo blastClient notifica al servidor del disparo usando un RemoteEvent, de modo que pueda verificar todos los datos que cada cliente envía, como si realmente etiquetaron a otro jugador con su bláster. Este proceso de validación de rayos ocurre en ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults, y cada verificación se correlaciona con un script de módulo anidado:
Primero, getValidatedRayResults llama a validateRayResult para verificar que cada entrada en la tabla rayResults del cliente sea un CFrame y un Player (o nil).
Luego, llama a isRayAngleFromOriginValid para comparar los ángulos esperados de la dispersión de láser con los del cliente. Este código en particular muestra la ventaja de usar ReplicatedStorage porque el servidor puede llamar a getDirectionsForBlast por sí mismo, almacenar la devolución como los datos "esperados", y luego compararlos con los datos del cliente.
Al igual que la validación del bláster del capítulo anterior, isRayAngleFromOriginValid depende de un valor de tolerancia para determinar qué constituye una diferencia "excesiva" en los ángulos:
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRoblox abstrae la mayor parte de las matemáticas más involucradas, por lo que el resultado es una función auxiliar corta y altamente reutilizable con aplicabilidad en una gama de experiencias:
getAngleBetweenDirectionslocal function getAngleBetweenDirections(directionA: Vector3, directionB: Vector3)local dotProduct = directionA:Dot(directionB)local cosAngle = math.clamp(dotProduct, -1, 1)local angle = math.acos(cosAngle)return math.deg(angle)endLa siguiente verificación es la más intuitiva. Mientras que getValidatedBlastData usa DISTANCE_SANITY_CHECK_TOLERANCE_STUDS para verificar que el jugador que disparó estaba cerca del punto de origen del rayo, isPlayerNearPosition utiliza una lógica idéntica para verificar si el jugador etiquetado estaba cerca del destino del rayo:
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseendLa verificación final isRayPathObstructed utiliza una variación de la operación de lanzamiento de rayo para comprobar si el destino del rayo está detrás de una pared u otra obstrucción desde la posición del cliente. Por ejemplo, si un jugador malicioso fuera a eliminar sistemáticamente todas las paredes de la experiencia para etiquetar a otros jugadores, el servidor verificaría y confirmaría que los rayos son inválidos porque conoce cada posición de objeto dentro del entorno.
isRayPathObstructedlocal scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude
Ninguna estrategia anti-explotación es completa, pero es importante considerar cómo los jugadores maliciosos pueden acercarse a tu experiencia para que puedas implementar verificaciones que el servidor pueda ejecutar para detectar comportamientos sospechosos.
Reducir la salud del jugador
Después de verificar que un jugador etiquetó a otro jugador, los pasos finales para completar el bucle principal de jugabilidad en la experiencia de láser tag de muestra son reducir la salud del jugador etiquetado, incrementar la tabla de clasificación y volver a hacer reaparecer al jugador en la ronda.
Comenzando con la reducción de la salud del jugador etiquetado, Reaparición y reaparecer cubre la distinción entre Player y Player.Character, específicamente que un personaje es un modelo de Humanoid. Los modelos de Humanoid tienen una propiedad Health con un valor predeterminado de 100. En lugar de implementar su propio sistema, la experiencia de láser tag de muestra utiliza esta propiedad incorporada para hacer un seguimiento de cuánto daño necesita un jugador antes de ser etiquetado fuera de la ronda.
La experiencia almacena los valores de daño en el atributo damagePerHit de cada bláster. Por ejemplo, el bláster que dispara un solo rayo láser inflige 10 puntos de daño, por lo que se necesitan diez disparos con este bláster para etiquetar a otro jugador. Para iniciar el proceso de etiquetar a un jugador fuera, LaserBlastHandler llama a ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers, que verifica la ahora validada tabla rayResults en busca de jugadores y pasa damagePerHit a onPlayerTagged.

Health no acepta valores negativos, por lo que onPlayerTagged tiene algo de lógica para mantener la salud del jugador en cero o por encima. Después de verificar que la salud del jugador está por encima de cero, compara la salud con damagePerHit y utiliza el menor de los dos valores. Por ejemplo, si un jugador tiene 10 de salud y es golpeado por un rayo láser de 15 de daño, el láser solo inflige 10 puntos de daño.
Este enfoque para resolver el problema puede parecer un poco complicado. Por ejemplo, ¿por qué no simplemente establecer la salud del jugador a cero si sería negativa? La razón es que establecer los valores de salud elude el campo de fuerza. Usar el método Humanoid:TakeDamage() asegura que los jugadores no reciban daño mientras sus campos de fuerza estén activos.
onPlayerTagged
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Prohibir fuego amigo
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Evitar salud negativa
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage asegura que la salud no baje si el Campo de Fuerza está activo
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Premiar a playerBlasted con un punto por etiquetar a playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end
El siguiente paso es incrementar la tabla de clasificación. Puede haber parecido innecesario que LaserBlastHandler incluyera al jugador que disparó junto con los datos del disparo, pero sin esa información, la experiencia no puede acreditar al jugador con haber etiquetado a alguien. Finalmente, el jugador etiquetado reaparece en la ronda, que puedes revisar en Reaparición y reaparecer.
Los cinco capítulos de este currículo cubren el bucle central de jugabilidad de la experiencia, pero todavía hay muchas áreas para explorar, como:
- Visuales del bláster: Consulta ReplicatedStorage ⟩ FirstPersonBlasterVisuals y ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- Audio: Consulta ReplicatedStorage ⟩ SoundHandler.
- Modos personalizados: ¿Cómo podrías modificar esta experiencia para introducir nuevos tipos de objetivos, como puntuar más puntos antes de que se acabe el tiempo?
Para lógica de jugabilidad extendida para la experiencia de láser tag, así como activos ambientales reutilizables y de alta calidad, consulta la plantilla Laser Tag.