Wykrywanie trafień to proces identyfikacji, kiedy strzały kolidują z graczami, a następnie odpowiedniego zmniejszenia ich zdrowia. Na wysokim poziomie można to rozumieć jako:
- Fizycznie symulowane sprawdzenie, czy pocisk trafił w cel.
- Natychmiastowe sprawdzenie, czy blaster był wymierzony w cel.
Rodzaj wykrywania trafień, którego używasz, zależy od wymagań rozgrywki w twojej grze. Na przykład, fizycznie symulowane sprawdzenie jest odpowiednie dla gry w zbijaka, gdzie piłki muszą opuszczać rękę z określoną prędkością, opadać w powietrzu lub zmieniać kierunek w zależności od warunków atmosferycznych. Jednak natychmiastowe sprawdzenie lepiej pasuje do gry w laserowy tag, gdzie promienie muszą mieć niemal nieskończoną prędkość i ignorować czynniki środowiskowe, takie jak grawitacja i prędkość wiatru.
Używając przykładowej gry w laserowy tag jako odniesienia, ta sekcja samouczka nauczy cię o skryptach odpowiedzialnych za wykrywanie trafień w przestrzeni 3D, w tym wskazówki dotyczące:
- Uzyskiwania kierunku strzału z aktualnych wartości kamery i typu blastera gracza.
- Rzucania promieni w prostą linię z blastera podczas strzału.
- Walidacji strzału, aby zapobiec wykorzystywaniu danych blastera.
- Zmniejszania zdrowia gracza zgodnie z obrażeniami od strzału z każdego typu blastera oraz tym, ile promieni trafiło w gracza.
Po ukończeniu tej sekcji możesz zbadać dodatkowe tematy rozwoju, aby wzbogacić swoją rozgrywkę, takie jak dźwięk, oświetlenie i efekty specjalne.
Uzyskaj kierunek strzału
Po tym, jak gracz strzela ze swojego blastera, ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData wywołuje dwie funkcje, aby rozpocząć proces wykrywania trafień: rayDirections() i rayResults().
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)Wejścia dla rayDirections są proste: aktualna pozycja kamery i wartości rotacji oraz typ blastera gracza. Gdyby przykładowa gra w laserowy tag dawała graczom tylko blastery, które produkują pojedynczy promień laserowy, ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast byłoby niepotrzebne, ponieważ można by użyć currentCamera.CFrame.LookVector, aby obliczyć kierunek strzału.
Jednak ponieważ przykład dostarcza dodatkowy typ blastera, który produkuje kilka promieni laserowych z szerokim, poziomym rozkładem, getDirectionsForBlast musi obliczyć kierunek dla każdego promienia laserowego w rozkładzie zgodnie z ich kątami w konfiguracji blastera:
if numLasers == 1 then
-- Dla pojedynczych laserów, celują prosto
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Dla wielu laserów, rozkładają je równomiernie poziomo
-- w zakresie laserSpreadDegrees wokół środka
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
endAby lepiej zobrazować ten koncept, jeśli chciałbyś dodać trzeci typ blastera z szerokim, pionowym rozkładem, mógłbyś stworzyć nowy atrybut blastera, taki jak spreadDirection, a następnie dostosować obliczenia CFrame, aby użyć innej osi. Na przykład, zwróć uwagę na różnicę w obliczeniach direction w poniższym skrypcie dla tego hipotetycznego trzeciego typu blastera.
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 directionsOstatecznie funkcja rayDirections() zwraca tabelę Vectors, która reprezentuje kierunek każdego promienia laserowego. Jeśli to pomocne, możesz dodać trochę logowania, aby zobaczyć, jak wyglądają te dane.
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
for _, direction in rayDirections do -- nowa linia
print(direction) -- nowa linia
end -- nowa linia
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)Rzucanie promieni
castLaserRay(), druga funkcja w ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData, wykonuje bardziej złożone operacje w skrypcie. Zaczyna od określenia parametrów, aby mogła wykonać wywołania Workspace:Raycast() w celach rysowania promieni. Rysowanie promieni to proces wysyłania niewidzialnego promienia z punktu Vector3 w określonym kierunku z zdefiniowaną długością, a następnie sprawdzania jego ścieżki, aby zobaczyć, gdzie przecina inne obiekty.
Informacje te są szczególnie przydatne w grach typu first-person shooter, ponieważ pozwalają zobaczyć, kiedy i gdzie strzały przecinają się z graczami lub otoczeniem. Na przykład, poniższy obrazek ilustruje dwa promienie, które są rysowane równolegle do siebie. Zgodnie z ich punktem początkowym i kierunkiem, Promień A omija ścianę i kontynuuje, aż osiągnie maksymalną odległość, podczas gdy Promień B koliduje ze ścianą. Aby uzyskać więcej informacji na ten temat, zobacz Rysowanie promieni.

