Treffer erkennen ist der Prozess, bei dem festgestellt wird, wann Blasts mit Spielern kollidieren und ihre Gesundheit entsprechend reduziert wird. Auf hoher Ebene können Sie diese Arbeit als entweder
- Eine physikalisch simulierte Überprüfung, ob ein Projektil das Ziel getroffen hat.
- Eine unmittelbare Überprüfung, ob der Blaster auf das Ziel gerichtet war.
Die Art der Treffererkennung, die Sie verwenden, hängt von den Spielanforderungen Ihrer Erfahrung ab. Beispielsweise ist eine physikalisch simulierte Überprüfung geeignet für ein Dodgeball-Erlebnis, bei dem Bälle mit einer bestimmten Geschwindigkeit aus der Hand fliegen, während sie durch die Luft fallen oder aufgrund von Wetterbedingungen die Richtung ändern müssen. Eine unmittelbare Überprüfung eignet sich jedoch besser für eine Lasertag-Erfahrung, bei der Strahlen nahezu unendliche Geschwindigkeit haben und Umwelteinflüsse wie Schwerkraft und Windgeschwindigkeit ignorieren.
Anhand der Beispiel-Lasertag-Erfahrung lehrt Sie dieser Abschnitt des Tutorials die Skripte hinter der Treffererkennung im 3D-Raum, einschließlich Anweisungen zu:
- Holen der Blastrichtung basierend auf den aktuellen Kamerawerten und dem Blastertyp des Spielers.
- Abfeuern von Strahlen in einem geraden Pfad vom Blaster während des Schusses.
- Validierung des Blasts, um die Ausnutzung von Blasterdaten zu verhindern.
- Reduzierung der Spielerhealth entsprechend dem Blast-Schaden jedes Blasters und wie viele Strahlen den Spieler getroffen haben.
Nachdem Sie diesen Abschnitt abgeschlossen haben, können Sie zusätzliche Entwicklungsthemen erkunden, um Ihr Gameplay zu verbessern, wie Audio, Beleuchtung und Spezialeffekte.
Blast-Richtung holen
Nachdem ein Spieler seinen Blaster abgefeuert hat, ruft ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData zwei Funktionen auf, um den Treffererkennungsprozess zu starten: rayDirections() und rayResults().
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
Die Eingaben für rayDirections sind einfach: die aktuellen Kameraposition und -rotationswerte sowie der Blastertyp des Spielers. Wenn die Beispiel-Lasertag-Erfahrung den Spielern nur Blaster gegeben hätte, die einen einzelnen Laserstrahl erzeugen, wäre ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast nicht notwendig gewesen, da Sie currentCamera.CFrame.LookVector zur Berechnung der Richtung für den Blast verwenden könnten.
Da das Beispiel jedoch einen zusätzlichen Blastertyp bietet, der mehrere Laserstrahlen mit einer breiten, horizontalen Streuung erzeugt, muss getDirectionsForBlast die Richtung für jeden Laserstrahl der Streuung gemäß ihren Winkeln in der Blasterkonfiguration berechnen:
getDirectionsForBlastif numLasers == 1 then-- Für einzelne Laser zielen sie geradeaustable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 then-- Für mehrere Laser streuen sie gleichmäßig horizontal über ein Intervall laserSpreadDegrees um die Mittelocal 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
Um dieses Konzept weiter zu veranschaulichen: Wenn Sie einen dritten Blastertyp mit einer breiten, vertikalen Streuung hinzufügen würden, könnten Sie ein neues Blasterattribut wie spreadDirection erstellen und dann die Berechnung von CFrame anpassen, um eine andere Achse zu verwenden. Beachten Sie beispielsweise den Unterschied in den direction-Berechnungen im folgenden Skript für diesen hypothetischen dritten Blastertyp.
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
Letztendlich gibt die Funktion rayDirections() eine Tabelle von Vectors zurück, die die Richtung jedes Laserstrahls darstellen. Wenn es hilfreich ist, können Sie einige Protokolle hinzufügen, um eine Vorstellung davon zu bekommen, wie diese Daten aussehen.
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)for _, direction in rayDirections do -- neuer Zeileprint(direction) -- neuer Zeileend -- neuer Zeilelocal rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
Strahlen abfeuern
castLaserRay(), die zweite Funktion in ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData, führt die komplexeren Operationen im Skript aus. Sie beginnt damit, Parameter zu spezifizieren, sodass sie Workspace:Raycast()-Aufrufe für Raycasting-Zwecke durchführen kann. Raycasting ist der Prozess, bei dem ein unsichtbarer Strahl von einem Vector3-Punkt in eine bestimmte Richtung mit einer definierten Länge ausgesendet wird und dann überprüft wird, wo er mit anderen Objekten in Berührung kommt.
Diese Informationen sind besonders nützlich für First-Person-Shooter-Erfahrungen, da sie Ihnen ermöglichen, zu sehen, wann und wo Blasts mit Spielern oder der Umgebung zusammenstoßen. Zum Beispiel zeigt das folgende Bild zwei Strahlen, die parallel zueinander abfeuern. Laut ihrem Ursprungsort und ihrer Richtung verfehlt Strahl A die Wand und setzt seinen Weg fort, bis er seine maximale Entfernung erreicht, während Strahl B mit der Wand kollidiert. Weitere Informationen zu diesem Prozess finden Sie im Artikel Raycasting.

