返回博客
Guide4 分钟阅读

ox_target addBoxZone 示例 - FiveM 射线投射目标 Lua

ox_target addBoxZone 示例,附带可复制粘贴的 FiveM 射线投射目标 Lua 代码。也涵盖 addSpherezone 和 qb-target。支持 QBCore、ESX 和 Standalone。

Agency Scripts

Agency Scripts 创始人兼首席开发者

FiveM 中的 Raycasting 是什么?

射线投射是从三维空间一点向另一点发射一条不可见线(射线),检查沿途击中物体的技术。在FiveM中,射线投射用于目标系统、对象交互、视线检测、表面检测和自定义瞄准机制。游戏引擎提供多个射线投射原生函数,能精准检测实体、表面和世界几何。理解射线投射可解锁高级玩法机制,如自定义目标、智能放置系统、激光指示器和根据玩家视线响应的上下文交互。

使用StartShapeTestRay的基础射线检测

最简单的射线投射形式使用 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

射线标志和过滤

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

表面检测与物体放置

射线投射是对象放置系统的关键,玩家指向表面时,物体预览跟随瞄准。射线返回的表面法线告诉你表面方向,使你能正确对齐放置在斜坡、墙壁和天花板上的物体。

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

视线检测

射线投射是检查两点之间是否有障碍物阻挡视线的标准方法。用于潜行系统、AI感知、狙击机制及任何依赖可见性的玩法。从一点向另一点投射射线,检查是否在到达目标前击中物体。

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

射线投射性能优化

  • 限制射线检测频率。 除非绝对必要,否则不要每帧投射射线。对于交互检查,每 100 毫秒一次通常足够。仅在主动瞄准和放置系统中使用每帧射线投射。
  • 使用特定标志。 对抗施法 -1 (所有内容)比针对特定实体类型的速度明显慢。请仅筛选您需要的内容。
  • 缓存结果。 如果玩家未移动或未改变视角,射线检测结果将相同。通过比较摄像机位置和旋转跳过冗余检测。
  • 先使用距离检测。 在进行射线检测以检查实体交互之前,确认玩家处于合理半径内。距离检查比射线检测便宜几个数量级。
  • 优先使用 StartShapeTestLosProbe 进行视线检测。 用于两个已知点之间的简单视线检查, StartShapeTestLosProbe 比...更轻 StartShapeTestRay 因为它只返回命中/未命中布尔值。
  • 避免对整个地图进行射线投射。 保持最大距离合理。50 单位的射线检测比 1000 单位的要便宜得多。

准备好开始了吗?

在我们的商店获取脚本,或加入 Discord 获取支持、更新以及新功能预告。