Volver al blog
Guide7 min de lectura

Ejemplo de ox_target addBoxZone: selección por raycast en FiveM con Lua

Ejemplo de ox_target addBoxZone con código Lua listo para copiar. Incluye addSpherezone y qb-target para QBCore, ESX y standalone.

Agency Scripts

Fundador y desarrollador principal en Agency Scripts

¿Qué es el raycasting en FiveM?

El raycasting es la técnica de proyectar una línea invisible (rayo) desde un punto del espacio 3D hasta otro y comprobar con qué choca por el camino. En FiveM, se usa para sistemas de apuntado, interacción con objetos, comprobaciones de línea de visión, detección de superficies y mecánicas de apuntado personalizadas. El motor del juego ofrece varios natives de raycasting que permiten detectar entidades, superficies y geometría del mundo con precisión. Entender el raycasting permite crear mecánicas de juego avanzadas, como apuntado personalizado, sistemas de colocación inteligente, punteros láser e interacciones contextuales que responden a lo que mira el jugador.

Raycasting básico con StartShapeTestRay

La forma más sencilla de raycasting utiliza StartShapeTestRay para proyectar un rayo entre dos coordenadas y devolver información sobre el primer objeto con el que choca. El resultado incluye si hubo impacto, las coordenadas exactas del punto de impacto, el vector normal de la superficie y el identificador de la entidad, si hubo impacto con una. Debes llamar a GetShapeTestResult en el siguiente frame para obtener los resultados.

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

Banderas y filtros de raycasting

El parámetro de banderas de StartShapeTestRay controla los tipos de objetos con los que puede chocar el rayo. Usar las banderas adecuadas es fundamental para el rendimiento y la precisión. Comprobar todo (-1) es costoso y a menudo devuelve resultados no deseados, como límites de colisión invisibles. Usa banderas específicas para apuntar solo a lo que necesitas.

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

Crear un sistema de apuntado

Un sistema de apuntado combina el raycasting con el filtrado de entidades para que los jugadores puedan seleccionar e interactuar con entidades específicas del mundo del juego. Esta es la base de sistemas de interacción como ox_target y qb-target. El sistema proyecta un rayo desde la cámara en cada frame, comprueba si la entidad alcanzada coincide con alguno de los objetivos registrados y muestra indicaciones de interacción.

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

Detección de superficies y colocación de objetos

El raycasting es esencial para los sistemas de colocación de objetos, donde el jugador apunta a una superficie y una vista previa del objeto sigue su puntería. La normal de la superficie que devuelve el raycast indica su orientación, lo que permite alinear correctamente los objetos colocados en pendientes, paredes y techos.

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

Comprobaciones de línea de visión

El raycasting es la forma estándar de comprobar si dos puntos pueden verse entre sí sin obstáculos. Se utiliza en sistemas de sigilo, percepción de la IA, mecánicas de francotirador y cualquier tipo de juego que dependa de la visibilidad. Lanza un rayo desde un punto hasta otro y comprueba si impacta con algo antes de llegar al objetivo.

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

Optimización del rendimiento de los raycasts

  • Limita la frecuencia de los raycasts. No lances rayos en cada fotograma salvo que sea absolutamente necesario. Para comprobar interacciones, cada 100ms suele ser suficiente. Usa raycasting en cada fotograma solo para sistemas de apuntado activo y colocación.
  • Usa flags específicos. Lanzar rayos contra -1 (todo) es mucho más lento que hacerlo contra tipos de entidades concretos. Filtra solo lo que necesites.
  • Guarda los resultados en caché. Si el jugador no se ha movido ni ha mirado en otra dirección, el resultado del raycast será el mismo. Evita repetir raycasts comparando la posición y la rotación de la cámara.
  • Comprueba primero la distancia. Antes de usar un raycast para comprobar una interacción con una entidad, verifica que el jugador esté dentro de un radio razonable. Las comprobaciones de distancia son mucho más baratas que los raycasts.
  • Prioriza StartShapeTestLosProbe para LOS. Para comprobaciones sencillas de línea de visión entre dos puntos conocidos, StartShapeTestLosProbe es más ligero que StartShapeTestRay porque solo devuelve un booleano de impacto/no impacto.
  • Evita hacer raycasts a través de todo el mapa. Mantén las distancias máximas dentro de lo razonable. Un raycast de 50 unidades es mucho más barato que uno de 1000 unidades.

¿Listo para empezar?

Consigue los scripts en nuestra tienda, o únete al Discord para soporte, novedades y un adelanto de lo que viene.