Treffer erkennen ist der Prozess, bei dem festgestellt wird, wann Blasts mit Spielern kollidieren, und deren Gesundheit entsprechend reduziert wird. Auf einer hohen Ebene können Sie diese Arbeit als entweder:
- Eine physikalisch simulierte Überprüfung, ob ein Projektil das Ziel getroffen hat.
- Eine sofortige Überprüfung, ob der Blaster auf das Ziel gerichtet war.
Die Art der Treffererkennung, die Sie verwenden, hängt von den Gameplay-Anforderungen Ihres Spiels ab. Zum Beispiel ist eine physikalisch simulierte Überprüfung für ein Völkerballspiel geeignet, bei dem Bälle mit einer bestimmten Geschwindigkeit aus der Hand fliegen, während sie durch die Luft fallen oder sich aufgrund von Wetterbedingungen in eine andere Richtung bewegen. Eine sofortige Überprüfung ist jedoch besser geeignet für ein Lasertag-Spiel, bei dem Strahlen eine nahezu unendliche Geschwindigkeit haben müssen und Umweltfaktoren wie Schwerkraft und Windgeschwindigkeit ignorieren.
Anhand des Beispiel-Lasertag-Spiels lehrt Sie dieser Abschnitt des Tutorials die Skripte hinter der Treffererkennung im 3D-Raum, einschließlich Anleitungen zu:
- Erhalten der Blast-Richtung aus den aktuellen Kamerawerten und dem Blastertyp des Spielers.
- Abfeuern von Strahlen in gerader Linie vom Blaster, während er feuert.
- Validierung des Blasts, um die Ausnutzung von Blasterdaten zu verhindern.
- Reduzierung der Spieler-Gesundheit gemäß dem Blast-Schaden jedes Blastertyps und wie viele Strahlen den Spieler getroffen haben.
Nachdem Sie diesen Abschnitt abgeschlossen haben, können Sie weitere Entwicklungsthemen erkunden, um Ihr Gameplay zu verbessern, wie z.B. Audio, Beleuchtung und Spezialeffekte.
Blast-Richtung erhalten
Nachdem ein Spieler seinen Blaster abgefeuert hat, ruft ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData zwei Funktionen auf, um den Treffererkennungsprozess zu starten: rayDirections() und rayResults().
local 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 das Beispiel-Lasertag-Spiel den Spielern nur Blaster gegeben hätte, die einen einzelnen Laserstrahl erzeugen, wäre ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast nicht notwendig, da Sie currentCamera.CFrame.LookVector verwenden könnten, um die Richtung für den Blast zu berechnen.
Da das Beispiel jedoch einen zusätzlichen Blastertyp bereitstellt, der mehrere Laserstrahlen mit einer breiten, horizontalen Streuung erzeugt, muss getDirectionsForBlast die Richtung für jeden Laserstrahl der Streuung gemäß ihren Winkeln innerhalb der Blasterkonfiguration berechnen:
if numLasers == 1 then
-- Für einzelne Laser zielen sie geradeaus
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Für mehrere Laser, gleichmäßig horizontal verteilen
-- über ein Intervall laserSpreadDegrees um die Mitte
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
endUm dieses Konzept weiter zu veranschaulichen, wenn Sie einen dritten Blastertyp mit einer breiten, vertikalen Streuung einfügen würden, könnten Sie ein neues Blasterattribut wie spreadDirection erstellen und dann die CFrame-Berechnung 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 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 directionsLetztendlich gibt die Funktion rayDirections() eine Tabelle von Vectors zurück, die die Richtung jedes Laserstrahls darstellen. Wenn es hilfreich ist, können Sie einige Protokollierungen hinzufügen, um ein Gefühl dafür zu bekommen, wie diese Daten aussehen.
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
for _, direction in rayDirections do -- neue Zeile
print(direction) -- neue Zeile
end -- neue Zeile
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)Strahlen abfeuern
castLaserRay(), die zweite Funktion in ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData, führt die komplexeren Operationen innerhalb des Skripts aus. Es beginnt damit, Parameter festzulegen, damit es 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 gesendet wird, und dann wird sein Pfad überprüft, um zu sehen, wo er mit anderen Objekten interagiert.
Diese Informationen sind besonders nützlich für Ego-Shooter-Spiele, da sie es Ihnen ermöglichen zu sehen, wann und wo Blasts mit Spielern oder der Umgebung interagieren. Zum Beispiel zeigt das folgende Bild zwei Strahlen, die parallel zueinander abgefeuert werden. Je nach ihrem Ursprungspunkt und ihrer Richtung verfehlt Strahl A die Wand und fährt fort, bis er seine maximale Entfernung erreicht, während Strahl B mit der Wand kollidiert. Für weitere Informationen zu diesem Prozess siehe Raycasting.

