Rilevare colpi

*Questo contenuto è tradotto usando AI (Beta) e potrebbe contenere errori. Per visualizzare questa pagina in inglese, clicca qui.

Rilevare colpi è il processo di identificazione di quando i colpi colpiscono i giocatori, riducendo di conseguenza la loro salute. A un livello alto, puoi pensare a questo lavoro come a:

  1. Un controllo simulato fisicamente per verificare se un proiettile ha colpito il bersaglio.
  2. Un controllo istantaneo per verificare se il blaster era puntato verso il bersaglio.

Il tipo di rilevamento dei colpi che utilizzi dipende dai requisiti di gioco del tuo gioco. Ad esempio, un controllo simulato fisicamente è appropriato per un gioco di dodgeball in cui le palle devono lasciare la mano a una certa velocità, cadere mentre si muovono nell'aria o cambiare direzione a causa delle condizioni atmosferiche. Tuttavia, un controllo istantaneo è più adatto per un gioco di laser tag in cui i raggi devono avere una velocità quasi infinita e ignorare fattori ambientali come la gravità e la velocità del vento.

Utilizzando il gioco di laser tag di esempio come riferimento, questa sezione del tutorial ti insegna gli script dietro il rilevamento dei colpi nello spazio 3D, inclusa la guida su:

  • Ottenere la direzione del colpo dai valori attuali della telecamera e dal tipo di blaster del giocatore.
  • Lanciare raggi in un percorso rettilineo dal blaster mentre spara.
  • Validare il colpo per prevenire lo sfruttamento dei dati del blaster.
  • Ridurre la salute del giocatore in base al danno del colpo di ciascun tipo di blaster e a quanti raggi colpiscono il giocatore.

Dopo aver completato questa sezione, puoi esplorare ulteriori argomenti di sviluppo per migliorare il tuo gameplay, come audio, illuminazione ed effetti speciali.

Ottieni la direzione del colpo

Dopo che un giocatore spara con il proprio blaster, ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData chiama due funzioni per avviare il processo di rilevamento dei colpi: rayDirections() e rayResults().

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

Gli input per rayDirections sono semplici: la posizione attuale della telecamera e i valori di rotazione, e il tipo di blaster del giocatore. Se il gioco di laser tag di esempio fornisse solo blaster che producono un singolo raggio laser, ReplicatedStorageLaserRaygetDirectionsForBlast sarebbe superfluo perché potresti usare currentCamera.CFrame.LookVector per calcolare la direzione del colpo.

Tuttavia, poiché il campione fornisce un tipo di blaster aggiuntivo che produce diversi raggi laser con una diffusione orizzontale ampia, getDirectionsForBlast deve calcolare la direzione per ciascun raggio laser della diffusione in base ai loro angoli all'interno della configurazione del blaster:

getDirectionsForBlast
if numLasers == 1 then
-- Per i laser singoli, puntano dritto
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Per più laser, distribuiscili uniformemente orizzontalmente
-- su un intervallo laserSpreadDegrees attorno al 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

Per dimostrare ulteriormente questo concetto, se volessi includere un terzo tipo di blaster con una diffusione verticale ampia, potresti creare un nuovo attributo del blaster, come spreadDirection, quindi regolare il calcolo di CFrame per utilizzare un asse diverso. Ad esempio, nota la differenza nei calcoli di direction nel seguente script per questo ipotetico terzo tipo di 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

In definitiva, la funzione rayDirections() restituisce una tabella di Vectors che rappresentano la direzione di ciascun raggio laser. Se è utile, puoi aggiungere un po' di logging per avere un'idea di come appare questo dato.

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

Lancia raggi

castLaserRay(), la seconda funzione in ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData, esegue le operazioni più complesse all'interno dello script. Inizia specificando i parametri in modo da poter effettuare chiamate Workspace:Raycast() per scopi di raycasting. Il raycasting è il processo di invio di un raggio invisibile da un punto Vector3 in una direzione specifica con una lunghezza definita, quindi controllando il suo percorso per vedere dove interseca altri oggetti.

