Detectar acertos

*Este conteúdo é traduzido por IA (Beta) e pode conter erros. Para ver a página em inglês, clique aqui.

Detectar acertos é o processo de identificar quando os disparos colidem com jogadores, reduzindo sua saúde de acordo. Em um nível alto, você pode pensar nesse trabalho como:

  1. Uma verificação fisicamente simulada de se um projétil atingiu o alvo.
  2. Uma verificação instantânea de se a arma foi apontada para o alvo.

O tipo de detecção de acertos que você usa depende dos requisitos de gameplay da sua experiência. Por exemplo, uma verificação fisicamente simulada é apropriada para uma experiência de queimada, onde as bolas precisam sair da mão a uma certa velocidade, cair enquanto se movem pelo ar ou mudar de direção devido a condições climáticas. No entanto, uma verificação instantânea é uma combinação melhor para uma experiência de laser tag, onde os feixes devem ter uma velocidade quase infinita e ignorar fatores ambientais como gravidade e velocidade do vento.

Usando a experiência de laser tag de amostra como referência, esta seção do tutorial ensina sobre os scripts por trás da detecção de acertos no espaço 3D, incluindo orientações sobre:

  • Obter a direção do disparo a partir dos valores atuais da câmera e do tipo de arma do jogador.
  • Lançar raios em um caminho reto da arma enquanto dispara.
  • Validar o disparo para evitar a exploração dos dados da arma.
  • Reduzir a saúde do jogador de acordo com o dano do disparo de cada tipo de arma e quantos raios atingiram o jogador.

Após concluir esta seção, você pode explorar tópicos adicionais de desenvolvimento para aprimorar sua jogabilidade, como áudio, iluminação e efeitos especiais.

Obter direção do disparo

Depois que um jogador dispara sua arma, ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData chama duas funções para iniciar o processo de detecção de acertos: rayDirections() e rayResults().

generateBlastData

local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)

As entradas para rayDirections são diretas: a posição atual e os valores de rotação da câmera, e o tipo de arma do jogador. Se a experiência de laser tag de amostra apenas fornecesse armas que produzem um único feixe laser, ReplicatedStorageLaserRaygetDirectionsForBlast não seria necessário porque você poderia usar currentCamera.CFrame.LookVector para calcular a direção do disparo.

No entanto, como a amostra fornece um tipo de arma adicional que produz vários feixes laser com uma ampla dispersão horizontal, getDirectionsForBlast deve calcular a direção de cada feixe laser da dispersão de acordo com seus ângulos dentro da configuração da arma:

getDirectionsForBlast

if numLasers == 1 then
-- Para lasers únicos, eles miram reto
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Para múltiplos lasers, distribua-os uniformemente na horizontal
-- sobre um intervalo laserSpreadDegrees ao redor do 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 demonstrar ainda mais este conceito, se você quisesse incluir um terceiro tipo de arma com uma ampla dispersão vertical, poderia criar um novo atributo de arma, como spreadDirection, e então ajustar o cálculo CFrame para usar um eixo diferente. Por exemplo, note a diferença nos cálculos de direction no seguinte script abaixo para esse hipotético terceiro tipo de arma.


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

No final, a função rayDirections() retorna uma tabela de Vectors que representam a direção de cada feixe laser. Se for útil, você pode adicionar algum registro para ter uma noção de como esses dados se parecem.

generateBlastData

local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
for _, direction in rayDirections do -- nova linha
print(direction) -- nova linha
end -- nova linha
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)

Lançar raios

castLaserRay(), a segunda função em ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData, realiza as operações mais complexas dentro do script. Começa especificando parâmetros para que possa fazer chamadas Workspace:Raycast() para fins de raycasting. Raycasting é o processo de enviar um raio invisível de um ponto Vector3 em uma direção específica com um comprimento definido e, em seguida, verificar seu caminho para ver onde ele intersecta com outros objetos.

Essa informação é particularmente útil para experiências de tiro em primeira pessoa porque permite ver quando e onde os disparos intersectam com jogadores ou o ambiente. Por exemplo, a seguinte imagem demonstra dois raios que estão lançando paralelamente um ao outro. De acordo com seu ponto de origem e direção, o Raio A não atinge a parede e continua até encontrar sua distância máxima, enquanto o Raio B colide com a parede. Para mais informações sobre esse processo, veja Raycasting.

Um diagrama onde o Raio A passa pela parede, e o Raio B colide com a parede.

Os parâmetros de castLaserRay() especificam que as chamadas Raycast() devem considerar todas as partes na workspace exceto o personagem que disparou. O script então lança um raio para cada direção na tabela directions. Se um raio atingir algo, ele gera um RaycastResult, que possui cinco propriedades:

O valor Instance é o mais crítico dessas propriedades para o gameplay da experiência de laser tag de amostra porque comunica quando os raios colidem com outros jogadores. Para recuperar essas informações, a experiência usa a função auxiliar ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant. Se retornar nil, a instância não faz parte de um jogador, significando que o raio atingiu um objeto inanimado dentro do ambiente.

castLaserRay() então usa Position e Normal para criar um novo CFrame que chama de destination do raio. Cada raio tem um destino, e é onde o raio atingiu no espaço 3D ou o ponto no final de sua distância máxima. Dependendo de quão bem seus jogadores miram, muitos ou a maioria dos valores taggedPlayer são nil.

castLaserRay

if result then
-- O disparo acertou algo, verifique se foi um jogador.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- O disparo não acertou nada, então seu destino é
-- o ponto em sua distância máxima.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
end

Validar o disparo

