Wykrywanie trafień

*Ta zawartość została przetłumaczona przy użyciu narzędzi AI (w wersji beta) i może zawierać błędy. Aby wyświetlić tę stronę w języku angielskim, kliknij tutaj.

Wykrywanie trafień to proces identyfikacji, kiedy strzały kolidują z graczami, a następnie odpowiednie zmniejszenie ich zdrowia. Na wysokim poziomie możesz pomyśleć o tej pracy jako o:

  1. Fizycznej symulacji sprawdzającej, czy pocisk uderzył w cel.
  2. Natychmiastowym sprawdzeniu, czy blaster był wymierzony w cel.

Typ detekcji trafień, którego używasz, zależy od wymagań gameplayowych twojego doświadczenia. Na przykład, fizyczna symulacja jest odpowiednia dla doświadczenia w grze w dwa ognie, gdzie piłki muszą odchodzić od ręki 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 doświadczenia w laser 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ładowego doświadczenia laser tag jako odniesienia, ta część samouczka nauczy cię o skryptach za detekcją trafień w przestrzeni 3D, w tym wskazówki dotyczące:

  • Uzyskiwania kierunku strzału z aktualnych wartości kamery oraz typu blastera gracza.
  • Wysyłania promieni w prostoliniowym kierunku z blastera w momencie strzału.
  • Walidacji strzału, aby zapobiec wykorzystaniu danych z blastera.
  • Redukcji zdrowia gracza zgodnie z obrażeniami od każdego typu blastera oraz tym, ile promieni trafiło w gracza.

Po zakończeniu tej sekcji możesz zbadać dodatkowe tematy rozwoju, aby poprawić swoją rozgrywkę, takie jak dźwięk, oświetlenie i efekty specjalne.

Uzyskaj kierunek strzału

Po tym, jak gracz wystrzeli swój blaster, ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData wywołuje dwie funkcje, aby rozpocząć proces detekcji trafień: rayDirections() i rayResults().

generateBlastData

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

Wejścia dla rayDirections są proste: aktualna pozycja i wartości rotacji kamery oraz typ blastera gracza. Gdyby przykładowe doświadczenie laser tag dawało graczom jedynie blastery, które produkują pojedynczy promień laserowy, ReplicatedStorageLaserRaygetDirectionsForBlast byłoby zbędne, ponieważ można by użyć currentCamera.CFrame.LookVector do obliczenia kierunku strzału.

Jednakże, ponieważ próbka dostarcza dodatkowy typ blastera, który produkuje kilka promieni laserowych z szerokim, poziomym rozprzestrzenieniem, getDirectionsForBlast musi obliczyć kierunek dla każdego promienia laserowego w rozprzestrzenieniu zgodnie z ich kątami w konfiguracji blastera:

getDirectionsForBlast

if numLasers == 1 then
-- Dla pojedynczych laserów, celują prosto
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Dla wielu laserów, rozprzestrzeniają 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
end

Aby jeszcze bardziej zilustrować ten koncept, jeśli miałbyś dodać trzeci typ blastera z szerokim, wertykalnym rozprzestrzenieniem, mógłbyś stworzyć nowy atrybut blastera, taki jak spreadDirection, a następnie dostosować obliczenia CFrame, aby używać innej osi. Na przykład, zauważ 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 directions

Ostatecznie funkcja rayDirections() zwraca tabelę Vectors, która reprezentuje kierunek każdego promienia laserowego. Jeśli to pomocne, możesz dodać jakieś logowanie, aby zobaczyć, jak dane te wyglądają.

generateBlastData

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)

Rzuć promienie

castLaserRay(), druga funkcja w ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData, wykonuje bardziej złożone operacje w skrypcie. Zaczyna się od określenia parametrów, aby mogła wykonywać wywołania Workspace:Raycast() w celach rzutowania promieni. Rzutowanie promieni to proces wysyłania niewidzialnego promienia z punktu Vector3 w określonym kierunku z określoną długością, a następnie sprawdzanie jego trasy, aby zobaczyć, gdzie krzyżuje się z innymi obiektami.

