Rilevare colpi è il processo di identificazione quando i proiettili colpiscono i giocatori, riducendo quindi la loro salute di conseguenza. In termini generali, puoi pensare a questo lavoro come a:
- Un controllo simulato fisicamente per determinare se un proiettile ha colpito il bersaglio.
- Un controllo istantaneo per vedere se il blaster era puntato verso il bersaglio.
Il tipo di rilevamento colpi che utilizzi dipende dai requisiti di gioco della tua esperienza. Ad esempio, un controllo simulato fisicamente è appropriato per un'esperienza di dodgeball in cui le palline devono lasciare la mano a una certa velocità, cadere mentre si muovono nell'aria o cambiare direzione a causa delle condizioni meteorologiche. Tuttavia, un controllo istantaneo è una scelta migliore per un'esperienza 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 l'esperienza di laser tag campione 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 della fotocamera attuale e dal tipo di blaster del giocatore.
- Lanciare raggi in un percorso rettilineo dal blaster mentre spara.
- Validare il colpo per prevenire sfruttamenti dei dati del blaster.
- Ridurre la salute del giocatore in base al danno da colpo di ciascun tipo di blaster e quanti raggi hanno colpito il giocatore.
Dopo aver completato questa sezione, puoi esplorare ulteriori argomenti di sviluppo per migliorare il tuo gameplay, come audio, illuminazione ed effetti speciali.
Ottenere la direzione del colpo
Dopo che un giocatore spara con il proprio blaster, ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData chiama due funzioni per avviare il processo di rilevamento colpi: rayDirections() e rayResults().
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
Gli input per rayDirections sono semplici: i valori di posizione e rotazione della fotocamera attuale e il tipo di blaster del giocatore. Se l'esperienza di laser tag campione fornisse solo blaster che producono un singolo raggio laser, ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast sarebbe superfluo perché potresti utilizzare 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 di ciascun raggio laser della diffusione in base ai loro angoli all'interno della configurazione del blaster:
getDirectionsForBlastif numLasers == 1 then-- Per laser singoli, sono puntati drittitable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 then-- Per più laser, distribuiscili uniformemente orizzontalmente-- su un intervallo laserSpreadDegrees attorno al centrolocal leftAngleBound = laserSpreadDegrees / 2local rightAngleBound = -leftAngleBoundlocal degreeInterval = laserSpreadDegrees / (numLasers - 1)for angle = rightAngleBound, leftAngleBound, degreeInterval dolocal direction = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVectortable.insert(directions, direction)endend
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 CFrame per utilizzare un asse diverso. Ad esempio, nota la differenza nei calcoli direction nello script seguente per questo ipotetico terzo tipo di blaster.
if numLasers == 1 thentable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 thenlocal leftAngleBound = laserSpreadDegrees / 2local rightAngleBound = -leftAngleBoundlocal degreeInterval = laserSpreadDegrees / (numLasers - 1)for angle = rightAngleBound, leftAngleBound, degreeInterval dolocal directionif spreadDirection == "vertical" thendirection = (originCFrame * CFrame.Angles(math.rad(angle), 0, 0)).LookVectorelsedirection = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVectorendtable.insert(directions, direction)endendreturn directions
Infine, la funzione rayDirections() restituisce una tabella di Vectors che rappresentano la direzione di ciascun raggio laser. Se è utile, puoi aggiungere un po' di registrazione per avere un'idea di come appare questo dato.
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)for _, direction in rayDirections do -- nuova rigaprint(direction) -- nuova rigaend -- nuova rigalocal rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
Lanciare raggi
castLaserRay(), la seconda funzione in ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData, 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 verificando il suo percorso per vedere dove interseca altri oggetti.
Queste informazioni sono particolarmente utili per le esperienze di sparatutto in prima persona poiché consentono di vedere quando e dove i colpi intersecano i giocatori o l'ambiente. Ad esempio, l'immagine seguente dimostra due raggi che stanno lanciando in parallelo. 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.

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 ciascuna direzione nella tabella directions. Se un raggio colpisce qualcosa, genera un RaycastResult, che ha cinque proprietà:
- Distance – La distanza tra l'origine del raggio e il punto di intersezione.
- Material – Il Enum.Material al punto di intersezione.
Il valore di Instance è il più critico di queste proprietà per il gameplay dell'esperienza di laser tag campione perché comunica quando i raggi collidono con altri giocatori. Per recuperare queste informazioni, l'esperienza utilizza la funzione ausiliaria ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant. 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() quindi utilizza Position e Normal per creare un nuovo CFrame che chiama destination del raggio. Ogni raggio ha una destinazione, ed è 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.
castLaserRayif 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_DISTANCEdestination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)taggedPlayer = nilend
Validare il colpo
Per prevenire imbrogli, 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 abbia effettivamente colpito un altro giocatore con il proprio blaster. Questo processo di validazione dei raggi avviene in ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults, e ogni controllo si correla a uno script di modulo annidato:
Per prima cosa, getValidatedRayResults chiama validateRayResult per controllare che ogni voce nella tabella rayResults dal client sia un CFrame e un Player (o nil).
Successivamente, chiama isRayAngleFromOriginValid per confrontare gli angoli attesi della diffusione laser con quelli del client. Questo codice in particolare mostra il vantaggio di utilizzare ReplicatedStorage perché il server può chiamare getDirectionsForBlast direttamente, memorizzare il ritorno come dati "attesi" e quindi confrontarlo con i dati dal 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:
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRoblox astrae via i pezzi più coinvolti della matematica, quindi il risultato è una breve funzione ausiliaria altamente riutilizzabile con applicabilità attraverso un'ampia gamma di esperienze:
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)endIl 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 contrassegnato era vicino alla destinazione del raggio:
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseendL'ultimo controllo isRayPathObstructed utilizza una variazione dell'operazione di ray cast per controllare se la destinazione del raggio è dietro un muro o altra ostruzione dalla posizione del client. Ad esempio, se un giocatore malintenzionato dovesse rimuovere sistematicamente tutti i muri dell'esperienza per contrassegnare altri giocatori, il server verificherebbe e confermerebbe che i raggi sono invalidi perché conosce ogni posizione degli oggetti all'interno dell'ambiente.
isRayPathObstructedlocal 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 possano avvicinarsi alla tua esperienza in modo da poter implementare controlli che il server possa eseguire per segnalare comportamenti sospetti.
Ridurre la salute del giocatore
Dopo aver verificato che un giocatore ha colpito un altro giocatore, i passaggi finali per completare il flusso di gioco principale nell'esperienza di laser tag campione sono ridurre la salute del giocatore contrassegnato, incrementare la classifica e riportare il giocatore nel round.
Iniziando con la riduzione della salute del giocatore contrassegnato, 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. Anziché implementare un proprio sistema, l'esperienza campione di laser tag utilizza questa proprietà integrata per tenere traccia di quanto danno un giocatore ha bisogno prima di essere contrassegnato fuori dal round.
L'esperienza 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 contrassegnare un altro giocatore. Per avviare il processo di contrassegnare un giocatore, LaserBlastHandler chiama ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers, che controlla la tabella rayResults ora validata 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 più. Dopo aver verificato che la salute del giocatore sia sopra zero, confronta la salute con damagePerHit e usa il valore più piccolo dei due. Ad esempio, se un giocatore ha 10 di salute ed è colpito da un laser beam 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? La ragione è che impostare i valori di salute elude il campo di forza. Utilizzando il metodo Humanoid:TakeDamage() si assicura 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 la salute negativa
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage assicura che la salute non venga ridotta se il campo di forza è attivo
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Assegnare a playerBlasted un punto per aver contrassegnato 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, l'esperienza non può accreditare il giocatore per aver contrassegnato qualcuno. Infine, il giocatore contrassegnato respawna nel round, che puoi rivedere in Spawn e Respawn.
I cinque capitoli di questo curriculum coprono il flusso di gioco principale dell'esperienza, ma ci sono ancora molte aree da esplorare, come:
- Visivi del blaster: Vedi ReplicatedStorage ⟩ FirstPersonBlasterVisuals e ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- Audio: Vedi ReplicatedStorage ⟩ SoundHandler.
- Modalità personalizzate: Come potresti modificare questa esperienza 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 l'esperienza di laser tag, così come risorse ambientali riutilizzabili e di alta qualità, rivedi il Laser Tag template.