Wróć do bloga
Guide6 min czytania

ox_target addBoxZone: przykład targetingu raycast Lua w FiveM

Przykład ox_target addBoxZone z kodem Lua do skopiowania dla targetingu raycast w FiveM. Obejmuje również addSpherezone & qb-target. QBCore, ESX & standalone.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Czym jest Raycasting w FiveM?

Raycasting to technika wypuszczania niewidzialnej linii (promienia) z jednego punktu w przestrzeni 3D do drugiego i sprawdzania, w co trafia po drodze. W FiveM raycasting jest używany do systemów targetowania, interakcji z obiektami, sprawdzania linii wzroku, wykrywania powierzchni i niestandardowych mechanik celowania. Silnik gry oferuje kilka natywnych funkcji raycastu, które pozwalają precyzyjnie wykrywać byty, powierzchnie i geometrię świata. Zrozumienie raycastingu odblokowuje zaawansowane mechaniki rozgrywki, takie jak własny target, inteligentne systemy stawiania obiektów, wskaźniki laserowe i interakcje kontekstowe reagujące na to, na co patrzy gracz.

Podstawowy raycasting z użyciem StartShapeTestRay

Najprostsza forma raycastingu wykorzystuje StartShapeTestRay który rzuca promień między dwoma współrzędnymi i zwraca informacje o pierwszym trafionym obiekcie. Wynik zawiera status trafienia, dokładne współrzędne punktu uderzenia, wektor normalny powierzchni oraz uchwyt entity, jeśli trafiono entity. Musisz wywołać GetShapeTestResult w następnej klatce, aby pobrać wyniki.

-- client/raycast_basic.lua
local function Raycast(origin, direction, maxDistance, flags, ignoreEntity)
    local destination = origin + direction * maxDistance

    local shapeTest = StartShapeTestRay(
        origin.x, origin.y, origin.z,
        destination.x, destination.y, destination.z,
        flags or -1,      -- flags: -1 = everything
        ignoreEntity or 0, -- entity to ignore
        0
    )

    local _, hit, endCoords, surfaceNormal, entityHit = GetShapeTestResult(shapeTest)

    return {
        hit = hit == 1,
        coords = endCoords,
        normal = surfaceNormal,
        entity = entityHit,
    }
end

-- Raycast from camera to world (what the player is looking at)
local function GetPlayerAimTarget(maxDist)
    local camCoords = GetGameplayCamCoord()
    local camRot = GetGameplayCamRot(2)

    -- Convert rotation to direction vector
    local radX = math.rad(camRot.x)
    local radZ = math.rad(camRot.z)

    local direction = vector3(
        -math.sin(radZ) * math.abs(math.cos(radX)),
         math.cos(radZ) * math.abs(math.cos(radX)),
         math.sin(radX)
    )

    local playerPed = PlayerPedId()
    return Raycast(camCoords, direction, maxDist or 50.0, -1, playerPed)
end

Flagi i filtrowanie raycast

Parametr flags w StartShapeTestRay kontroluje, w jakie typy obiektów może trafić promień. Użycie poprawnych flag jest kluczowe dla wydajności i dokładności. Sprawdzanie względem wszystkiego (-1) jest kosztowne i często zwraca niepożądane wyniki, takie jak niewidzialne granice kolizji. Użyj określonych flag, aby celować tylko w to, czego potrzebujesz.

-- client/raycast_flags.lua
-- Common flag values for StartShapeTestRay
local RayFlags = {
    WORLD        = 1,      -- Static world geometry (buildings, terrain)
    VEHICLES     = 2,      -- Vehicles
    PEDS         = 4,      -- Pedestrians and players (on foot)
    OBJECTS      = 16,     -- Props and objects
    WATER        = 32,     -- Water surfaces
    VEGETATION   = 256,    -- Trees and bushes

    -- Common combinations
    WORLD_AND_VEHICLES = 3,
    WORLD_AND_OBJECTS  = 17,
    ENTITIES_ONLY      = 22, -- Vehicles + Peds + Objects
    ALL                = -1, -- Everything
}

-- Example: Only detect vehicles (for a speed camera script)
local function DetectVehicleAhead(origin, direction)
    return Raycast(origin, direction, 100.0, RayFlags.VEHICLES, PlayerPedId())
