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:
- Una verificación simulada físicamente de si un proyectil golpeó el objetivo.
- Una verificación instantánea de si el blaster estaba apuntando al objetivo.
El tipo de detección de impactos que utilices depende de los requisitos de juego de tu juego. Por ejemplo, una verificación simulada físicamente es apropiada para un juego de dodgeball donde las pelotas necesitan salir de la mano a una cierta velocidad, caer a medida que se mueven por el aire o cambiar de dirección debido a las condiciones climáticas. Sin embargo, una verificación instantánea es una mejor opción para un juego 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 el juego 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 blaster del jugador.
- Lanzar rayos en un camino recto desde el blaster mientras dispara.
- Validar el disparo para prevenir la explotación de los datos del blaster.
- Reducir la salud del jugador de acuerdo con el daño del disparo de cada tipo de blaster y cuántos rayos golpearon al jugador.
Después de completar esta sección, puedes explorar temas de desarrollo adicionales para mejorar tu jugabilidad, como audio, iluminación y efectos especiales.
Obtener dirección del disparo
Después de que un jugador dispara su blaster, ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData llama a dos funciones para iniciar el proceso de detección de impactos: rayDirections() y rayResults().
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)Las entradas para rayDirections son sencillas: la posición y los valores de rotación actuales de la cámara, y el tipo de blaster del jugador. Si el juego de láser tag de muestra solo diera a los jugadores blasters 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 blaster adicional que produce varios rayos láser con una amplia dispersión horizontal, getDirectionsForBlast debe calcular la dirección de cada rayo láser de la dispersión de acuerdo con sus ángulos dentro de la configuración del blaster:
if numLasers == 1 then
-- Para rayos láser individuales, apuntan recto
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Para múltiples rayos láser, distribúyelos uniformemente en horizontal
-- sobre un intervalo de laserSpreadDegrees alrededor del centro
local leftAngleBound = laserSpreadDegrees / 2
local rightAngleBound = -leftAngleBound
local degreeInterval = laserSpreadDegrees / (numLasers - 1)
for angle = rightAngleBound, leftAngleBound, degreeInterval do
local direction = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVector
table.insert(directions, direction)
end
endPara demostrar este concepto más a fondo, si quisieras incluir un tercer tipo de blaster con una amplia dispersión vertical, podrías crear un nuevo atributo de blaster, 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 blaster.
if numLasers == 1 then
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
local leftAngleBound = laserSpreadDegrees / 2
local rightAngleBound = -leftAngleBound
local degreeInterval = laserSpreadDegrees / (numLasers - 1)
for angle = rightAngleBound, leftAngleBound, degreeInterval do
local direction
if spreadDirection == "vertical" then
direction = (originCFrame * CFrame.Angles(math.rad(angle), 0, 0)).LookVector
else
direction = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVector
end
table.insert(directions, direction)
end
end
return directionsEn ú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 un poco de registro para tener una idea de cómo se ve esta información.
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
for _, direction in rayDirections do -- nueva línea
print(direction) -- nueva línea
end -- nueva línea
local 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 verificar su trayectoria para ver dónde intersecta con otros objetos.
Esta información es particularmente útil para juegos de disparos en primera persona porque permite ver cuándo y dónde los disparos intersectan con los jugadores o el entorno. Por ejemplo, la siguiente imagen demuestra dos rayos que se lanzan paralelamente entre sí. Según su punto de origen y dirección, el Rayo A falla al golpear la pared y continúa hasta alcanzar su distancia máxima, mientras que el Rayo B colisiona con la pared. Para 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ó. El script luego 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 del juego de láser tag de muestra porque comunica cuándo los rayos colisionan con otros jugadores. Para recuperar esta información, el juego utiliza 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 destino del rayo. Cada rayo tiene un destino, y es donde el rayo golpeó en el espacio 3D, o el punto al final de su distancia máxima. Dependiendo de cuán bien apunten tus jugadores, muchos o la mayoría de los valores taggedPlayer son nil.
if result then
-- El disparo golpeó algo, verifica si fue un jugador.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- El disparo no golpeó nada, por lo que su destino es
-- el punto en su distancia máxima.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
endValidar el disparo
Para prevenir trampas, el capítulo anterior Implementación de Blasters explica cómo blastClient notifica al servidor del disparo usando un RemoteEvent para que pueda verificar todos los datos que cada cliente envía, como si realmente etiquetaron a otro jugador con su blaster. 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).
A continuación, llama a isRayAngleFromOriginValid para comparar los ángulos esperados de la dispersión del 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 el retorno como los datos "esperados" y luego compararlo con los datos del cliente.
Al igual que la validación del blaster del capítulo anterior, isRayAngleFromOriginValid se basa en 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 los bits más complejos de matemáticas, por lo que el resultado es una función auxiliar corta y altamente reutilizable con aplicabilidad en una variedad de juegos:
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 utiliza 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 rayos para verificar 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 eliminara sistemáticamente todas las paredes del juego para etiquetar a otros jugadores, el servidor verificaría y confirmaría que los rayos son inválidos porque conoce la posición de cada 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 abordar tu juego para que puedas implementar verificaciones que el servidor pueda ejecutar para marcar 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 el juego de láser tag de muestra son reducir la salud del jugador etiquetado, incrementar el marcador y reaparecer al jugador en la ronda.
Comenzando con la reducción de la salud del jugador etiquetado, Aparecer 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, el juego 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.
El juego almacena los valores de daño en el atributo damagePerHit de cada blaster. Por ejemplo, el blaster que dispara un solo rayo láser inflige 10 puntos de daño, por lo que se necesitan diez disparos con este blaster para etiquetar a otro jugador. Para iniciar el proceso de etiquetar a un jugador fuera, LaserBlastHandler llama a ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers, que verifica la tabla rayResults ahora validada 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 más. 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.
Esta forma de abordar el problema puede parecer un poco complicada. Por ejemplo, ¿por qué no simplemente establecer la salud del jugador en cero si sería negativa? La razón es que establecer 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.
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Desalentar el 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 se reduzca si el campo de fuerza está activo
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Otorgar un punto a playerBlasted por etiquetar a playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
endEl siguiente paso es incrementar el marcador. Puede haber parecido innecesario que LaserBlastHandler incluyera al jugador que disparó junto con los datos del disparo, pero sin esa información, el juego no puede acreditar al jugador por etiquetar a alguien. Finalmente, el jugador etiquetado reaparece en la ronda, lo que puedes revisar en Aparecer y Reaparecer.
Los cinco capítulos de este currículo cubren el bucle principal de jugabilidad del juego, pero aún hay muchas áreas por explorar, como:
- Visuales del blaster: Consulta ReplicatedStorage ⟩ FirstPersonBlasterVisuals y ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- Audio: Consulta ReplicatedStorage ⟩ SoundHandler.
- Modos personalizados: ¿Cómo podrías modificar este juego para introducir nuevos tipos de objetivos, como anotar la mayor cantidad de puntos antes de que se acabe el tiempo?
Para una lógica de jugabilidad extendida para el juego de láser tag, así como activos ambientales reutilizables y de alta calidad, revisa la plantilla Laser Tag.