Parametry castLaserRay() określają, że wywołania Raycast() muszą uwzględniać każdą część w przestrzeni roboczej oprócz postaci, która strzelała. Skrypt następnie rzuca promień dla każdego kierunku w tabeli directions. Jeśli promień trafi w coś, generuje RaycastResult, który ma pięć właściwości:
- Distance – Odległość między punktem początkowym promienia a punktem przecięcia.
- Material – Enum.Material w punkcie przecięcia.
Wartość Instance jest najważniejsza z tych właściwości dla rozgrywki w przykładowej grze w laserowy tag, ponieważ informuje, kiedy promienie kolidują z innymi graczami. Aby uzyskać te informacje, gra używa funkcji pomocniczej ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant. Jeśli zwraca nil, instancja nie jest częścią gracza, co oznacza, że promień trafił w nieożywiony obiekt w otoczeniu.
castLaserRay() następnie używa Position i Normal, aby stworzyć nowy CFrame, który nazywa destination promienia. Każdy promień ma cel, którym jest miejsce, w którym promień trafił w przestrzeni 3D, lub punkt na końcu jego maksymalnej odległości. W zależności od tego, jak dobrze gracze celują, wiele lub większość wartości taggedPlayer jest nil.
if result then
-- Strzał trafił w coś, sprawdź, czy to był gracz.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- Strzał nie trafił w nic, więc jego cel to
-- punkt na maksymalnej odległości.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
endWalidacja strzału
Aby zapobiec oszustwom, poprzedni rozdział Implementacja Blasterów wyjaśnia, jak blastClient powiadamia serwer o strzale za pomocą RemoteEvent, aby mógł zweryfikować wszystkie dane, które każdy klient wysyła, takie jak to, czy rzeczywiście oznaczyli innego gracza swoim blasterem. Proces walidacji promieni odbywa się w ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults, a każda kontrola odpowiada zagnieżdżonemu skryptowi modułowemu:
Najpierw getValidatedRayResults wywołuje validateRayResult, aby sprawdzić, czy każdy wpis w tabeli rayResults z klienta jest CFrame i Player (lub nil).
Następnie wywołuje isRayAngleFromOriginValid, aby porównać oczekiwane kąty rozkładu lasera z tymi z klienta. Ten kod szczególnie pokazuje zaletę używania ReplicatedStorage, ponieważ serwer może sam wywołać getDirectionsForBlast, przechować zwrócone dane jako "oczekiwane" i następnie porównać je z danymi z klienta.
Podobnie jak walidacja blasterów z poprzedniego rozdziału, isRayAngleFromOriginValid opiera się na wartości tolerancji, aby określić, co stanowi "nadmierną" różnicę w kątach:
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRoblox abstrahuje najbardziej złożone elementy matematyki, więc wynik to krótka, wysoce wielokrotnego użytku funkcja pomocnicza, która ma zastosowanie w różnych grach:
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)endNastępna kontrola jest najbardziej intuicyjna. Podczas gdy getValidatedBlastData używa DISTANCE_SANITY_CHECK_TOLERANCE_STUDS, aby zweryfikować, że gracz, który strzelał, był blisko punktu początkowego promienia, isPlayerNearPosition używa identycznej logiki, aby sprawdzić, czy oznaczony gracz był blisko celu promienia:
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseendOstatnia kontrola isRayPathObstructed używa wariacji operacji rysowania promieni, aby sprawdzić, czy cel promienia znajduje się za ścianą lub inną przeszkodą z pozycji klienta. Na przykład, jeśli złośliwy gracz systematycznie usunąłby wszystkie ściany z gry, aby oznaczać innych graczy, serwer sprawdzi i potwierdzi, że promienie są nieważne, ponieważ zna pozycje wszystkich obiektów w otoczeniu.
isRayPathObstructedlocal scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude
Żadna strategia przeciwdziałania oszustwom nie jest wszechstronna, ale ważne jest, aby rozważyć, jak złośliwi gracze mogą podchodzić do twojej gry, abyś mógł wprowadzić kontrole, które serwer może uruchomić, aby oznaczyć podejrzane zachowanie.
Zmniejsz zdrowie gracza
Po zweryfikowaniu, że gracz oznaczył innego gracza, ostatnie kroki w zakończeniu głównej pętli rozgrywki w przykładowej grze w laserowy tag to zmniejszenie zdrowia oznaczonego gracza, zwiększenie wyników na tablicy wyników i respawn gracza z powrotem do rundy.
Zaczynając od zmniejszenia zdrowia oznaczonego gracza, Spawning and respawning omawia różnicę między Player a Player.Character, a konkretnie, że postać jest modelem Humanoid. Modele Humanoid mają właściwość Health z domyślną wartością 100. Zamiast wdrażać własny system, przykładowa gra w laserowy tag korzysta z tej wbudowanej właściwości, aby śledzić, ile obrażeń gracz potrzebuje, zanim zostanie oznaczony jako wyeliminowany z rundy.
Gra przechowuje wartości obrażeń w atrybucie damagePerHit każdego blastera. Na przykład, blaster, który strzela pojedynczym promieniem laserowym, zadaje 10 punktów obrażeń, więc potrzeba dziesięciu strzałów tym blasterem, aby wyeliminować innego gracza. Aby rozpocząć proces oznaczania gracza, LaserBlastHandler wywołuje ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers, który sprawdza teraz zweryfikowaną tabelę rayResults pod kątem graczy i przekazuje damagePerHit do onPlayerTagged.

