Назад к блогу
Guide6 мин чтения

ox_target addBoxZone Пример - FiveM Raycast Targeting Lua

Пример addBoxZone для ox_target с кодом на Lua для FiveM raycast targeting, готовым к копированию и вставке. Также охватывает addSpherezone и qb-target. QBCore, ESX и standalone.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Что такое Raycasting в FiveM?

Raycasting , это техника, при которой из одной точки в 3D пространстве в другую выпускается невидимая линия (луч), и проверяется, что она задевает по пути. В FiveM raycasting используется для систем прицеливания, взаимодействия с объектами, проверки линии видимости, обнаружения поверхностей и пользовательских механик прицеливания. Игровой движок предоставляет несколько native-функций raycast, позволяющих точно обнаруживать сущности, поверхности и геометрию мира. Понимание raycasting открывает доступ к продвинутым игровым механикам, таким как пользовательское прицеливание, умные системы размещения, лазерные указки и контекстно-зависимые взаимодействия, реагирующие на то, куда смотрит игрок.

Базовый Raycasting с StartShapeTestRay

Самая простая форма raycasting использует StartShapeTestRay который отбрасывает луч между двумя координатами и возвращает информацию о первом объекте, в который он попал. Результат включает статус попадания, точные координаты точки удара, вектор нормали поверхности и дескриптор сущности, если была поражена сущность. Вы должны вызвать GetShapeTestResult на следующем кадре для получения результатов.

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

Флаги и фильтрация Raycast

Параметр flags в StartShapeTestRay управляет типами объектов, по которым может проходить луч. Использование правильных флагов критично для производительности и точности. Проверка по всему (-1) дорогостоящий и часто возвращает нежелательные результаты, например невидимые коллизионные границы. Используйте конкретные флаги для таргетинга только нужного.

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

Создание системы прицеливания

Система таргетинга сочетает лучевой кастинг с фильтрацией сущностей, позволяя игрокам выбирать и взаимодействовать с конкретными объектами в игровом мире. Это основа для систем взаимодействия, таких как ox_target и qb-target. Система каждый кадр посылает луч от камеры, проверяет, совпадает ли попавшая сущность с зарегистрированными целями, и отображает подсказки для взаимодействия.

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

Обнаружение поверхности и размещение объектов

Raycasting необходим для систем размещения объектов, когда игрок указывает на поверхность, и предварительный просмотр объекта следует за прицелом. Нормаль поверхности, возвращаемая raycast, показывает ориентацию поверхности, позволяя правильно выравнивать объекты на склонах, стенах и потолках.

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

Проверки линии видимости

Raycasting , стандартный способ проверить, видят ли две точки друг друга без препятствий. Это используется в системах скрытности, осведомленности ИИ, механике снайпера и любом геймплее, зависящем от видимости. Выпускайте луч от одной точки к другой и проверяйте, не столкнулся ли он с чем-то до достижения цели.

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

Оптимизация производительности для Raycasts

  • Ограничьте частоту лучевого кастинга. Не выполняйте лучевой кастинг каждый кадр, если это не абсолютно необходимо. Для проверок взаимодействия обычно достаточно делать это каждые 100 мс. Лучевой кастинг на каждый кадр используйте только для активного прицеливания и систем размещения.
  • Используйте конкретные флаги. Кастинг против -1 (всё) работает значительно медленнее, чем при таргетинге конкретных типов сущностей. Фильтруйте только то, что вам нужно.
  • Кэшировать результаты. Если игрок не перемещался и не менял направление взгляда, результат raycast будет одинаковым. Пропускайте лишние запросы, сравнивая позицию и поворот камеры.
  • Сначала используйте проверки расстояния. Перед выполнением raycasting для проверки взаимодействия с объектом убедитесь, что игрок находится в разумном радиусе. Проверки расстояния в разы дешевле, чем raycasting.
  • Предпочитайте StartShapeTestLosProbe для LOS. Для простых проверок прямой видимости между двумя известными точками, StartShapeTestLosProbe легче, чем StartShapeTestRay потому что он возвращает только булево значение попадания/промаха.
  • Избегайте лучевого кастинга по всей карте. Соблюдайте разумные максимальные расстояния. Raycast на 50 единиц гораздо дешевле, чем на 1000 единиц.

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.