Informacje te są szczególnie użyteczne dla doświadczeń typu first-person shooter, ponieważ pozwalają zrozumieć, kiedy i gdzie strzały krzyżują się z graczami lub środowiskiem. Na przykład, poniższy obraz ilustruje dwa promienie, które są rzucone równolegle do siebie. Zgodnie z punktem początkowym i kierunkiem, Promień A mija ścianę i kontynuuje aż do osiągnięcia maksymalnej odległości, podczas gdy Promień B zderza się ze ścianą. Aby uzyskać więcej informacji na temat tego procesu, zapoznaj się z Rzutowaniem promieni.

Diagram, na którym Promień A przechodzi przez ścianę, a Promień B zderza się ze ścianą.

Parametry castLaserRay() określają, że wywołania Raycast() muszą brać pod uwagę każdą część w przestrzeni roboczej z wyjątkiem postaci, która wystrzeliła. Skrypt następnie rzuca promień dla każdego kierunku w tabeli directions. Jeśli promień coś trafi, generuje RaycastResult, który ma pięć właściwości:

Wartość Instance jest najważniejsza z tych właściwości dla rozgrywki w przykładowym doświadczeniu laser tag, ponieważ komunikuje, kiedy promienie zderzają się z innymi graczami. Aby uzyskać te informacje, doświadczenie używa funkcji pomocniczej ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant. Jeśli zwróci nil, instancja nie jest częścią gracza, co oznacza, że promień trafił w nieożywiony obiekt w środowisku.

castLaserRay() następnie używa Position i Normal, aby stworzyć nowy CFrame, który nazywa destination promienia. Każdy promień ma cel, i jest to albo miejsce, w którym promień trafił w przestrzeni 3D, albo 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 może być nil.

castLaserRay

if result then
-- Trafienie coś, sprawdź, czy to był gracz.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- Trafienie nie trafiło 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
end

Walidacja strzału

Aby zapobiec oszustwom, poprzedni rozdział Implementacja Blasterów wyjaśnia, jak blastClient powiadamia serwer o strzale przy użyciu RemoteEvent, aby mógł zweryfikować wszystkie dane, które wysyła każdy klient, takie jak to, czy faktycznie oznaczył innego gracza swoim blasterem. Proces walidacji promieni zachodzi w ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults, a każda kontrole jest związana z zagnieżdżonym skryptem modułu:

  1. Najpierw getValidatedRayResults wywołuje validateRayResult, aby sprawdzić, czy każdy wpis w tabeli rayResults od klienta jest CFrame i Player (lub nil).

  2. Następnie wywołuje isRayAngleFromOriginValid, aby porównać oczekiwane kąty rozprzestrzenienia lasera z tymi od klienta. Ten kod szczególnie pokazuje zaletę używania ReplicatedStorage, ponieważ serwer może wywołać getDirectionsForBlast samodzielnie, zapisać wyniki jako "oczekiwane" dane, a następnie porównać je z danymi od klienta.

    Podobnie jak walidacja blasterów w poprzednim rozdziale, isRayAngleFromOriginValid polega na wartości tolerancji, aby określić, co stanowi "nadmierną" różnicę kątów:

    isRayAngleFromOriginValid

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

    Roblox abstruuje najtrudniejsze elementy matematyki, dzięki czemu wynik jest krótką, wysoko-wielokrotnego użytku funkcją pomocniczą, która może być stosowana w różnych doświadczeniach:

    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. Następnie kontrola jest najintuitwniejsza. Podczas gdy getValidatedBlastData używa DISTANCE_SANITY_CHECK_TOLERANCE_STUDS, aby zweryfikować, że gracz, który strzelił, był blisko punktu początkowego promienia, isPlayerNearPosition wykorzystuje identyczną logikę, aby sprawdzić, czy oznaczony gracz był blisko celu promienia:

    isPlayerNearPosition

    local distanceFromCharacterToPosition = position - character:GetPivot().Position
    if distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS then
    return false
    end
  4. Ostatnia kontrola isRayPathObstructed używa wariacji operacji rzutowania 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 doświadczenia, aby oznaczyć innych graczy, serwer sprawdziłby i potwierdził, że promienie są nieprawidłowe, ponieważ zna każdą pozycję obiektów w środowisku.

    isRayPathObstructed

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

