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

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

إضافة وقوف سيارات واقعي إلى مدينتك في FiveM. وقوف الشوارع، المواقف المدفوعة، قواعد الحجز وأفضل سكربتات الوقوف لمنع ازدحام المركبات.

Agency Scripts

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

فهم بنية نظام المواقف

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

تحديد مناطق ومواقف الانتظار

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

Config.ParkingZones = {
    ['pillbox_lot'] = {
        label = 'Pillbox Medical Parking',
        type = 'metered',       -- 'free', 'metered', 'permit'
        rate = 50,              -- $ per hour (metered only)
        maxTime = 120,          -- minutes max parking
        blip = {sprite = 357, color = 3, scale = 0.6},
        spots = {
            {coords = vector4(310.5, -590.2, 43.3, 70.0), occupied = false},
            {coords = vector4(313.8, -591.6, 43.3, 70.0), occupied = false},
            {coords = vector4(317.1, -593.0, 43.3, 70.0), occupied = false},
            {coords = vector4(320.4, -594.4, 43.3, 70.0), occupied = false},
        },
        fineAmount = 250,       -- fine for overtime or illegal parking
    },
    ['legion_street'] = {
        label = 'Legion Square Parking',
        type = 'metered',
        rate = 75,
        maxTime = 60,
        blip = {sprite = 357, color = 3, scale = 0.6},
        spots = {
            {coords = vector4(215.2, -810.5, 30.7, 160.0), occupied = false},
            {coords = vector4(212.6, -810.5, 30.7, 160.0), occupied = false},
        },
        fineAmount = 350,
    },
}

ال vector4 التنسيق يخزن إحداثيات x و y و z مع زاوية التوجيه، مما يضمن ركن المركبات باتجاه صحيح. ال occupied يتم إدارة العلم أثناء التشغيل بواسطة الخادم ولا يتم حفظه بين عمليات إعادة التشغيل لأن المركبات يتم تتبعها بشكل منفصل من خلال قاعدة بيانات نظام الجراج. احتفظ بعدد الأماكن واقعيًا لكل موقع، لأن وجود عدد كبير جدًا من الأماكن في منطقة صغيرة يؤدي إلى مشاكل تقاطع المركبات.

نظام مؤقت عداد مواقف السيارات

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

local ActiveMeters = {} -- keyed by plate

RegisterNetEvent('parking:server:startMeter', function(plate, zoneId, duration)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local zone = Config.ParkingZones[zoneId]
    if not zone or zone.type ~= 'metered' then return end

    -- Calculate cost based on duration in minutes
    local hours = duration / 60
    local cost = math.ceil(hours * zone.rate)

    if Player.Functions.RemoveMoney('cash', cost, 'parking-meter') then
        ActiveMeters[plate] = {
            zone = zoneId,
            startTime = os.time(),
            paidUntil = os.time() + (duration * 60),
            plate = plate,
            owner = Player.PlayerData.citizenid,
        }
        TriggerClientEvent('parking:client:meterStarted', src, duration, cost)
    else
        TriggerClientEvent('QBCore:Notify', src, 'Not enough cash', 'error')
    end
end)

-- Check for expired meters every 60 seconds
CreateThread(function()
    while true do
        Wait(60000)
        local now = os.time()
        for plate, meter in pairs(ActiveMeters) do
            if now > meter.paidUntil then
                -- Meter expired, issue fine
                IssueParkingFine(plate, meter.zone, meter.owner)
                ActiveMeters[plate] = nil
            end
        end
    end
end)

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

كشف المركبات وتعيين المواقع

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

-- client/detection.lua
local nearestZone = nil
local currentSpot = nil

CreateThread(function()
    while true do
        local sleep = 1000
        local ped = PlayerPedId()
        local inVehicle = IsPedInAnyVehicle(ped, false)

        if inVehicle then
            local veh = GetVehiclePedIsIn(ped, false)
            local vehCoords = GetEntityCoords(veh)

            for zoneId, zone in pairs(Config.ParkingZones) do
                local firstSpot = zone.spots[1]
                local zoneDist = #(vehCoords - vector3(firstSpot.coords.x,
                    firstSpot.coords.y, firstSpot.coords.z))

                if zoneDist < 50.0 then
                    sleep = 200
                    nearestZone = zoneId

                    for i, spot in ipairs(zone.spots) do
                        local spotPos = vector3(spot.coords.x, spot.coords.y, spot.coords.z)
                        local dist = #(vehCoords - spotPos)
                        if dist < 3.0 then
                            local speed = GetEntitySpeed(veh)
                            if speed < 0.5 then
                                currentSpot = {zone = zoneId, index = i}
                                ShowParkingPrompt(zone)
                            end
                        end
                    end
                    break
                end
            end
        else
            nearestZone = nil
            currentSpot = nil
        end

        Wait(sleep)
    end
end)

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

نظام غرامات ومراقبة مواقف السيارات

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

function IssueParkingFine(plate, zoneId, citizenid)
    local zone = Config.ParkingZones[zoneId]
    local amount = zone and zone.fineAmount or 250

    MySQL.insert([[
        INSERT INTO parking_fines (plate, citizenid, amount, reason, zone, issued_at)
        VALUES (?, ?, ?, ?, ?, NOW())
    ]], {plate, citizenid, amount, 'Expired parking meter', zoneId})

    -- Notify owner if online
    local Player = QBCore.Functions.GetPlayerByCitizenId(citizenid)
    if Player then
        TriggerClientEvent('QBCore:Notify', Player.PlayerData.source,
            'Parking fine issued: $' .. amount, 'error')
    end
end

-- Police manual ticket command
RegisterNetEvent('parking:server:issueTicket', function(plate, reason)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    if Player.PlayerData.job.name ~= 'police' then return end

    local vehicle = MySQL.query.await(
        'SELECT citizenid FROM player_vehicles WHERE plate = ?', {plate}
    )

    if vehicle and vehicle[1] then
        IssueParkingFine(plate, 'manual', vehicle[1].citizenid)
        TriggerClientEvent('QBCore:Notify', src, 'Ticket issued for ' .. plate, 'success')
    end
end)

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

عرض عداد NUI وواجهة الدفع

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

دمج السحب والمركبات المهجورة

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

الاعتبارات المتعلقة بالأداء وقابلية التوسع

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

جاهز للبدء؟

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