Para prevenir trapaças, o capítulo anterior Implementando Armas explica como blastClient notifica o servidor do disparo usando um RemoteEvent para que ele possa verificar todos os dados que cada cliente envia, como se realmente marcaram outro jogador com sua arma. Esse processo de validação de raios ocorre em ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults, e cada verificação está correlacionada a um módulo de script aninhado:

  1. Primeiro, getValidatedRayResults chama validateRayResult para verificar se cada entrada na tabela rayResults do cliente é um CFrame e um Player (ou nil).

  2. Em seguida, é chamada isRayAngleFromOriginValid para comparar os ângulos esperados da dispersão do laser com aqueles do cliente. Este código em particular mostra a vantagem de usar ReplicatedStorage porque o servidor pode chamar getDirectionsForBlast ele mesmo, armazenar o retorno como os dados "esperados" e, em seguida, compará-lo com os dados do cliente.

    Assim como a validação da arma do capítulo anterior, isRayAngleFromOriginValid depende de um valor de tolerância para determinar o que constitui uma diferença "excessiva" em â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 abstrai as partes mais complicadas da matemática, então o resultado é uma função auxiliar curta e altamente reutilizável com aplicabilidade em uma variedade de experiências:

    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. A próxima verificação é a mais intuitiva. Enquanto getValidatedBlastData usa DISTANCE_SANITY_CHECK_TOLERANCE_STUDS para verificar se o jogador que disparou estava perto do ponto de origem do feixe, isPlayerNearPosition usa lógica idêntica para verificar se o jogador marcado estava perto do destino do feixe:

    isPlayerNearPosition

    local distanceFromCharacterToPosition = position - character:GetPivot().Position
    if distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS then
    return false
    end
  4. A verificação final isRayPathObstructed usa uma variação da operação de lançamento de raio para verificar se o destino do raio está atrás de uma parede ou outra obstrução a partir da posição do cliente. Por exemplo, se um jogador mal-intencionado estivesse sistematicamente removendo todas as paredes da experiência para marcar outros jogadores, o servidor verificaria e confirmaria que os raios são inválidos porque conhece a posição de cada objeto dentro do ambiente.

    isRayPathObstructed

    local scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)
    scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude

Nenhuma estratégia de anti-exploração é abrangente, mas é importante considerar como jogadores mal-intencionados podem abordar sua experiência para que você possa implementar verificações que o servidor possa executar para sinalizar comportamentos suspeitos.

Reduzir a saúde do jogador

Após verificar que um jogador acertou outro jogador, os passos finais para completar o loop principal de gameplay na experiência de laser tag de amostra são reduzir a saúde do jogador marcado, incrementar a pontuação e reanimar o jogador de volta na rodada.

Começando com a redução da saúde do jogador marcado, Spawn e respawn cobre a distinção entre Player e Player.Character, especificamente que um personagem é um modelo Humanoid. Modelos Humanoid têm uma propriedade Health com um valor padrão de 100. Em vez de implementar seu próprio sistema, a experiência de laser tag de amostra usa essa propriedade embutida para acompanhar quanto dano um jogador precisa antes de ser marcado.

A experiência armazena valores de dano no atributo damagePerHit de cada arma. Por exemplo, a arma que dispara um único feixe laser causa 10 pontos de dano, então são necessários dez disparos com essa arma para marcar outro jogador. Para iniciar o processo de marcar um jogador, LaserBlastHandler chama ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers, que verifica a tabela rayResults já validada em busca de jogadores e passa damagePerHit para onPlayerTagged.

Health não aceita valores negativos, então onPlayerTagged possui alguma lógica para manter a saúde do jogador em zero ou acima. Após verificar que a saúde do jogador está acima de zero, ele compara a saúde com damagePerHit e usa o menor dos dois valores. Por exemplo, se um jogador tem 10 de saúde e é atingido por um feixe laser de 15 de dano, o laser causa apenas 10 pontos de dano.

Essa abordagem do problema pode parecer um pouco convoluta. Por exemplo, por que não apenas definir a saúde do jogador como zero se isso seria negativo? A razão é que definir valores de saúde contorna o campo de força. Usar o método Humanoid:TakeDamage() garante que os jogadores não sofram dano enquanto seus campos de força estiverem ativos.

onPlayerTagged

local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Proibir fogo amigo
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Evitar saúde negativa
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage garante que a saúde não seja reduzida se o campo de força estiver ativo
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Premiar playerBlasted com um ponto por marcar playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end

O próximo passo é incrementar a pontuação. Pode ter parecido desnecessário para LaserBlastHandler incluir o jogador que disparou junto com os dados do disparo, mas sem essas informações, a experiência não pode creditar o jogador por marcar alguém. Por fim, o jogador que foi marcado retorna à rodada, o que você pode revisar em Spawn e Respawn.

Os cinco capítulos deste currículo cobrem o loop principal de gameplay da experiência, mas ainda há muitas áreas a explorar, como:

  • Visuais da arma: Veja ReplicatedStorageFirstPersonBlasterVisuals e ServerScriptServiceThirdPersonBlasterVisuals.
  • Áudio: Veja ReplicatedStorageSoundHandler.
  • Modos personalizados: Como você poderia modificar essa experiência para introduzir novos tipos de objetivos, como marcar mais pontos antes que o tempo acabe?

Para lógica de gameplay estendida para a experiência de laser tag, assim como ativos ambientais reutilizáveis e de alta qualidade, revise o Laser Tag template.

©2026 Roblox Corporation, Roblox, o logotipo Roblox e Powering Imagination estão entre nossas marcas registradas e não registradas nos EUA e em outros países.