Sistema de garajes para FiveM: guarda, saca y confisca vehículos
Diseña un sistema de garajes sólido para FiveM, con garajes personales y de trabajo, depósito, llaves, inventario y los mejores recursos gratis y de pago.
Agency Scripts
Fundador y desarrollador principal en Agency Scripts
Arquitectura de los sistemas de garaje
Un sistema de garaje es una de las funciones más esenciales de cualquier servidor de roleplay de FiveM, ya que es el medio principal para que los jugadores guarden, saquen y gestionen sus vehículos. En esencia, un sistema de garaje consta de tres capas conectadas: una capa de base de datos que conserva la propiedad y el estado de los vehículos, una capa de lógica del servidor que gestiona la aparición y eliminación de vehículos con validaciones, y una capa de interfaz del cliente que permite a los jugadores interactuar con sus vehículos guardados. Antes de escribir código, debes decidir aspectos clave de la arquitectura, como si los garajes estarán vinculados a ubicaciones o serán globales, si los jugadores podrán acceder a cualquier garaje o solo a garajes específicos, y cómo gestionarás propiedades de los vehículos como las modificaciones, el nivel de combustible y los daños. Los mejores sistemas de garaje guardan el objeto completo con las propiedades del vehículo para que, cuando un jugador saque su coche, esté exactamente como lo dejó, con la pintura personalizada, las mejoras de rendimiento e incluso el nivel de suciedad de la carrocería.
Esquema de base de datos y persistencia de vehículos
El esquema de la base de datos es la base de todo el sistema de garaje. Necesitas una tabla que registre la propiedad, el estado actual y las propiedades guardadas de cada vehículo. La columna de estado es fundamental porque determina si un vehículo está en el mundo, guardado en un garaje o en el depósito. Aquí tienes un esquema práctico que incluye los campos esenciales:
CREATE TABLE IF NOT EXISTS player_vehicles (
id INT AUTO_INCREMENT PRIMARY KEY,
citizenid VARCHAR(50) NOT NULL,
vehicle VARCHAR(50) NOT NULL,
hash VARCHAR(50) NOT NULL,
mods LONGTEXT DEFAULT '{}',
plate VARCHAR(8) NOT NULL,
fakeplate VARCHAR(8) DEFAULT NULL,
garage VARCHAR(50) DEFAULT 'pillboxgarage',
fuel INT DEFAULT 100,
engine FLOAT DEFAULT 1000.0,
body FLOAT DEFAULT 1000.0,
state INT DEFAULT 1, -- 0 = out, 1 = garaged, 2 = impounded
depotprice INT DEFAULT 0,
drivingdistance INT DEFAULT 0,
INDEX idx_citizenid (citizenid),
INDEX idx_plate (plate),
INDEX idx_state (state)
);
El mods La columna almacena un objeto codificado en JSON con todas las modificaciones del vehículo que devuelven funciones como QBCore.Functions.GetVehicleProperties(vehicle) o el equivalente en ESX. Indexar la citizenid, plate, y state las columnas garantizan que las consultas sigan siendo rápidas incluso cuando tu base de jugadores crezca hasta alcanzar los miles. Usa siempre consultas parametrizadas al interactuar con esta tabla para evitar ataques de inyección SQL.
Lógica de aparición y eliminación del lado del servidor
El lado del servidor es donde se realizan todas las validaciones críticas. Cuando un jugador solicita sacar un vehículo del garaje, el servidor debe verificar que realmente sea el propietario del vehículo, que el vehículo esté actualmente en estado de guardado y que haya un punto de aparición válido disponible. Nunca permitas que el cliente indique directamente la posición de aparición, porque los tramposos podrían hacer aparecer vehículos en cualquier lugar del mapa. En su lugar, define los puntos de aparición en el servidor y selecciona el más cercano que esté disponible. Aquí tienes un ejemplo de un controlador seguro para sacar vehículos del lado del servidor:
RegisterNetEvent('garage:server:takeVehicle', function(vehicleId, garageId)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local citizenid = Player.PlayerData.citizenid
local result = MySQL.query.await(
'SELECT * FROM player_vehicles WHERE id = ? AND citizenid = ? AND state = 1',
{vehicleId, citizenid}
)
if not result or not result[1] then
TriggerClientEvent('QBCore:Notify', src, 'Vehicle not found', 'error')
return
end
local vehData = result[1]
local spawnPoint = GetAvailableSpawnPoint(garageId)
if not spawnPoint then
TriggerClientEvent('QBCore:Notify', src, 'No parking spots available', 'error')
return
end
MySQL.update('UPDATE player_vehicles SET state = 0 WHERE id = ?', {vehicleId})
TriggerClientEvent('garage:client:spawnVehicle', src, vehData, spawnPoint)
end)
Para el proceso de eliminación, el servidor debe capturar las propiedades actuales del vehículo antes de quitarlo del mundo. Así se garantiza que se guarden las modificaciones realizadas desde el último almacenamiento. Actualiza siempre el combustible, la salud del motor y la salud de la carrocería junto con el JSON de modificaciones para que todo persista correctamente. Implementa una comprobación de distancia del lado del servidor para asegurarte de que el jugador esté realmente cerca de un garaje antes de permitir operaciones de almacenamiento.
Interfaz del garaje del lado del cliente con NUI
La interfaz del garaje es donde los jugadores interactúan con el sistema, y una interfaz bien diseñada marca la diferencia entre una experiencia frustrante y una fluida. Usa NUI con HTML, CSS y JavaScript para crear un panel adaptable que muestre todos los vehículos guardados en el garaje actual. Cada entrada de vehículo debe mostrar de un vistazo el nombre del vehículo, la matrícula, el nivel de combustible y su estado general. Incluye un sistema de vista previa que haga aparecer temporalmente el modelo del vehículo para que los jugadores puedan ver qué están seleccionando, algo especialmente útil cuando un jugador tiene varios vehículos del mismo tipo. Aquí tienes la lógica del lado del cliente para abrir el menú del garaje y recopilar los datos de los vehículos:
RegisterNetEvent('garage:client:openMenu', function(garageId)
QBCore.Functions.TriggerCallback('garage:server:getVehicles', function(vehicles)
if not vehicles or #vehicles == 0 then
QBCore.Functions.Notify('No vehicles stored here', 'info')
return
end
SetNuiFocus(true, true)
SendNUIMessage({
action = 'openGarage',
vehicles = vehicles,
garageName = Config.Garages[garageId].label
})
end, garageId)
end)
RegisterNUICallback('takeVehicle', function(data, cb)
SetNuiFocus(false, false)
TriggerServerEvent('garage:server:takeVehicle', data.vehicleId, currentGarage)
cb('ok')
end)
Del lado de JavaScript, muestra cada vehículo como una tarjeta con botones para sacarlo o transferirlo a otro garaje. Considera añadir opciones de ordenación y filtrado para que los jugadores con colecciones grandes encuentren rápidamente el vehículo que necesitan. Una barra de búsqueda que filtre por matrícula o nombre del vehículo es una pequeña mejora que facilita mucho el uso en servidores donde los jugadores acumulan muchos vehículos con el tiempo.
Almacenamiento y restauración de las propiedades de los vehículos
Guardar y restaurar correctamente las propiedades de los vehículos es una de las partes más complicadas del desarrollo de un sistema de garajes. El objeto de propiedades contiene decenas de campos, incluidos colores, diseños, luces de neón, tintado de ventanas, color del humo de los neumáticos, extras y todas las modificaciones de rendimiento. Al guardar un vehículo, captura las propiedades justo antes de eliminar la entidad para asegurarte de obtener su estado más reciente. Al volver a hacer aparecer un vehículo, debes esperar a que la entidad se cargue por completo antes de aplicar las propiedades; de lo contrario, algunas modificaciones, como las ruedas personalizadas o las mejoras del motor, fallarán sin avisar. Usa una pequeña pausa o un bucle adecuado que compruebe si la entidad existe:
function SpawnAndApplyMods(vehData, spawnPoint)
local model = GetHashKey(vehData.vehicle)
RequestModel(model)
while not HasModelLoaded(model) do
Wait(10)
end
local veh = CreateVehicle(model, spawnPoint.x, spawnPoint.y, spawnPoint.z,
spawnPoint.w, true, false)
while not DoesEntityExist(veh) do
Wait(10)
end
local props = json.decode(vehData.mods)
if props then
QBCore.Functions.SetVehicleProperties(veh, props)
end
SetVehicleFuelLevel(veh, vehData.fuel + 0.0)
SetVehicleEngineHealth(veh, vehData.engine + 0.0)
SetVehicleBodyHealth(veh, vehData.body + 0.0)
SetEntityAsMissionEntity(veh, true, true)
SetModelAsNoLongerNeeded(model)
TaskWarpPedIntoVehicle(PlayerPedId(), veh, -1)
end
Presta especial atención a los vehículos addon, porque a veces tienen extras personalizados o índices de diseños que se comportan de manera distinta a los vehículos originales de GTA. Prueba a fondo el ciclo de guardado y restauración de propiedades con distintos tipos de vehículos para detectar casos extremos cuanto antes.
Integración del sistema de depósito municipal
Un sistema de depósito municipal complementa el garaje y añade el nivel de realismo que exigen los servidores de roleplay. Los vehículos pueden acabar en el depósito por varios motivos: incautación policial durante una detención, limpieza automática de vehículos abandonados tras reiniciar el servidor o una acción administrativa por infracciones de las reglas. Cuando un vehículo va al depósito, actualiza su estado a 2 en la base de datos y, si quieres, establece una tarifa que el jugador deberá pagar para recuperarlo. El depósito debe funcionar de forma similar a un garaje, pero con el requisito adicional de pagar antes de retirar el vehículo. Crea una ubicación independiente para el depósito en el mapa, con sus propios puntos de aparición y una interfaz NUI que muestre claramente la tarifa.
RegisterNetEvent('police:server:impoundVehicle', function(plate, price)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
-- Verify the player has police job authorization
if Player.PlayerData.job.name ~= 'police' then return end
local result = MySQL.update.await(
'UPDATE player_vehicles SET state = 2, depotprice = ? WHERE plate = ?',
{price or 500, plate}
)
if result > 0 then
TriggerClientEvent('QBCore:Notify', src, 'Vehicle impounded', 'success')
end
end)
Considera implementar un sistema de tarifas escalonadas en el que el costo del depósito aumente cada vez que se envíe el mismo vehículo, para disuadir a los jugadores de tratar el depósito como estacionamiento gratuito. También puedes añadir una mecánica basada en el tiempo, donde los vehículos que lleven más de una cantidad configurable de días reales en el depósito vuelvan automáticamente al garaje sin costo, evitando situaciones de pérdida permanente que frustran a los jugadores.
Blips de garajes e integración con sistemas de interacción
Para que los garajes sean fáciles de encontrar y usar, debes colocar correctamente los blips y las zonas de interacción. Añade blips en el mapa para cada ubicación de garaje, así los jugadores podrán encontrarlos en el minimapa, y utiliza marcadores de proximidad o la integración con un sistema de interacción para activar la interacción. Los sistemas de interacción como ox_target o qb-target ofrecen una experiencia más clara, porque solo muestran las opciones cuando el jugador apunta a un punto específico, lo que reduce el desorden en pantalla. Define las ubicaciones de los garajes en un archivo de configuración compartido al que puedan acceder tanto el cliente como el servidor, para mantener sincronizadas las coordenadas, los puntos de aparición y los ajustes:
Config.Garages = {
['pillboxgarage'] = {
label = 'Pillbox Garage',
coords = vector3(215.83, -810.18, 30.73),
spawnPoints = {
vector4(218.32, -803.28, 30.73, 248.5),
vector4(222.41, -799.84, 30.73, 248.5),
vector4(226.52, -796.41, 30.73, 248.5),
},
blip = { sprite = 357, color = 3, scale = 0.7 },
vehicleType = 'car', -- car, boat, aircraft
},
}
Admite varios tipos de vehículos creando garajes separados para barcos y aeronaves, con ubicaciones de aparición adecuadas cerca del agua o en aeropuertos. El vehicleType filtro garantiza que los jugadores solo vean vehículos terrestres en un garaje urbano y barcos en una marina, lo que evita confusiones y problemas de aparición. Cuando un jugador se acerque a un garaje, comprueba si tiene vehículos guardados allí antes de mostrar el mensaje de interacción, para evitar aperturas innecesarias del menú si no tiene vehículos en esa ubicación.
Consejos para optimizar el rendimiento
Los sistemas de garajes pueden convertirse en un cuello de botella de rendimiento si no se implementan con cuidado, especialmente en servidores con cientos de jugadores conectados a la vez, cada uno con varios vehículos. Guarda en caché las listas de vehículos en el servidor en lugar de consultar la base de datos cada vez que un jugador abre el menú del garaje, e invalida la caché solo cuando cambie el estado de un vehículo. Del lado del cliente, evita mantener abiertos los marcos NUI cuando no se necesiten, porque incluso los marcos NUI ocultos consumen recursos si ejecutan temporizadores de JavaScript o animaciones. Al hacer aparecer vehículos, asegúrate de limpiar correctamente las entidades marcándolas como ya no necesarias cuando el jugador las guarde, e implementa una rutina de limpieza de respaldo que se ejecute periódicamente para detectar entidades de vehículos huérfanas que no se hayan eliminado correctamente por bloqueos o desconexiones. Usa funciones nativas como GetGamePool('CVehicle') con moderación y guarda los resultados en caché cuando necesites comprobar si ya hay vehículos de jugadores en el mundo. Por último, considera establecer un límite máximo de vehículos por garaje para mantener acotadas las consultas a la base de datos y evitar que un solo jugador guarde cientos de vehículos que podrían ralentizar las operaciones de recuperación.