Queste informazioni sono particolarmente utili per i giochi di tiro in prima persona perché consentono di vedere quando e dove i colpi intersecano i giocatori o l'ambiente. Ad esempio, l'immagine seguente dimostra due raggi che vengono lanciati paralleli tra loro. Secondo il loro punto di origine e direzione, il Raggio A manca il muro e continua fino a raggiungere la sua distanza massima, mentre il Raggio B collide con il muro. Per ulteriori informazioni su questo processo, vedere Raycasting.

Un diagramma in cui il Raggio A continua attraverso il muro e il Raggio B collide con il muro.

I parametri di castLaserRay() specificano che le chiamate Raycast() devono considerare ogni parte nello spazio di lavoro eccetto il personaggio che ha sparato. Lo script quindi lancia un raggio per ogni direzione nella tabella directions. Se un raggio colpisce qualcosa, genera un RaycastResult, che ha cinque proprietà:

Il valore Instance è il più critico di queste proprietà per il gameplay del gioco di laser tag di esempio perché comunica quando i raggi collidono con altri giocatori. Per recuperare queste informazioni, il gioco utilizza la funzione di aiuto ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant. Se restituisce nil, l'istanza non fa parte di un giocatore, il che significa che il raggio ha colpito un oggetto inanimato all'interno dell'ambiente.

castLaserRay() utilizza quindi Position e Normal per creare un nuovo CFrame che chiama la destinazione del raggio. Ogni raggio ha una destinazione, che è o dove il raggio ha colpito nello spazio 3D, o il punto alla fine della sua distanza massima. A seconda di quanto bene i tuoi giocatori mirano, molti o la maggior parte dei valori taggedPlayer sono nil.

castLaserRay
if result then
-- Il colpo ha colpito qualcosa, controlla se era un giocatore.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- Il colpo non ha colpito nulla, quindi la sua destinazione è
-- il punto alla sua distanza massima.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
end

Valida il colpo

Per prevenire le frodi, il capitolo precedente Implementazione dei Blaster spiega come blastClient notifichi il server del colpo utilizzando un RemoteEvent in modo che possa verificare tutti i dati che ogni client invia, come se hanno effettivamente colpito un altro giocatore con il loro blaster. Questo processo di validazione del raggio avviene in ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults, e ogni controllo corrisponde a uno script di modulo annidato:

  1. Per prima cosa, getValidatedRayResults chiama validateRayResult per controllare che ogni voce nella tabella rayResults dal client sia un CFrame e un Player (o nil).

  2. Successivamente, chiama isRayAngleFromOriginValid per confrontare gli angoli attesi della diffusione del laser con quelli del client. Questo codice in particolare mostra il vantaggio di utilizzare ReplicatedStorage perché il server può chiamare getDirectionsForBlast stesso, memorizzare il ritorno come dati "attesi" e quindi confrontarlo con i dati del client.

    Proprio come la validazione del blaster del capitolo precedente, isRayAngleFromOriginValid si basa su un valore di tolleranza per determinare cosa costituisce una differenza "eccessiva" negli angoli:

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

    Roblox astrae via i pezzi di matematica più coinvolti, quindi il risultato è una breve funzione di aiuto altamente riutilizzabile con applicabilità in una gamma di giochi:

    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. Il controllo successivo è il più intuitivo. Mentre getValidatedBlastData utilizza DISTANCE_SANITY_CHECK_TOLERANCE_STUDS per verificare che il giocatore che ha sparato fosse vicino al punto di origine del raggio, isPlayerNearPosition utilizza una logica identica per controllare se il giocatore colpito era vicino alla destinazione del raggio:

    isPlayerNearPosition
    local distanceFromCharacterToPosition = position - character:GetPivot().Position
    if distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS then
    return false
    end
  4. L'ultimo controllo isRayPathObstructed utilizza una variazione dell'operazione di ray cast per controllare se la destinazione del raggio è dietro un muro o un'altra ostruzione dalla posizione del client. Ad esempio, se un giocatore malintenzionato dovesse rimuovere sistematicamente tutti i muri dal gioco per colpire altri giocatori, il server controllerebbe e confermerebbe che i raggi sono invalidi perché conosce ogni posizione dell'oggetto all'interno dell'ambiente.

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