Die Parameter von castLaserRay() geben an, dass Raycast()-Aufrufe jedes Teil im Workspace außer dem Charakter, der geschossen hat, berücksichtigen müssen. Das Skript feuert dann einen Strahl für jede Richtung in der directions-Tabelle ab. Wenn ein Strahl etwas trifft, erzeugt er ein RaycastResult, das fünf Eigenschaften hat:
- Distance – Die Entfernung zwischen dem Strahlursprung und dem Schnittpunkt.
- Material – Das Enum.Material am Schnittpunkt.
Der Wert Instance ist die kritischste dieser Eigenschaften für das Gameplay des Beispiel-Lasertag-Spiels, da er kommuniziert, wann Strahlen mit anderen Spielern kollidieren. Um diese Informationen abzurufen, verwendet das Spiel die ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant-Hilfsfunktion. Wenn sie nil zurückgibt, ist die Instanz kein Teil eines Spielers, was bedeutet, dass der Strahl ein unbelebtes Objekt innerhalb der Umgebung getroffen hat.
castLaserRay() verwendet dann Position und Normal, um ein neues CFrame zu erstellen, das es als Ziel des Strahls bezeichnet. Jeder Strahl hat ein Ziel, und es ist entweder der Punkt, an dem der Strahl im 3D-Raum getroffen hat, oder der Punkt am Ende seiner maximalen Entfernung. Je nachdem, wie gut Ihre Spieler zielen, sind viele oder die meisten taggedPlayer-Werte nil.
if result then
-- Der Blast hat etwas getroffen, überprü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, also ist sein Ziel
-- der Punkt an seiner maximalen Entfernung.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
endDen Blast validieren
Um Betrug zu verhindern, erklärt das vorherige Kapitel Implementierung von Blastern, wie blastClient den Server über den Blast mit einem RemoteEvent benachrichtigt, damit er alle Daten, die jeder Client sendet, überprüfen kann, z.B. ob sie tatsächlich einen anderen Spieler mit ihrem Blaster markiert haben. Dieser Validierungsprozess für Strahlen findet in ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults statt, und jede Überprüfung korreliert mit einem verschachtelten Modul-Skript:
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 zu vergleichen. Dieser Code zeigt besonders den Vorteil der Verwendung von ReplicatedStorage, da der Server getDirectionsForBlast selbst aufrufen, das Ergebnis als "erwartete" Daten speichern und dann mit den Daten des Clients vergleichen kann.
Genau wie die Blastervalidierung aus dem vorherigen Kapitel verlässt sich isRayAngleFromOriginValid auf einen Toleranzwert, um zu bestimmen, was einen "exzessiven" Unterschied in den Winkeln darstellt:
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRoblox abstrahiert die kompliziertesten mathematischen Teile, sodass das Ergebnis eine kurze, hochgradig wiederverwendbare Hilfsfunktion ist, die in einer Vielzahl von Spielen 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 überprüfen, ob der Spieler, der geschossen hat, in der Nähe des Ursprungs des Strahls war, verwendet isPlayerNearPosition dieselbe Logik, um zu überprüfen, ob der markierte Spieler in der Nähe des Ziels des Strahls war:
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 liegt. Wenn ein böswilliger Spieler systematisch alle Wände aus dem Spiel entfernen würde, um andere Spieler zu markieren, würde der Server überprüfen und bestätigen, dass die Strahlen ungültig sind, da 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 sich Ihrem Spiel nähern könnten, damit Sie Überprüfungen implementieren können, die der Server ausführen kann, um verdächtiges Verhalten zu kennzeichnen.
Spieler-Gesundheit reduzieren
Nachdem überprüft wurde, dass ein Spieler einen anderen Spieler markiert hat, sind die letzten Schritte zur Vollständigung der Hauptspielschleife im Beispiel-Lasertag-Spiel, 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. Spawnen und Respawnen behandelt den Unterschied 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 das Beispiel-Lasertag-Spiel diese integrierte Eigenschaft, um zu verfolgen, wie viel Schaden ein Spieler benötigt, bevor er aus der Runde markiert wird.
Das Spiel speichert Schadenswerte im damagePerHit-Attribut jedes Blasters. Zum Beispiel verursacht der Blaster, der einen einzelnen Laserstrahl abfeuert, 10 Schadenspunkte, sodass es zehn Blasts mit diesem Blaster benötigt, um einen anderen Spieler aus dem Spiel zu markieren. Um den Prozess des Markierens eines Spielers zu starten, ruft LaserBlastHandler ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers auf, das die nun validierte rayResults-Tabelle nach Spielern überprüft und damagePerHit an onPlayerTagged übergibt.

Health akzeptiert keine negativen Werte, daher hat onPlayerTagged einige Logik, um die Gesundheit der Spieler bei oder über null zu halten. Nachdem überprüft wurde, dass die Gesundheit des Spielers über null liegt, vergleicht es die Gesundheit mit damagePerHit und verwendet den kleineren der beiden Werte. Wenn ein Spieler beispielsweise 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 des Spielers nicht einfach auf null setzen, wenn sie negativ wäre? Der Grund ist, dass das Setzen von Gesundheitswerten das Kraftfeld umgeht. Die Verwendung der Methode Humanoid:TakeDamage() stellt sicher, dass Spieler keinen Schaden erleiden, während ihre Kraftfelder aktiv sind.
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Freundliches Feuer verbieten
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Negative Gesundheit vermeiden
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage stellt sicher, dass die Gesundheit nicht gesenkt wird, wenn das Kraftfeld aktiv ist
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- SpielerBlasted einen Punkt für das Markieren von SpielerTagged geben
Scoring.incrementScore(playerBlasted, 1)
end
end
endDer 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 Blast-Daten einbezieht, aber ohne diese Informationen kann das Spiel den Spieler nicht für das Markieren eines anderen Spielers gutschreiben. Schließlich respawnt der markierte Spieler zurück in die Runde, was Sie in Spawnen und Respawnen überprüfen können.
Die fünf Kapitel in diesem Lehrplan decken die Hauptspielschleife des Spiels ab, aber es gibt noch viele Bereiche zu erkunden, wie z.B.:
- Blaster-Visuals: Siehe ReplicatedStorage ⟩ FirstPersonBlasterVisuals und ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- Audio: Siehe ReplicatedStorage ⟩ SoundHandler.
- Benutzerdefinierte Modi: Wie könnten Sie dieses Spiel ändern, um neue Arten von Zielen einzuführen, wie z.B. die meisten Punkte zu erzielen, bevor die Zeit abläuft?
Für erweiterte Gameplay-Logik für das Lasertag-Spiel sowie wiederverwendbare, hochwertige Umgebungsassets überprüfen Sie die Lasertag-Vorlage.