Die Parameter von castLaserRay() spezifizieren, dass die Raycast()-Aufrufe alle Teile im Arbeitsbereich außer dem Charakter, der geschossen hat, berücksichtigen müssen. Das Skript führt dann für jede Richtung in der directions-Tabelle einen Strahl aus. Wenn ein Strahl etwas trifft, generiert es ein RaycastResult, das fünf Eigenschaften hat:
- Distance – Der Abstand zwischen dem Strahlursprung und dem Schnittpunkt.
- Material – Das Enum.Material am Schnittpunkt.
Der Wert Instance ist die wichtigste dieser Eigenschaften für das Gameplay der Beispiel-Lasertag-Erfahrung, da er kommuniziert, wann Strahlen mit anderen Spielern kollidieren. Um diese Informationen abzurufen, verwendet die Erfahrung die Hilfefunktion ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant. Wenn sie nil zurückgibt, ist die Instanz kein Teil eines Spielers, was bedeutet, dass der Strahl ein unbelebtes Objekt in der Umgebung getroffen hat.
castLaserRay() verwendet dann Position und Normal, um ein neues CFrame zu erstellen, das als Zielpunkt des Strahls bezeichnet wird. Jeder Strahl hat ein Ziel, und es befindet sich entweder dort, wo der Strahl im 3D-Raum getroffen hat, oder an dem Punkt am Ende seiner maximalen Entfernung. Je nachdem, wie gut Ihre Spieler zielen, sind viele oder die meisten taggedPlayer-Werte nil.
castLaserRayif result then-- Der Blast hat etwas getroffen, prüfen Sie, ob es ein Spieler war.destination = CFrame.lookAt(result.Position, result.Position + result.Normal)taggedPlayer = getPlayerFromDescendant(result.Instance)else-- Der Blast hat nichts getroffen, daher ist sein Zielpunkt-- der Punkt an seiner maximalen Entfernung.local distantPosition = origin + rayDirection * MAX_DISTANCEdestination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)taggedPlayer = nilend
Den Blast validieren
Um Betrug zu verhindern, erklärt das vorherige Kapitel Implementierung von Blastern, wie blastClient den Server über den Blast mithilfe eines RemoteEvent benachrichtigt, damit dieser alle Daten, die jeder Client sendet, überprüfen kann, z. B. ob sie einen anderen Spieler tatsächlich mit ihrem Blaster markiert haben oder nicht. Dieser Validierungsprozess für Strahlen findet in ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults statt, wobei jede Überprüfung mit einem verschachtelten Modulskript korreliert:
Zuerst ruft getValidatedRayResults validateRayResult auf, um zu überprüfen, ob jeder Eintrag in der rayResults-Tabelle vom Client ein CFrame und ein Player (oder nil) ist.
Als Nächstes ruft es isRayAngleFromOriginValid auf, um die erwarteten Winkel der Laserstreuung mit den vom Client gemeldeten zu vergleichen. Dieser Code zeigt besonders den Vorteil der Nutzung von ReplicatedStorage, da der Server getDirectionsForBlast selbst aufrufen, den Rückgabewert als "erwartete" Daten speichern und ihn dann mit den Daten des Clients vergleichen kann.
Genau wie die Blastervalidierung im vorherigen Kapitel verlässt sich isRayAngleFromOriginValid auf einen Toleranzwert, um zu bestimmen, was einen "exzessiven" Unterschied in den Winkeln ausmacht:
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRoblox abstrahiert die komplexesten mathematischen Aspekte, sodass das Ergebnis eine kurze, sehr wiederverwendbare Hilfsfunktion ist, die in einer Vielzahl von Erfahrungen anwendbar ist:
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)endDie nächste Überprüfung ist die intuitivste. Während getValidatedBlastData DISTANCE_SANITY_CHECK_TOLERANCE_STUDS verwendet, um zu bestätigen, dass der Spieler, der geschossen hat, sich in der Nähe des Ursprungs des Strahls befand, verwendet isPlayerNearPosition die gleiche Logik, um zu überprüfen, ob der markierte Spieler sich in der Nähe des Ziels des Strahls befand:
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseendDie letzte Überprüfung isRayPathObstructed verwendet eine Variation des Raycast-Vorgangs, um zu überprüfen, ob das Ziel des Strahls hinter einer Wand oder einer anderen Behinderung von der Position des Clients aus ist. Zum Beispiel, wenn ein böswilliger Spieler systematisch alle Wände aus der Erfahrung entfernt, um andere Spieler zu markieren, würde der Server überprüfen und bestätigen, dass die Strahlen ungültig sind, weil er jede Objektposition innerhalb der Umgebung kennt.
isRayPathObstructedlocal scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude
Keine Anti-Betrugsstrategie ist umfassend, aber es ist wichtig zu überlegen, wie böswillige Spieler Ihre Erfahrung angehen können, damit Sie Überprüfungen implementieren können, die der Server ausführen kann, um verdächtiges Verhalten zu kennzeichnen.
Spielerhealth reduzieren
Nachdem bestätigt wurde, dass ein Spieler einen anderen Spieler markiert hat, besteht der letzte Schritt zur Vervollständigung der Hauptspielschleife in der Beispiel-Lasertag-Erfahrung darin, die Gesundheit des markierten Spielers zu reduzieren, die Rangliste zu erhöhen und den Spieler zurück in die Runde zu respawnen.
Beginnen wir mit der Reduzierung der Gesundheit des markierten Spielers. Spawn und Respawn behandelt die Unterscheidung zwischen Player und Player.Character, insbesondere dass ein Charakter ein Humanoid-Modell ist. Humanoid-Modelle haben eine Health-Eigenschaft mit einem Standardwert von 100. Anstatt ein eigenes System zu implementieren, verwendet die Beispiel-Lasertag-Erfahrung diese integrierte Eigenschaft, um zu verfolgen, wie viel Schaden ein Spieler erleiden muss, bevor er aus der Runde markiert wird.
Die Erfahrung speichert die Schadenswerte im damagePerHit-Attribut jedes Blasters. Zum Beispiel verursacht der Blaster, der einen einzelnen Laserstrahl abfeuert, 10 Schadenspunkte, sodass es zehn Schüsse mit diesem Blaster benötigt, um einen anderen Spieler auszumarkieren. Um den Prozess zu starten, um einen Spieler auszuzeichnen, ruft LaserBlastHandler ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers auf, um die jetzt validierte rayResults-Tabelle nach Spielern zu durchsuchen und damagePerHit an onPlayerTagged zu übergeben.

