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

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

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

Agency Scripts

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

هيكل نظام وظيفة التاكسي

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

تنفيذ عداد التاكسي

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

local meterActive = false
local currentFare = 0.0
local lastPosition = nil
local baseFare = 50
local ratePerMile = 15
local waitRatePerSecond = 0.5
local totalDistance = 0.0

function StartMeter()
    meterActive = true
    currentFare = baseFare
    totalDistance = 0.0
    lastPosition = GetEntityCoords(PlayerPedId())

    SendNUIMessage({action = 'showMeter', fare = currentFare, distance = 0.0})

    CreateThread(function()
        while meterActive do
            Wait(500)
            local currentPos = GetEntityCoords(PlayerPedId())
            local speed = GetEntitySpeed(PlayerPedId())

            if speed > 1.0 then
                local dist = #(currentPos - lastPosition)
                totalDistance = totalDistance + dist
                local miles = totalDistance / 1609.34
                currentFare = baseFare + (miles * ratePerMile)
            else
                currentFare = currentFare + (waitRatePerSecond * 0.5)
            end

            lastPosition = currentPos

            SendNUIMessage({
                action = 'updateMeter',
                fare = math.floor(currentFare),
                distance = math.floor(totalDistance),
                speed = math.floor(speed * 2.236936)
            })
        end
    end)
end

function StopMeter()
    meterActive = false
    local finalFare = math.floor(currentFare)
    SendNUIMessage({action = 'stopMeter', fare = finalFare})
    return finalFare
end

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

نظام مهام التقاط NPC

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

local MissionLocations = {
    {coords = vector3(-240.49, -987.78, 29.29), label = 'Pillbox Medical Center'},
    {coords = vector3(228.28, -790.70, 30.58), label = 'Legion Square'},
    {coords = vector3(-1044.57, -2739.76, 13.85), label = 'Los Santos Airport'},
    {coords = vector3(126.19, -1282.72, 29.27), label = 'Strawberry Avenue'},
    {coords = vector3(-556.18, -196.17, 38.22), label = 'Rockford Hills'},
    {coords = vector3(1159.56, -314.45, 69.21), label = 'Vinewood Hills'},
    {coords = vector3(-1222.08, -906.82, 12.32), label = 'Del Perro Pier'},
    {coords = vector3(373.58, 325.71, 103.57), label = 'Downtown Vinewood'},
}

RegisterNetEvent('taxi:server:requestNPCMission', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player or Player.PlayerData.job.name ~= 'taxi' then return end

    local pickup = MissionLocations[math.random(#MissionLocations)]
    local dropoff = MissionLocations[math.random(#MissionLocations)]

    while pickup.label == dropoff.label do
        dropoff = MissionLocations[math.random(#MissionLocations)]
    end

    local estimatedDistance = #(pickup.coords - dropoff.coords)
    local basePay = 50
    local distancePay = math.floor(estimatedDistance / 1609.34 * 15)
    local totalPay = basePay + distancePay

    TriggerClientEvent('taxi:client:startNPCMission', src, {
        pickup = pickup,
        dropoff = dropoff,
        estimatedPay = totalPay,
        npcModel = Config.NPCModels[math.random(#Config.NPCModels)],
        missionId = 'TAXI-' .. os.time() .. '-' .. math.random(1000, 9999)
    })
end)

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

حساب الأجرة وتسعير الذروة

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

function CalculateFare(pickupCoords, dropoffCoords, vehicleClass)
    local distance = #(pickupCoords - dropoffCoords)
    local miles = distance / 1609.34

    local rates = Config.FareRates
    local baseFare = rates.base
    local perMile = rates.perMile

    -- Vehicle class multiplier
    local classMultiplier = {
        [0] = 0.8,   -- Compacts (budget)
        [1] = 1.0,   -- Sedans (standard)
        [2] = 1.0,   -- SUVs
        [3] = 1.2,   -- Coupes (comfort)
        [18] = 1.5,  -- Super (luxury)
    }
    local vehMultiplier = classMultiplier[vehicleClass] or 1.0

    -- Time-of-day multiplier (night premium)
    local hour = GetClockHours()
    local timeMultiplier = 1.0
    if hour >= 22 or hour <= 5 then
        timeMultiplier = 1.3  -- 30% night surcharge
    end

    -- Surge pricing based on demand
    local surgeMultiplier = GetCurrentSurgeMultiplier()

    local totalFare = (baseFare + (miles * perMile)) * vehMultiplier * timeMultiplier * surgeMultiplier

    return {
        total = math.floor(totalFare),
        breakdown = {
            base = baseFare,
            distance = math.floor(miles * perMile),
            vehicleClass = vehMultiplier,
            nightSurcharge = timeMultiplier > 1.0,
            surgeActive = surgeMultiplier > 1.0,
            surgeRate = surgeMultiplier
        }
    }
end

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

عرض المسار والملاحة بنظام GPS

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

local routeBlip = nil
local checkpointBlips = {}

function SetTaxiRoute(destination, checkpoints)
    ClearTaxiRoute()

    routeBlip = AddBlipForCoord(destination.x, destination.y, destination.z)
    SetBlipSprite(routeBlip, 198)
    SetBlipColour(routeBlip, 5)
    SetBlipScale(routeBlip, 1.0)
    SetBlipRoute(routeBlip, true)
    SetBlipRouteColour(routeBlip, 5)
    BeginTextCommandSetBlipName('STRING')
    AddTextComponentSubstringPlayerName('Destination')
    EndTextCommandSetBlipName(routeBlip)

    if checkpoints then
        for i, cp in ipairs(checkpoints) do
            local cpBlip = AddBlipForCoord(cp.x, cp.y, cp.z)
            SetBlipSprite(cpBlip, 1)
            SetBlipColour(cpBlip, 3)
            SetBlipScale(cpBlip, 0.6)
            table.insert(checkpointBlips, cpBlip)
        end
    end
end

function ClearTaxiRoute()
    if routeBlip then
        SetBlipRoute(routeBlip, false)
        RemoveBlip(routeBlip)
        routeBlip = nil
    end
    for _, blip in ipairs(checkpointBlips) do
        RemoveBlip(blip)
    end
    checkpointBlips = {}
end

function UpdateETADisplay(destination)
    CreateThread(function()
        while routeBlip do
            Wait(2000)
            local pos = GetEntityCoords(PlayerPedId())
            local remaining = #(pos - destination)
            local speed = GetEntitySpeed(PlayerPedId())

            local etaSeconds = speed > 2.0 and (remaining / speed) or 0
            local etaMinutes = math.floor(etaSeconds / 60)

            SendNUIMessage({
                action = 'updateETA',
                distance = math.floor(remaining),
                eta = etaMinutes > 0 and (etaMinutes .. ' min') or 'Arriving'
            })
        end
    end)
end

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

نظام الإكراميات وتقييمات السائق

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

لوحة الإرسال وطلبات الركوب

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

نظام إدارة المركبات والأسطول

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

جاهز للبدء؟

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