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 odpowiedniego zmniejszenia ich zdrowia. Na wysokim poziomie można to rozumieć jako:

  1. Fizycznie symulowane sprawdzenie, czy pocisk trafił w cel.
  2. 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, ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData wywołuje dwie funkcje, aby rozpocząć proces wykrywania 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 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, ReplicatedStorageLaserRaygetDirectionsForBlast 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:

getDirectionsForBlast
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
end

Aby 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 directions

Ostatecznie 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.

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)

Rzucanie promieni

castLaserRay(), druga funkcja w ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData, 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.

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

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:

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 ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant. 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.

castLaserRay
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
end

Walidacja 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 ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults, a każda kontrola odpowiada zagnieżdżonemu skryptowi modułowemu:

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

  2. 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:

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

    Roblox 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:

    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ę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:

    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 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.

    isRayPathObstructed
    local 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 ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers, 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.

onPlayerTagged
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
end

Nastę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 ReplicatedStorageFirstPersonBlasterVisuals i ServerScriptServiceThirdPersonBlasterVisuals.
  • Dźwięk: Zobacz ReplicatedStorageSoundHandler.
  • 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.

©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.