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

نظام سحب السيارات FiveM: سكريبت وظائف السحب الاحترافية

تشغيل وظيفة شاحنة سحب في FiveM. طلبات NPC، منطق الحجز، رسوم متحركة للسطح المسطح وأفضل سكربتات السحب لتكامل الميكانيكي والشرطة.

Agency Scripts

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

نظرة عامة على نظام شاحنة السحب

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

إعداد الوظائف ونظام الدوام

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

Config.TowJob = {
    jobName = 'tow',
    clockInLocation = vector3(409.09, -1622.57, 29.29),
    vehicleSpawn = vector4(405.87, -1631.14, 29.29, 228.35),
    towVehicles = {
        {model = 'flatbed', label = 'Flatbed Truck', rank = 0},
        {model = 'towtruck', label = 'Tow Truck (Hook)', rank = 1},
        {model = 'towtruck2', label = 'Heavy Tow Truck', rank = 2},
    },
    impoundLot = vector3(409.59, -1637.87, 29.29),
    impoundReturn = vector4(403.12, -1640.23, 29.29, 138.92),
    payPerTow = {min = 350, max = 750},
    payPerImpound = 500,
}

RegisterNetEvent('tow:server:clockIn', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    if Player.PlayerData.job.name ~= Config.TowJob.jobName then
        TriggerClientEvent('QBCore:Notify', src, 'You are not a tow driver', 'error')
        return
    end

    Player.Functions.SetJobDuty(true)
    TowDrivers[src] = {
        onDuty = true,
        currentCall = nil,
        vehicleNet = nil,
        towedVehicle = nil,
    }

    TriggerClientEvent('QBCore:Notify', src, 'You are now on duty', 'success')
    TriggerClientEvent('tow:client:spawnVehicle', src)
end)

آليات إرفاق المركبات

التحدي الفني الأساسي لنظام السحب هو ربط مركبة بأخرى بطريقة تبدو واقعية وتتصرّف بشكل صحيح أثناء القيادة. يوفر FiveM AttachEntityToEntity native لهذا الغرض، لكن ضبط مواقع الإزاحة يتطلب معايرة دقيقة لكل طراز شاحنة سحب. يعمل نهج السرير المسطح بشكل أفضل لمعظم المركبات: توضع السيارة المسحوبة على منصة السرير المسطح مع إزاحات مناسبة بحيث تجلس بشكل طبيعي على السرير. بالنسبة لشاحنات السحب ذات الخطاف، تُرفع العجلات الأمامية للمركبة المسحوبة بينما تبقى العجلات الخلفية على الأرض. يحتاج كل نمط تثبيت إلى متجهات إزاحة وقيم دوران مختلفة. اختبر هذه القيم داخل اللعبة بإجراء تعديلات صغيرة حتى تجلس المركبة بشكل صحيح دون اختراق الشاحنة:

function AttachVehicleToFlatbed(towTruck, targetVehicle)
    local towModel = GetEntityModel(towTruck)

    -- Offsets calibrated for the flatbed model
    local offsets = {
        ['flatbed'] = {x = 0.0, y = -0.5, z = 1.1, rx = 0.0, ry = 0.0, rz = 0.0},
    }

    local offset = offsets['flatbed']
    if not offset then return false end

    -- Freeze target vehicle physics
    SetEntityCollision(targetVehicle, false, false)
    FreezeEntityPosition(targetVehicle, true)

    -- Attach to flatbed
    AttachEntityToEntity(
        targetVehicle, towTruck,
        0, -- bone index
        offset.x, offset.y, offset.z,
        offset.rx, offset.ry, offset.rz,
        false, false, false, false, 0, true
    )

    -- Store attachment state
    TriggerServerEvent('tow:server:vehicleAttached', VehToNet(towTruck), VehToNet(targetVehicle))
    return true
end