Health nie akceptuje wartości ujemnych, więc onPlayerTagged ma pewną logikę, aby utrzymać zdrowie gracza na poziomie zerowym lub wyższym. Po zweryfikowaniu, że zdrowie gracza jest powyżej zera, porównuje zdrowie z damagePerHit i używa mniejszej z dwóch wartości. Na przykład, jeśli gracz ma 10 zdrowia i zostaje trafiony przez promień laserowy zadający 15 obrażeń, promień zadaje tylko 10 punktów obrażeń.
Takie podejście do problemu może wydawać się nieco skomplikowane. Na przykład, dlaczego nie ustawić zdrowia gracza na zero, jeśli byłoby ujemne? Powód jest taki, że ustawienie wartości zdrowia omija pole siłowe. Użycie metody Humanoid:TakeDamage() zapewnia, że gracze nie otrzymują obrażeń, gdy ich pola siłowe są aktywne.
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Zablokuj ogień przyjazny
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Unikaj ujemnego zdrowia
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage zapewnia, że zdrowie nie jest obniżane, jeśli pole siłowe jest aktywne
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Przyznaj playerBlasted punkt za oznaczenie playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
endNastępnym krokiem jest zwiększenie wyników na tablicy wyników. Może wydawać się niepotrzebne, aby LaserBlastHandler dołączył gracza, który strzelał, obok danych o strzale, ale bez tych informacji gra nie może przypisać graczowi punktu za oznaczenie kogoś. Na koniec, oznaczony gracz respawnuje się z powrotem do rundy, co możesz przejrzeć w Spawning and Respawning.
Pięć rozdziałów w tym programie obejmuje główną pętlę rozgrywki w grze, ale wciąż jest wiele obszarów do zbadania, takich jak:
- Wizualizacje blasterów: Zobacz ReplicatedStorage ⟩ FirstPersonBlasterVisuals i ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- Dźwięk: Zobacz ReplicatedStorage ⟩ SoundHandler.
- Tryby niestandardowe: Jak możesz zmodyfikować tę grę, aby wprowadzić nowe typy celów, takie jak zdobycie największej liczby punktów przed upływem czasu?
Aby uzyskać rozszerzoną logikę rozgrywki dla gry w laserowy tag, a także wielokrotnego użytku, wysokiej jakości zasoby środowiskowe, zapoznaj się z szablonem Laser Tag.