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.