Guide 2026-05-07

Système de raycast et ciblage pour FiveM

TDYSKY

TDYSKY

Fondateur et développeur principal chez Agency Scripts

Qu'est-ce que le Raycasting dans FiveM ?

Le Raycasting est la technique consistant à tirer une ligne invisible (rayon) d'un point dans l'espace 3D à un autre et à vérifier ce qu'elle frappe en cours de route. Dans FiveM, le raycasting est utilisé pour les systèmes de ciblage, l'interaction avec les objets, les contrôles de ligne de vue, la détection de surface et les mécanismes de visée personnalisés. Le moteur de jeu fournit plusieurs natifs raycast qui tu permettent de détecter avec précision les entités, les surfaces et la géométrie du monde. Comprendre le raycasting débloque des mécanismes de jeu avancés tels que le ciblage personnalisé, les systèmes de placement intelligents, les pointeurs laser et les interactions contextuelles qui répondent à ce que le joueur regarde.

Raycasting de base avec StartShapeTestRay

La forme la plus simple d'utilisation du raycasting StartShapeTestRay qui projette un rayon entre deux coordonnées et renvoie des informations sur la première chose qu'il frappe. Le résultat inclut le statut d'impact, les coordonnées exactes du point d'impact, le vecteur normal à la surface et le descripteur d'entité si une entité a été touchée. Tu dois appeler GetShapeTestResult sur l'image suivante pour récupérer les résultats.

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

Indicateurs et filtrage Raycast

Le paramètre flags dans StartShapeTestRay contrôle les types d'objets que le rayon peut toucher. L’utilisation des bons indicateurs est cruciale pour les performances et la précision. Vérifier tout (-1) est coûteux et renvoie souvent des résultats indésirables comme des limites de collision invisibles. Utilisez des indicateurs spécifiques pour cibler uniquement ce dont tu as besoin.

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

Construire un système de ciblage

Un système de ciblage combine le raycasting et le filtrage d'entités pour permettre aux joueurs de sélectionner et d'interagir avec des entités spécifiques dans le monde du jeu. C'est la base des systèmes d'interaction tels que ox_target et qb-target. Le système projette un rayon depuis la caméra à chaque image, vérifie si l'entité touchée correspond à des cibles enregistrées et affiche des invites d'interaction.

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

Détection de surface et placement d'objets

Le Raycasting est essentiel pour les systèmes de placement d'objets dans lesquels le joueur pointe vers une surface et un aperçu de l'objet suit son objectif. La normale à la surface renvoyée par le raycast tu indique l'orientation de la surface, tu permettant d'aligner correctement les objets placés sur les pentes, les murs et les plafonds.

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

Vérifications de la ligne de vue

Le Raycasting est le moyen standard de vérifier si deux points peuvent se voir sans obstructions. Ceci est utilisé dans les systèmes furtifs, la sensibilisation à l'IA, la mécanique des tireurs d'élite et tout gameplay qui dépend de la visibilité. Lancez un rayon d'un point à un autre et vérifiez s'il touche quelque chose avant d'atteindre la cible.

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

Optimisation des performances pour les Raycasts

  • Limitez la fréquence de raycast. Ne projetez pas de rayons à chaque image, sauf en cas d'absolue nécessité. Pour les contrôles d'interaction, toutes les 100 ms sont généralement suffisantes. Utilisez uniquement le raycasting par image pour les systèmes de visée et de placement actifs.
  • Utilisez des indicateurs spécifiques. Lancer contre -1 (tout) est nettement plus lent que le ciblage de types d’entités spécifiques. Filtrez uniquement ce dont tu as besoin.
  • Mettre en cache les résultats. Si le joueur n’a pas bougé ou regardé dans une direction différente, le résultat du raycast sera le même. Évitez les castings redondants en comparant la position et la rotation de la caméra.
  • Utilisez d’abord les contrôles de distance. Avant de lancer un raycast pour vérifier l'interaction de l'entité, vérifiez que le joueur se trouve dans un rayon raisonnable. Les contrôles de distance sont des ordres de grandeur moins chers que les raycasts.
  • Préférez StartShapeTestLosProbe pour LOS. Pour de simples contrôles de visibilité directe entre deux points connus, StartShapeTestLosProbe est plus léger que StartShapeTestRay car il ne renvoie qu'un booléen hit/no-hit.
  • Évitez de diffuser des rayons sur toute la carte. Gardez les distances maximales raisonnables. Un raycast de 50 unités est beaucoup moins cher qu’un raycast de 1 000 unités.

Partager cet article

Prêt à améliorer votre serveur ?

Découvrez nos scripts FiveM premium dans la boutique Agency Scripts ou rejoignez notre communauté Discord pour le support et les mises à jour.