Health akzeptiert keine negativen Werte, sodass onPlayerTagged über eine Logik verfügt, um die Gesundheit der Spieler bei null oder darüber zu halten. Nach der Überprüfung, dass die Gesundheit des Spielers über null liegt, vergleicht es die Gesundheit mit damagePerHit und verwendet den kleineren der beiden Werte. Wenn ein Spieler z. B. 10 Gesundheit hat und von einem Laserstrahl mit 15 Schadenspunkten getroffen wird, verursacht der Laser nur 10 Schadenspunkte.
Diese Herangehensweise an das Problem mag etwas kompliziert erscheinen. Warum sollte man die Gesundheit nicht einfach auf null setzen, wenn sie negativ wäre? Der Grund ist, dass die Festlegung von Gesundheitswerten die Kraftfeldmechanik umgeht. Durch die Verwendung der Methode Humanoid:TakeDamage() wird sichergestellt, dass Spieler keinen Schaden nehmen, solange ihre Kraftfelder aktiv sind.
onPlayerTagged
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Freundliches Feuer nicht erlauben
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Verhindern negativer Gesundheit
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage stellt sicher, dass die Gesundheit nicht gesenkt wird, wenn das ForceField aktiv ist
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- SpielerBlasted einen Punkt für das Auszeichnen von PlayerTagged geben
Scoring.incrementScore(playerBlasted, 1)
end
end
end
Der nächste Schritt besteht darin, die Rangliste zu erhöhen. Es mag unnötig erscheinen, dass LaserBlastHandler den Spieler, der geschossen hat, zusammen mit den Blastdaten einfügt, aber ohne diese Informationen kann die Erfahrung den Spieler nicht mit dem Markieren von jemandem belohnen. Schließlich respawnt der markierte Spieler zurück in die Runde, was Sie in Spawning und Respawning überprüfen können.
Die fünf Kapitel in diesem Lehrplan decken den Kern-Spieleloop der Erfahrung ab, aber es gibt noch viele Bereiche zu erkunden, wie zum Beispiel:
- Blaster-Visuals: Siehe ReplicatedStorage ⟩ FirstPersonBlasterVisuals und ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- Audio: Siehe ReplicatedStorage ⟩ SoundHandler.
- Benutzerdefinierte Modi: Wie könnten Sie diese Erfahrung ändern, um neue Arten von Zielen einzuführen, z. B. die meisten Punkte zu erzielen, bevor die Zeit abläuft?
Für erweiterte Gameplay-Logik für die Lasertag-Erfahrung sowie wiederverwendbare, hochwertige Umgebungsassets, überprüfen Sie die Laser Tag-Vorlage.