Volver al blog
Tutorial9 min de lectura

Sistema de estacionamiento para FiveM: scripts realistas para aparcar vehículos

Añade estacionamiento realista a tu ciudad de FiveM: aparcamiento en la calle, parkings de pago, normas de depósito y scripts contra el desorden.

Agency Scripts

Fundador y desarrollador principal en Agency Scripts

Comprender la arquitectura de un sistema de estacionamiento

Un sistema de estacionamiento añade profundidad y realismo a cualquier servidor de roleplay de FiveM al ofrecer a los jugadores zonas designadas donde dejar sus vehículos, con consecuencias si incumplen las normas. En esencia, un sistema de estacionamiento registra qué plazas están ocupadas, controla cuánto tiempo llevan estacionados los vehículos y gestiona los pagos mediante parquímetros o permisos. El sistema consta de lógica del servidor para registrar el estado y procesar los pagos, detección del cliente para controlar la entrada y salida de las zonas de estacionamiento y una interfaz NUI opcional para mostrar el estado del parquímetro y las opciones de pago. Un sistema de estacionamiento bien implementado también se integra con tus scripts de policía y grúa para que los jugadores autorizados puedan multar o incautar los vehículos estacionados ilegalmente.

Definir zonas y plazas de estacionamiento

Las zonas de estacionamiento se definen como áreas poligonales o rectangulares en el mapa, cada una con plazas individuales que tienen coordenadas y orientaciones específicas. Usa un archivo de configuración para definir estas zonas, de modo que los administradores del servidor puedan añadir nuevas áreas de estacionamiento sin modificar el código. Cada zona puede tener reglas distintas, como estacionamiento gratuito, de pago por parquímetro o exclusivo para vehículos con permiso. Aquí tienes una estructura de configuración práctica para las zonas:

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

El vector4 El formato almacena las coordenadas x, y, z junto con el ángulo de orientación, para que los vehículos queden estacionados en la dirección correcta. El indicador occupied se gestiona en tiempo de ejecución por el servidor y no se guarda entre reinicios, ya que los vehículos se registran por separado en la base de datos del sistema de garajes. Mantén un número de plazas realista para cada ubicación, porque demasiadas plazas en un área pequeña pueden hacer que los vehículos se solapen.

Sistema de temporizador del parquímetro

El sistema del parquímetro registra cuánto tiempo lleva estacionado cada vehículo y cobra en consecuencia. Cuando un jugador estaciona en una zona de pago, interactúa con un parquímetro para iniciar su sesión, selecciona una duración y paga por adelantado. El servidor guarda la hora de inicio de la sesión, la duración pagada, la matrícula y el ID de la zona. Un hilo del temporizador del servidor comprueba periódicamente todas las sesiones activas del parquímetro y marca las vencidas para que se apliquen las normas. Aquí tienes la lógica principal del parquímetro:

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)

Una resolución del temporizador de 60 segundos es suficiente para aplicar las normas de estacionamiento sin generar carga de CPU innecesaria. Guarda las sesiones del parquímetro en memoria en lugar de en la base de datos, ya que son temporales y no necesitan mantenerse tras los reinicios del servidor. Cuando se reinicie el servidor, todos los parquímetros se reiniciarán, lo cual es aceptable porque, en la mayoría de los frameworks, los vehículos también vuelven a los garajes al reiniciar.

Detección de vehículos y asignación de plazas

Detectar cuándo un vehículo entra o sale de una plaza de estacionamiento requiere comprobaciones periódicas de proximidad en el cliente. Comprueba la distancia respecto a las plazas de las zonas cercanas en lugar de revisar todas las plazas del mapa. Ejecuta el bucle de detección solo cuando el jugador esté en un vehículo y cerca de una zona de estacionamiento configurada. Así se mantiene un uso mínimo de CPU durante el juego normal:

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

La comprobación de velocidad de 0.5 garantiza que el aviso de estacionamiento solo aparezca cuando el vehículo esté casi detenido y evita que la interfaz parpadee mientras el jugador conduce por un estacionamiento. El intervalo de espera adaptable, de 200ms cerca de las zonas frente a 1000ms en otras áreas, equilibra la capacidad de respuesta y el rendimiento. En servidores con muchas zonas de estacionamiento, considera usar un hash espacial o un quadtree para evitar recorrer todas las zonas en cada tick.

Sistema de multas y control del estacionamiento