function DetachVehicleFromFlatbed(towTruck, targetVehicle)
    DetachEntity(targetVehicle, true, true)
    SetEntityCollision(targetVehicle, true, true)
    FreezeEntityPosition(targetVehicle, false)

    -- Place vehicle on ground behind the truck
    local truckCoords = GetEntityCoords(towTruck)
    local truckHeading = GetEntityHeading(towTruck)
    local behind = GetOffsetFromEntityInWorldCoords(towTruck, 0.0, -12.0, 0.0)

    SetEntityCoords(targetVehicle, behind.x, behind.y, behind.z)
    SetEntityHeading(targetVehicle, truckHeading)
    SetVehicleOnGroundProperly(targetVehicle)

    TriggerServerEvent('tow:server:vehicleDetached', VehToNet(towTruck))
end

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

نظام الإرسال وطلبات السحب

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

RegisterNetEvent('tow:server:requestTow', function(reason)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local playerCoords = GetEntityCoords(GetPlayerPed(src))
    local vehicle = GetVehiclePedIsIn(GetPlayerPed(src), true)
    local vehicleModel = vehicle ~= 0 and GetDisplayNameFromVehicleModel(GetEntityModel(vehicle)) or 'Unknown'

    -- Find nearest available tow driver
    local nearestDriver = nil
    local nearestDist = math.huge

    for driverId, data in pairs(TowDrivers) do
        if data.onDuty and not data.currentCall then
            local driverCoords = GetEntityCoords(GetPlayerPed(driverId))
            local dist = #(playerCoords - driverCoords)
            if dist < nearestDist then
                nearestDist = dist
                nearestDriver = driverId
            end
        end
    end

    if nearestDriver then
        TowDrivers[nearestDriver].currentCall = {
            requesterId = src,
            coords = playerCoords,
            vehicle = vehicleModel,
            reason = reason,
            timestamp = os.time(),
        }

        TriggerClientEvent('tow:client:incomingCall', nearestDriver, {
            coords = playerCoords,
            vehicle = vehicleModel,
            reason = reason,
            playerName = Player.PlayerData.charinfo.firstname,
        })

        TriggerClientEvent('QBCore:Notify', src, 'Tow truck dispatched. ETA based on driver location.', 'success')
    else
        TriggerClientEvent('QBCore:Notify', src, 'No tow drivers available. NPC service will arrive shortly.', 'info')
        StartNPCTowService(src, playerCoords, vehicle)
    end
end)

إدارة ساحة الحجز

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

CREATE TABLE IF NOT EXISTS impounded_vehicles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    plate VARCHAR(10) NOT NULL,
    citizenid VARCHAR(50) NOT NULL,
    vehicle_data LONGTEXT NOT NULL,
    impound_reason ENUM('tow', 'police', 'abandoned', 'parking') DEFAULT 'tow',
    impound_fee INT DEFAULT 250,
    impounded_by VARCHAR(50) DEFAULT NULL,
    impounded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    hold_until TIMESTAMP DEFAULT NULL,
    released_at TIMESTAMP DEFAULT NULL,
    INDEX idx_plate (plate),
    INDEX idx_citizen (citizenid),
    INDEX idx_released (released_at)
);

-- Server-side: Impound a vehicle
RegisterNetEvent('tow:server:impoundVehicle', function(vehicleNet, reason)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local vehicle = NetworkGetEntityFromNetworkId(vehicleNet)
    if not DoesEntityExist(vehicle) then return end

    local plate = GetVehicleNumberPlateText(vehicle)
    local vehicleData = QBCore.Functions.GetVehicleProperties(vehicle)
    local fee = Config.ImpoundFees[reason] or 250

    MySQL.insert('INSERT INTO impounded_vehicles (plate, citizenid, vehicle_data, impound_reason, impound_fee, impounded_by) VALUES (?, ?, ?, ?, ?, ?)', {
        plate:trim(),
        GetVehicleOwner(plate),
        json.encode(vehicleData),
        reason,
        fee,
        Player.PlayerData.citizenid,
    })

    DeleteEntity(vehicle)
    local pay = math.random(Config.TowJob.payPerImpound - 100, Config.TowJob.payPerImpound + 100)
    Player.Functions.AddMoney('bank', pay, 'tow-impound')
    TriggerClientEvent('QBCore:Notify', src, 'Vehicle impounded. Earned $' .. pay, 'success')
end)

تطبيق تلقائي لإنفاذ مواقف السيارات

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

الإيرادات والتقدم

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

التعامل مع الحالات الخاصة والشبكات

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

جاهز للبدء؟

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