العودة إلى المدونة
Guide6 دقيقة قراءة

مثال addBoxZone من ox_target - استهداف FiveM باستخدام Raycast بلغة Lua

مثال addBoxZone من ox_target مع كود Lua للنسخ واللصق لاستهداف FiveM باستخدام Raycast. يشمل أيضًا addSpherezone و qb-target. QBCore، ESX وStandalone.

Agency Scripts

المؤسس والمطور الرئيسي في Agency Scripts

ما هو Raycasting في FiveM؟

Raycasting هي تقنية إطلاق خط غير مرئي (شعاع) من نقطة في الفضاء ثلاثي الأبعاد إلى أخرى والتحقق مما يصطدم به على طول الطريق. في FiveM، يُستخدم Raycasting لأنظمة الاستهداف، التفاعل مع الكائنات، فحوصات خط الرؤية، اكتشاف السطح، وآليات التصويب المخصصة. يوفر محرك اللعبة عدة دوال Raycast تتيح لك اكتشاف الكيانات والأسطح والهندسة العالمية بدقة. فهم Raycasting يفتح آليات لعب متقدمة مثل الاستهداف المخصص، أنظمة الوضع الذكية، مؤشرات الليزر، والتفاعلات الحساسة للسياق التي تستجيب لما ينظر إليه اللاعب.

رسم الأشعة الأساسي مع StartShapeTestRay

أبسط شكل من أشكال raycasting يستخدم 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

أعلام وفلترة Raycast

معامل 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

بناء نظام الاستهداف

نظام الاستهداف يجمع بين raycasting وتصنيف الكيانات لتمكين اللاعبين من اختيار والتفاعل مع كيانات محددة في عالم اللعبة. هذا هو الأساس لأنظمة التفاعل مثل 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

كشف السطح ووضع الأجسام

يعد Raycasting ضروريًا لأنظمة وضع الأشياء حيث يشير اللاعب إلى سطح ويتبع معاينة الكائن هدفه. تخبرك العادية السطحية التي يعيدها Raycast باتجاه السطح، مما يسمح لك بمحاذاة الكائنات الموضوعة بشكل صحيح على المنحدرات والجدران والأسقف.

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

فحوصات خط الرؤية

Raycasting هو الطريقة القياسية للتحقق مما إذا كان بإمكان نقطتين رؤية بعضهما البعض دون عوائق. يُستخدم هذا في أنظمة التخفي، ووعي الذكاء الاصطناعي، وآليات القناص، وأي طريقة لعب تعتمد على الرؤية. أطلق شعاعًا من نقطة إلى أخرى وتحقق مما إذا كان يصطدم بشيء قبل الوصول إلى الهدف.

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

تحسين الأداء لعمليات Raycasts

  • تحديد تردد raycast. لا تقم بإرسال أشعة في كل إطار إلا إذا كان ذلك ضروريًا للغاية. بالنسبة لفحوصات التفاعل، يكون كل 100 مللي ثانية عادةً كافيًا. استخدم إرسال الأشعة لكل إطار فقط لأنظمة التصويب النشطة وأنظمة التمركز.
  • استخدم العلامات المحددة. الإلقاء ضد -1 (كل شيء) أبطأ بكثير من استهداف أنواع الكيانات المحددة. قم بالتصفية لتشمل فقط ما تحتاجه.
  • خزن النتائج مؤقتًا. إذا لم يتحرك اللاعب أو ينظر في اتجاه مختلف، ستكون نتيجة raycast هي نفسها. تخطى عمليات الإرسال المكررة بمقارنة موقع الكاميرا ودورانها.
  • استخدم فحوصات المسافة أولاً. قبل استخدام رسم الأشعة للتحقق من تفاعل الكيان، تحقق من أن اللاعب ضمن نصف قطر معقول. فحوصات المسافة أرخص بكثير من رسم الأشعة.
  • يفضل استخدام StartShapeTestLosProbe لخط الرؤية. لفحوصات خط الرؤية البسيطة بين نقطتين معروفتين, StartShapeTestLosProbe أخف من StartShapeTestRay لأنه يعيد فقط قيمة منطقية ضربة/لا ضربة.
  • تجنب استخدام raycasting عبر الخريطة بأكملها. حافظ على مسافات قصوى معقولة. شعاع 50 وحدة أرخص بكثير من شعاع 1000 وحدة.

جاهز للبدء؟

احصل على السكربتات من متجرنا، أو انضم إلى Discord للدعم والتحديثات ونظرة على ما هو قادم.