Nie ma strategii antypirackiej, która byłaby wszechstronna, ale важно rozważyć, jak złośliwi gracze mogą podchodzić do twojego doświadczenia, aby móc 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ładowym doświadczeniu laser tag to zmniejszenie zdrowia oznaczonego gracza, zwiększenie punktacji w tabeli oraz respawnowanie 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 to, że postać jest modelem Humanoid. Modele Humanoid mają właściwość Health z domyślną wartością 100. Zamiast implementować własny system, przykładowe doświadczenie laser tag wykorzystuje tę wbudowaną właściwość, aby śledzić, ile obrażeń gracz potrzebuje, zanim zostanie oznaczony.

Doświadczenie przechowuje wartości obrażeń w atrybutach damagePerHit każdego blastera. Na przykład, blaster, który wystrzeliwuje pojedynczy promień laserowy, zadaje 10 punktów obrażeń, więc potrzeba dziesięciu strzałów z tego blastera, aby oznaczyć innego gracza. Aby rozpocząć proces oznaczania gracza, LaserBlastHandler wywołuje ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers, która sprawdza teraz zweryfikowaną tabelę rayResults pod kątem graczy i przekazuje damagePerHit do onPlayerTagged.

Health nie akceptuje wartości ujemnych, więc onPlayerTagged ma trochę logiki, 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ń, laser zadaje tylko 10 punktów obrażeń.

Takie podejście do problemu może wydawać się trochę złożone. Na przykład, dlaczego po prostu nie ustawić zdrowia gracza na zero, jeśli byłoby ujemne? Powód jest taki, że ustawianie wartości zdrowia omija pole ochronne. Używając metody Humanoid:TakeDamage() zapewniasz, że gracze nie otrzymają obrażeń, gdy ich pola ochronne są aktywne.

onPlayerTagged

local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Zablokuj ogień przyjacielski
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 ochronne jest aktywne
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Przyznaj graczowiBlasted punkt za oznaczenie playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end

Następnym krokiem jest zwiększenie punktacji w tabeli. Mogło się wydawać, że niepotrzebne było dołączanie gracza, który strzelił, obok danych o strzale w LaserBlastHandler, ale bez tych informacji doświadczenie nie może zapisać gracza, który kogoś oznaczył. W końcu, oznaczony gracz respawnowany jest z powrotem do rundy, co można sprawdzić w Spawning and Respawning.

Pięć rozdziałów w tym kursie obejmuje główną pętlę rozgrywki doświadczenia, ale są jeszcze liczne obszary do zgłębienia, takie jak:

  • Wizualizacje blasterów: Zobacz ReplicatedStorageFirstPersonBlasterVisuals oraz ServerScriptServiceThirdPersonBlasterVisuals.
  • Audio: Zobacz ReplicatedStorageSoundHandler.
  • Niestandardowe tryby: Jak możesz zmodyfikować to doświadczenie, aby wprowadzić nowe rodzaje celów, takie jak zdobywanie największej liczby punktów przed upływem czasu?

Aby uzyskać rozszerzoną logikę rozgrywki dla doświadczenia laser tag, a także wielokrotnego użytku, wysokiej jakości zasoby środowiskowe, zapoznaj się z szablonem Laser Tag.

©2026 Roblox Corporation. Nazwa Roblox, logo Roblox oraz hasło „Powering Imagination” należą do naszych zarejestrowanych i niezarejestrowanych znaków towarowych na terenie Stanów Zjednoczonych oraz w innych krajach.