Cuando vence el parquímetro o un vehículo está estacionado ilegalmente, el sistema emite una multa asociada al propietario del vehículo. Las multas se guardan en una tabla de la base de datos y se muestran cuando el propietario vuelve a iniciar sesión o retira su vehículo. Los jugadores policías también pueden emitir multas de estacionamiento manualmente mediante un comando o una interacción de target. El registro de la multa incluye la matrícula, el importe, el motivo y la fecha y hora:

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)

Integra el sistema de multas con el proceso de retirada de vehículos para que los jugadores tengan que pagar las multas pendientes antes de sacar un vehículo del garaje. Así se crea un ciclo natural de control en el que incumplir las normas de estacionamiento tiene consecuencias reales en el juego. Considera añadir un período de gracia de cinco a diez minutos tras el vencimiento del parquímetro antes de emitir la multa. Esto refleja el control del estacionamiento en el mundo real y reduce la frustración de los jugadores por multas emitidas con una precisión excesiva.

Interfaz NUI de visualización y pago del parquímetro

La NUI del parquímetro debe ser sencilla e informativa, y mostrar el nombre de la zona actual, la tarifa por hora, el tiempo restante si ya se ha pagado y botones para seleccionar la duración y realizar el pago. Muestra una cuenta regresiva que se actualice cada segundo cuando haya un parquímetro activo, para que el jugador sepa exactamente cuánto tiempo le queda. Usa un diseño compacto que no obstruya la vista del juego, similar a la pantalla de un parquímetro real. Anima la pantalla del temporizador con una transición de color de verde a amarillo y luego a rojo a medida que se acaba el tiempo, proporcionando una señal visual intuitiva sin que el jugador tenga que leer los minutos exactos que quedan. Mantén la NUI ligera con JavaScript puro y CSS mínimo, y evita frameworks pesados que aumenten el tiempo de carga de una interfaz sencilla.

Integración con grúas y vehículos abandonados

Conecta tu sistema de estacionamiento con una mecánica de grúa para que los vehículos con el parquímetro vencido o estacionados ilegalmente puedan trasladarse al depósito de incautación. Cuando un vehículo lleve más tiempo en infracción que el umbral configurado, márcalo como remolcable en el estado del servidor. Los jugadores que trabajen con grúas podrán ver estos vehículos resaltados en el mapa y recibir un pago por remolcarlos. El proceso de remolque debe actualizar el estado del vehículo en la base de datos de "out" a "impounded" y registrar como motivo de incautación una infracción de estacionamiento. Para los vehículos abandonados que lleven mucho tiempo en una plaza sin interacción de ningún jugador, implementa una rutina de limpieza automática que los traslade al depósito tras un tiempo de espera configurable, liberando plazas para los jugadores activos. Así se evita que los estacionamientos se llenen de vehículos de jugadores que no han iniciado sesión durante días o semanas.

Consideraciones de rendimiento y escalabilidad

Los sistemas de estacionamiento ejecutan ciclos continuos de detección que pueden afectar al rendimiento si no se optimizan con cuidado. La detección de vehículos del lado del cliente solo debería activarse para las zonas que estén dentro de la distancia de renderizado, y el hilo de comprobación de parquímetros del lado del servidor debería procesar por lotes todas las sesiones activas, en lugar de crear temporizadores individuales para cada vehículo. Crea índices en las columnas plate y citizenid de la tabla de multas de estacionamiento de la base de datos para garantizar búsquedas rápidas cuando los jugadores recuperen vehículos o los administradores consulten el historial de multas. En servidores con decenas de zonas de estacionamiento y cientos de vehículos simultáneos, considera almacenar en caché los datos de las zonas en una tabla de Lua, en lugar de leerlos de la configuración en cada comprobación, y usa la función nativa GetClosestVehicle con moderación, ya que analiza todos los vehículos cercanos. Analiza el rendimiento de tu recurso de estacionamiento con el profiler integrado de FiveM durante los picos de carga para detectar posibles cuellos de botella, y recuerda que un sistema de estacionamiento debería usar menos de 0.1ms del tiempo de tick del servidor para no afectar al rendimiento general del servidor.

El script adecuado

Listo para tu servidor, con actualizaciones incluidas.

¿Listo para empezar?

Consigue los scripts en nuestra tienda, o únete al Discord para soporte, novedades y un adelanto de lo que viene.