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 os 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 blaster estava apontada para o alvo.

O tipo de detecção de acertos que você usa depende dos requisitos de jogabilidade do seu jogo. Por exemplo, uma verificação fisicamente simulada é apropriada para um jogo 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 um jogo de laser tag onde os feixes devem ter uma velocidade quase infinita e ignorar fatores ambientais como gravidade e velocidade do vento.

Usando o jogo de laser tag de exemplo 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:

  • Obtendo a direção do disparo a partir dos valores atuais da câmera e do tipo de blaster do jogador.
  • Lançando raios em um caminho reto a partir do blaster enquanto ele dispara.
  • Validando o disparo para evitar a exploração dos dados do blaster.
  • Reduzindo a saúde do jogador de acordo com o dano do disparo de cada tipo de blaster e quantos raios atingiram o jogador.

Depois de completar 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 seu blaster, 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 da câmera e os valores de rotação, e o tipo de blaster do jogador. Se o jogo de laser tag de exemplo apenas desse aos jogadores blasters que produzem um único feixe de laser, ReplicatedStorageLaserRaygetDirectionsForBlast seria desnecessário porque você poderia usar currentCamera.CFrame.LookVector para calcular a direção do disparo.

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

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

No final, a função rayDirections() retorna uma tabela de Vectors que representam a direção de cada feixe de laser. Se for útil, você pode adicionar alguns logs 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 então verificar seu caminho para ver onde ele intersecta com outros objetos.

Essa informação é particularmente útil para jogos de tiro em primeira pessoa porque permite que você veja quando e onde os disparos intersectam com jogadores ou o ambiente. Por exemplo, a imagem a seguir 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 erra a parede e continua até atingir 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 continua através da parede, e o Raio B colide com a parede.

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

O valor Instance é o mais crítico dessas propriedades para a jogabilidade do jogo de laser tag de exemplo porque comunica quando os raios colidem com outros jogadores. Para recuperar essa informação, o jogo 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 atingiu algo, verifique se foi um jogador.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- O disparo não atingiu 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 evitar trapaças, o capítulo anterior Implementando Blasters 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 seu blaster. Esse processo de validação de raios ocorre em ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults, e cada verificação se correlaciona a um script de módulo 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, chama isRayAngleFromOriginValid para comparar os ângulos esperados da dispersão do laser com os 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 então compará-los com os dados do cliente.

    Assim como a validação do blaster 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 envolvidas da matemática, então o resultado é uma função auxiliar curta e altamente reutilizável com aplicabilidade em uma variedade de jogos:

    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 raios 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 malicioso estivesse sistematicamente removendo todas as paredes do jogo 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 anti-exploração é abrangente, mas é importante considerar como jogadores maliciosos podem abordar seu jogo para que você possa implementar verificações que o servidor pode executar para sinalizar comportamentos suspeitos.

Reduzir a saúde do jogador

Depois de verificar que um jogador marcou outro jogador, os passos finais para completar o loop principal de jogabilidade no jogo de laser tag de exemplo são reduzir a saúde do jogador marcado, incrementar a tabela de pontuação e respawnar 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, o jogo de laser tag de exemplo usa essa propriedade embutida para acompanhar quanto dano um jogador precisa antes de ser marcado para fora da rodada.

O jogo armazena valores de dano no atributo damagePerHit de cada blaster. Por exemplo, o blaster que dispara um único feixe de laser inflige 10 pontos de dano, então leva dez disparos com esse blaster para marcar outro jogador. Para iniciar o processo de marcar um jogador para fora, LaserBlastHandler chama ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers, que verifica a tabela rayResults agora validada em busca de jogadores e passa damagePerHit para onPlayerTagged.

Health não aceita valores negativos, então onPlayerTagged tem alguma lógica para manter a saúde do jogador em ou acima de zero. Depois de verificar que a saúde do jogador está acima de zero, 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 de laser de 15 de dano, o laser só inflige 10 pontos de dano.

Essa abordagem para o problema pode parecer um pouco convoluta. Por exemplo, por que não simplesmente definir a saúde do jogador como zero se ela fosse negativa? 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 estão 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 ForceField 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 tabela de pontuação. Pode ter parecido desnecessário para LaserBlastHandler incluir o jogador que disparou junto com os dados do disparo, mas sem essa informação, o jogo não pode creditar o jogador por marcar alguém para fora. Finalmente, o jogador marcado para fora respawna de volta na rodada, o que você pode revisar em Spawn e Respawn.

Os cinco capítulos deste currículo cobrem o loop principal de jogabilidade do jogo, mas ainda há muitas áreas a explorar, como:

  • Visuais do blaster: Veja ReplicatedStorageFirstPersonBlasterVisuals e ServerScriptServiceThirdPersonBlasterVisuals.
  • Áudio: Veja ReplicatedStorageSoundHandler.
  • Modos personalizados: Como você poderia modificar este jogo para introduzir novos tipos de objetivos, como marcar mais pontos antes que o tempo acabe?

Para lógica de jogabilidade estendida para o jogo de laser tag, bem 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.