end

-- Example: Detect ground position (for object placement)
local function GetGroundPosition(x, y, z)
    local result = Raycast(
        vector3(x, y, z + 50.0),
        vector3(0.0, 0.0, -1.0),
        100.0,
        RayFlags.WORLD
    )

    if result.hit then
        return result.coords
    end
    return nil
end

Tworzenie systemu targetingu

System targetingu łączy raycasting z filtrowaniem bytów (entities), umożliwiając graczom wybieranie określonych obiektów w świecie gry i wchodzenie z nimi w interakcję. Jest to fundament systemów interakcji, takich jak ox_target czy qb-target. System rzuca promień z kamery w każdej klatce, sprawdza, czy trafiony byt pasuje do zarejestrowanych celów, i wyświetla podpowiedzi interakcji.

-- client/targeting.lua
local TargetSystem = {
    targets = {},
    currentTarget = nil,
    enabled = true,
}

function TargetSystem.AddTarget(entity, options)
    TargetSystem.targets[entity] = {
        label = options.label or 'Interact',
        icon = options.icon or 'fas fa-hand',
        distance = options.distance or 3.0,
        canInteract = options.canInteract,
        onSelect = options.onSelect,
    }
end

function TargetSystem.RemoveTarget(entity)
    TargetSystem.targets[entity] = nil
end

-- Main targeting loop
CreateThread(function()
    while true do
        if TargetSystem.enabled then
            local aimResult = GetPlayerAimTarget(10.0)

            if aimResult.hit and aimResult.entity ~= 0 then
                local entity = aimResult.entity
                local target = TargetSystem.targets[entity]

                if target then
                    local playerCoords = GetEntityCoords(PlayerPedId())
                    local entityCoords = GetEntityCoords(entity)
                    local dist = #(playerCoords - entityCoords)

                    if dist <= target.distance then
                        local canInteract = true
                        if target.canInteract then
                            canInteract = target.canInteract(entity)
                        end

                        if canInteract then
                            TargetSystem.currentTarget = entity

                            -- Draw interaction prompt
                            DrawText3D(entityCoords.x, entityCoords.y, entityCoords.z + 1.0,
                                target.label)

                            -- Handle interaction key press
                            if IsControlJustPressed(0, 38) then -- E key
                                target.onSelect(entity)
                            end
                        end
                    end
                end
            else
                TargetSystem.currentTarget = nil
            end
        end

        Wait(0)
    end
end)

-- Helper: Draw 3D text at world coordinates
function DrawText3D(x, y, z, text)
    SetTextScale(0.35, 0.35)
    SetTextFont(4)
    SetTextProportional(true)
    SetTextColour(255, 255, 255, 215)
    SetTextEntry('STRING')
    SetTextCentre(true)
    AddTextComponentString(text)

    SetDrawOrigin(x, y, z, 0)
    DrawText(0.0, 0.0)
    ClearDrawOrigin()
end

Wykrywanie powierzchni i umieszczanie obiektów

Raycasting jest niezbędny w systemach stawiania obiektów, w których gracz wskazuje powierzchnię, a podgląd obiektu podąża za jego celownikiem. Wektor normalny powierzchni zwracany przez raycast informuje o orientacji powierzchni, co pozwala poprawnie dopasować stawiane obiekty na zboczach, ścianach i sufitach.

-- client/placement.lua
local Placement = {
    active = false,
    previewEntity = nil,
    modelName = nil,
}

function Placement.Start(modelName)
    Placement.modelName = modelName
    Placement.active = true

    -- Create preview prop
    local model = joaat(modelName)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(10) end

    Placement.previewEntity = CreateObject(model, 0.0, 0.0, 0.0, false, true, false)
    SetEntityAlpha(Placement.previewEntity, 150, false)
    SetEntityCollision(Placement.previewEntity, false, false)
    FreezeEntityPosition(Placement.previewEntity, true)
    SetModelAsNoLongerNeeded(model)

    CreateThread(function()
        while Placement.active do
            local result = GetPlayerAimTarget(15.0)

            if result.hit then
                local coords = result.coords
                SetEntityCoords(Placement.previewEntity,
                    coords.x, coords.y, coords.z, false, false, false, false)

                -- Align to surface normal
                local normal = result.normal
                local pitch = math.deg(math.asin(normal.z)) - 90.0
                SetEntityRotation(Placement.previewEntity, pitch, 0.0,
                    GetEntityHeading(PlayerPedId()), 2, true)

                -- Color: green if valid, red if not
                local valid = IsPlacementValid(coords)
                if valid then
                    SetEntityDrawOutline(Placement.previewEntity, true)
                end

                -- Confirm placement
                if IsControlJustPressed(0, 38) and valid then
                    Placement.Confirm(coords)
                end
            end

            -- Cancel placement
            if IsControlJustPressed(0, 73) then -- X key
                Placement.Cancel()
            end

            Wait(0)
        end
    end)
