Detectar impactos

*Este contenido se traduce usando la IA (Beta) y puede contener errores. Para ver esta página en inglés, haz clic en aquí.

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:

  1. Un chequeo simulado físicamente para saber si un proyectil golpeó el objetivo.
  2. 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, ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData llama a dos funciones para iniciar el proceso de detección de impactos: rayDirections() y rayResults().

generateBlastData

local 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, ReplicatedStorageLaserRaygetDirectionsForBlast 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:

getDirectionsForBlast

if numLasers == 1 then
-- Para láseres individuales, apuntan recto
table.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 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
end

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 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 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.

generateBlastData

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 ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData, 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.

Un diagrama donde el Rayo A continúa a través de la pared y el Rayo B colisiona con la pared.

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:

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 ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant. 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.

castLaserRay

if 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_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
end

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 ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults, y cada verificación se correlaciona con un script de módulo anidado:

  1. Primero, getValidatedRayResults llama a validateRayResult para verificar que cada entrada en la tabla rayResults del cliente sea un CFrame y un Player (o nil).

  2. 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:

    isRayAngleFromOriginValid

    local claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unit
    local directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)
    return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREES

    Roblox 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:

    getAngleBetweenDirections

    local 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)
    end
  3. La 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:

    isPlayerNearPosition

    local distanceFromCharacterToPosition = position - character:GetPivot().Position
    if distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS then
    return false
    end
  4. La 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.

    isRayPathObstructed

    local 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 ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers, 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 ReplicatedStorageFirstPersonBlasterVisuals y ServerScriptServiceThirdPersonBlasterVisuals.
  • Audio: Consulta ReplicatedStorageSoundHandler.
  • 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.

©2026 Roblox Corporation. Roblox, el logotipo de Roblox y "Powering Imagination" son algunas de nuestras marcas registradas y no registradas en los Estados Unidos y otros países.