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,
StartShapeTestLosProbees más ligero queStartShapeTestRayporque 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.