end

function Placement.Confirm(coords)
    Placement.active = false

    if Placement.previewEntity then
        DeleteEntity(Placement.previewEntity)
        Placement.previewEntity = nil
    end

    TriggerServerEvent('myresource:placeObject', Placement.modelName, coords)
end

function Placement.Cancel()
    Placement.active = false
    if Placement.previewEntity then
        DeleteEntity(Placement.previewEntity)
        Placement.previewEntity = nil
    end
end

function IsPlacementValid(coords)
    local playerCoords = GetEntityCoords(PlayerPedId())
    local dist = #(playerCoords - coords)
    return dist >= 1.0 and dist <= 10.0
end

Sprawdzanie linii widzenia

Raycasting to standardowy sposób sprawdzania, czy dwa punkty widzą się nawzajem bez przeszkód. Jest używany w systemach skradania, świadomości AI, mechanikach snajperskich i każdym elemencie rozgrywki zależnym od widoczności. Wypuść promień z jednego punktu do drugiego i sprawdź, czy w coś trafia, zanim dotrze do celu.

-- shared/los.lua (can run on both client and server)
local function HasLineOfSight(from, to, ignoreEntity)
    local direction = to - from
    local distance = #direction
    direction = direction / distance -- normalize

    local result = Raycast(from, direction, distance, RayFlags.WORLD, ignoreEntity)

    if not result.hit then
        return true -- nothing blocking the path
    end

    -- Check if the hit point is past the target
    local hitDist = #(from - result.coords)
    return hitDist >= distance * 0.95
end

-- Usage: Check if NPC can see the player
local function CanNPCSeePlayer(npcPed, playerPed)
    local npcCoords = GetEntityCoords(npcPed) + vector3(0, 0, 0.7)
    local playerCoords = GetEntityCoords(playerPed) + vector3(0, 0, 0.7)

    return HasLineOfSight(npcCoords, playerCoords, npcPed)
end

Optymalizacja wydajności dla raycastów

  • Ogranicz częstotliwość raycastów. Nie wykonuj raycastingu w każdej klatce, chyba że jest to absolutnie konieczne. Do sprawdzania interakcji zazwyczaj wystarczy co 100 ms. Używaj raycastingu w każdej klatce wyłącznie przy aktywnym celowaniu i systemach pozycjonowania obiektów.
  • Używaj określonych flag. Rzutowanie na -1 (wszystko) jest znacznie wolniejsze niż celowanie w określone typy bytów. Filtruj tylko to, czego potrzebujesz.
  • Cachuj wyniki. Jeśli gracz się nie poruszył ani nie spojrzał w innym kierunku, wynik raycasta będzie taki sam. Pomiń zbędne rzuty promienia, porównując pozycję i rotację kamery.
  • Najpierw sprawdzaj dystans. Zanim użyjesz raycastingu do sprawdzenia interakcji z bytem, upewnij się, że gracz znajduje się w rozsądnym promieniu. Sprawdzanie odległości jest o rzędy wielkości mniej zasobożerne niż raycasting.
  • Preferuj StartShapeTestLosProbe dla LOS. Do prostego sprawdzania pola widzenia między dwoma znanymi punktami, StartShapeTestLosProbe jest lżejszy niż StartShapeTestRay ponieważ zwraca tylko wartość boolean informującą o trafieniu lub jego braku.
  • Unikaj raycastingu przez całą mapę. Zachowaj rozsądne maksymalne odległości. Raycast na 50 jednostek jest znacznie lżejszy niż na 1000 jednostek.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.