Nessuna strategia anti-sfruttamento è completa, ma è importante considerare come i giocatori malintenzionati potrebbero avvicinarsi al tuo gioco in modo da poter mettere in atto controlli che il server può eseguire per segnalare comportamenti sospetti.

Riduci la salute del giocatore

Dopo aver verificato che un giocatore ha colpito un altro giocatore, i passaggi finali per completare il ciclo di gioco principale nel gioco di laser tag di esempio sono ridurre la salute del giocatore colpito, incrementare la classifica e far rinascere il giocatore nel round.

Iniziando con la riduzione della salute del giocatore colpito, Spawn e respawn copre la distinzione tra Player e Player.Character, specificamente che un personaggio è un modello Humanoid. I modelli Humanoid hanno una proprietà Health con un valore predefinito di 100. Piuttosto che implementare un proprio sistema, il gioco di laser tag di esempio utilizza questa proprietà integrata per tenere traccia di quanto danno un giocatore deve subire prima di essere eliminato dal round.

Il gioco memorizza i valori di danno nell'attributo damagePerHit di ciascun blaster. Ad esempio, il blaster che spara un singolo raggio laser infligge 10 punti di danno, quindi ci vogliono dieci colpi con questo blaster per eliminare un altro giocatore. Per avviare il processo di eliminazione di un giocatore, LaserBlastHandler chiama ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers, che controlla la tabella rayResults ora convalidata per i giocatori e passa damagePerHit a onPlayerTagged.

Health non accetta valori negativi, quindi onPlayerTagged ha una logica per mantenere la salute del giocatore a zero o superiore. Dopo aver verificato che la salute del giocatore sia superiore a zero, confronta la salute con damagePerHit e utilizza il valore minore dei due. Ad esempio, se un giocatore ha 10 di salute ed è colpito da un raggio laser da 15 danni, il laser infligge solo 10 punti di danno.

Questo modo di affrontare il problema potrebbe sembrare un po' contorto. Ad esempio, perché non impostare semplicemente la salute del giocatore a zero se sarebbe negativa? Il motivo è che impostare i valori di salute elude il campo di forza. Utilizzare il metodo Humanoid:TakeDamage() garantisce che i giocatori non subiscano danni mentre i loro campi di forza sono attivi.

onPlayerTagged
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Vietare il fuoco amico
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Evitare salute negativa
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage garantisce che la salute non venga abbassata se il ForceField è attivo
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Assegna un punto a playerBlasted per aver colpito playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end

Il passo successivo è incrementare la classifica. Potrebbe sembrare superfluo per LaserBlastHandler includere il giocatore che ha sparato insieme ai dati del colpo, ma senza queste informazioni, il gioco non può accreditare il giocatore per aver eliminato qualcuno. Infine, il giocatore eliminato rinasce nel round, che puoi rivedere in Spawn e Respawn.

I cinque capitoli di questo curriculum coprono il ciclo di gioco principale del gioco, ma ci sono ancora molte aree da esplorare, come:

  • Visuali del blaster: Vedi ReplicatedStorageFirstPersonBlasterVisuals e ServerScriptServiceThirdPersonBlasterVisuals.
  • Audio: Vedi ReplicatedStorageSoundHandler.
  • Modalità personalizzate: Come potresti modificare questo gioco per introdurre nuovi tipi di obiettivi, come segnare il maggior numero di punti prima che il tempo scada?

Per una logica di gioco estesa per il gioco di laser tag, così come risorse ambientali riutilizzabili e di alta qualità, consulta il template Laser Tag.

© 2026 Roblox Corporation. Roblox, il logo Roblox e Powering Imagination sono tra i nostri marchi registrati e non registrati negli Stati Uniti. e altri paesi.