O que é Raycasting no FiveM?
Raycasting é a técnica de disparar uma linha invisível (raio) de um ponto a outro no espaço 3D e verificar o que ela atinge ao longo do caminho. No FiveM, o raycasting é usado para sistemas de mira, interação de objetos, verificações de linha de visão, detecção de superfície e mecânica de mira personalizada. O mecanismo de jogo fornece vários nativos de raycast que permitem detectar entidades, superfícies e geometria mundial com precisão. Compreender o raycasting desbloqueia mecânicas de jogo avançadas, como segmentação personalizada, sistemas de posicionamento inteligentes, ponteiros laser e interações sensíveis ao contexto que respondem ao que o jogador está olhando.
Raycasting básico com StartShapeTestRay
A forma mais simples de raycasting usa StartShapeTestRay que lança um raio entre duas coordenadas e retorna informações sobre a primeira coisa que atinge. O resultado inclui o status do acerto, as coordenadas exatas do ponto de impacto, o vetor normal da superfície e o identificador da entidade se uma entidade foi atingida. Você deve chamar GetShapeTestResult no próximo quadro para recuperar os 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
Sinalizações e filtragem do Raycast
O parâmetro flags em StartShapeTestRay controla quais tipos de objetos o raio pode atingir. Usar os sinalizadores corretos é crucial para desempenho e precisão. Verificar tudo (-1) é caro e geralmente retorna resultados indesejados, como limites de colisão invisíveis. Use sinalizações específicas para segmentar apenas o que você precisa.
-- 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
Construindo um sistema de segmentação
Um sistema de segmentação combina raycasting com filtragem de entidades para permitir que os jogadores selecionem e interajam com entidades específicas no mundo do jogo. Esta é a base para sistemas de interação como ox_target e qb-target. O sistema emite um raio da câmera a cada quadro, verifica se a entidade atingida corresponde a algum alvo registrado e exibe avisos de interação.
-- 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
Detecção de superfície e posicionamento de objetos
Raycasting é essencial para sistemas de posicionamento de objetos onde o jogador aponta para uma superfície e uma visualização do objeto segue sua mira. A normal da superfície retornada pelo raycast informa a orientação da superfície, permitindo alinhar objetos colocados corretamente em encostas, paredes e tetos.
-- 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
Verificações da linha de visão
Raycasting é a forma padrão de verificar se dois pontos podem se ver sem obstruções. Isso é usado em sistemas furtivos, reconhecimento de IA, mecânica de atiradores e qualquer jogabilidade que dependa de visibilidade. Lance um raio de um ponto a outro e verifique se ele atinge algo antes de atingir o alvo.
-- 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
Otimização de desempenho para Raycasts
- Limite a frequência de transmissão de raios. Não lance raios em todos os quadros, a menos que seja absolutamente necessário. Para verificações de interação, cada 100 ms geralmente é suficiente. Use raycasting por quadro apenas para sistemas ativos de mira e posicionamento.
- Use sinalizações específicas. A transmissão em
-1(tudo) é significativamente mais lenta do que a segmentação de tipos de entidade específicos. Filtre apenas o que você precisa. - Resultados do cache. Se o jogador não se moveu ou olhou em uma direção diferente, o resultado do raycast será o mesmo. Evite transmissões redundantes comparando a posição e a rotação da câmera.
- Use primeiro verificações de distância. Antes de lançar raios para verificar a interação da entidade, verifique se o jogador está dentro de um raio razoável. As verificações de distância são muito mais baratas que os raycasts.
- Prefira StartShapeTestLosProbe para LOS. Para verificações simples de linha de visão entre dois pontos conhecidos,
StartShapeTestLosProbeé mais leve queStartShapeTestRayporque retorna apenas um booleano de acerto/não acerto. - Evite a projeção de raios em todo o mapa. Mantenha distâncias máximas razoáveis. Um raycast de 50 unidades é muito mais barato que um de 1.000 unidades.
