Por qué la creación de personajes importa en rol
La creación de personajes es el primer momento interactivo que vive un jugador en tu servidor de rol de FiveM. Marca el tono de todo lo que viene después. Un creador pulido e inmersivo dice a los jugadores nuevos que tu servidor está construido con mimo y que vale la pena dedicarle tiempo. Uno básico o lleno de bugs les devuelve al navegador de servidores buscando alternativas. Más allá de la primera impresión, el creador define la identidad visual que cada jugador lleva durante toda su experiencia de rol. Determina mezclas de herencia, rasgos faciales, peinados, ropa y datos biográficos como nombre y fecha de nacimiento. Acertar aquí significa construir un sistema que hable con el sistema nativo de apariencia de GTA V, presente las opciones mediante una interfaz NUI intuitiva, persista las selecciones en base de datos y las recargue de forma fiable cada vez que el jugador spawnee.
Entender el sistema de apariencia de peds de GTA V
GTA V utiliza un sistema de apariencia basado en herencia donde la cara de un personaje se mezcla a partir de dos modelos padre. Hay 46 posibles madres y padres, cada uno con una estructura facial distinta. La native SetPedHeadBlendData controla esta mezcla aceptando valores de shape y skin mix entre 0.0 y 1.0 que determinan cuánto se parece el personaje a cada progenitor. Más allá de la herencia, el juego ofrece 20 categorías de overlay mediante SetPedHeadOverlay que cubren funciones como manchas, vello facial, cejas, envejecimiento, maquillaje y daño en la piel. Cada overlay tiene su propio valor de opacidad y se puede colorear de forma independiente usando SetPedHeadOverlayColor. El pelo se gestiona aparte con SetPedComponentVariation para el modelo y SetPedHairColor para color y mechas. El color de ojos usa SetPedEyeColor con valores de 0 a 31. Entender estas natives es esencial porque tu interfaz NUI necesita mapear sliders y selectores amigables a estas llamadas concretas.
-- client/appearance.lua
function ApplyAppearance(ped, data)
-- Mezcla de herencia (madre, padre, shape mix, skin mix)
SetPedHeadBlendData(ped,
data.mother, data.father, 0,
data.mother, data.father, 0,
data.shapeMix, data.skinMix, 0.0,
false
)
-- Rasgos faciales (-1.0 a 1.0 para cada uno de los 20 rasgos)
for i = 0, 19 do
SetPedFaceFeature(ped, i, data.features[i] or 0.0)
end
-- Overlays (manchas, barba, cejas, envejecimiento, maquillaje, etc.)
for i = 0, 12 do
local overlay = data.overlays[i] or {style = 0, opacity = 0.0}
SetPedHeadOverlay(ped, i, overlay.style, overlay.opacity)
if overlay.color then
SetPedHeadOverlayColor(ped, i, overlay.colorType or 1,
overlay.color, overlay.secondColor or overlay.color)
end
end
-- Pelo
SetPedComponentVariation(ped, 2, data.hair or 0, 0, 2)
SetPedHairColor(ped, data.hairColor or 0, data.hairHighlight or 0)
-- Color de ojos
SetPedEyeColor(ped, data.eyeColor or 0)
end
Construir la interfaz NUI
La NUI (Natural User Interface) es el overlay de HTML, CSS y JavaScript con el que el jugador interactúa mientras crea su personaje. Un creador bien diseñado divide el proceso en pasos lógicos: datos biográficos primero, luego selección de herencia, rasgos faciales, overlays como vello facial y maquillaje, peinado y, por último, ropa. Cada paso debería mostrar una previsualización en tiempo real sobre el modelo del personaje para que los jugadores vean exactamente cómo queda su elección. Usa un diseño con pestañas o tipo wizard para que los jugadores no se vean abrumados con decenas de opciones a la vez. Los sliders de rango funcionan bien para valores continuos como shape mix y rasgos faciales, mientras que los selectores tipo grid con miniaturas funcionan mejor para opciones discretas como peinados y prendas. Envía cada cambio al script de cliente vía fetch o el sistema de mensajes NUI para que el ped se actualice al instante.
<!-- nui/index.html (estructura simplificada) -->
<div id="creator" class="creator-wrapper">
<div class="steps-nav">
<button data-step="bio" class="active">Identity</button>
<button data-step="heritage">Heritage</button>
<button data-step="features">Features</button>
<button data-step="overlays">Appearance</button>
<button data-step="hair">Hair</button>
<button data-step="clothing">Clothing</button>
</div>
<div class="step-content" id="step-heritage">
<label>Mother</label>
<input type="range" id="mother" min="0" max="45" value="0">
<label>Father</label>
<input type="range" id="father" min="0" max="45" value="0">
<label>Resemblance (Shape Mix)</label>
<input type="range" id="shapeMix" min="0" max="100" value="50">
<label>Skin Tone (Skin Mix)</label>
<input type="range" id="skinMix" min="0" max="100" value="50">
</div>
</div>
<script>
document.querySelectorAll('input[type="range"]').forEach(slider => {
slider.addEventListener('input', () => {
fetch('https://myresource/updateAppearance', {
method: 'POST',
body: JSON.stringify({
type: slider.id,
value: parseFloat(slider.value)
})
});
});
});
</script>
Sistema de cámara para la previsualización
Un sistema de cámara específico es clave para una experiencia de creación pulida. Los jugadores necesitan ver primeros planos del rostro al ajustar herencia y overlays, planos medios para pelo y parte superior de la ropa y planos de cuerpo entero para pantalones y calzado. Implementa un controlador de cámara que interpole suavemente entre posiciones predefinidas según el paso activo. Usa CreateCam con el tipo DEFAULT_SCRIPTED_CAMERA y RenderScriptCams para tomar el control desde la cámara del juego. Para la rotación, deja al jugador arrastrar el ratón para orbitar alrededor del personaje ajustando el heading de la cámara mientras se mantiene apuntando al ped. Una animación idle sutil en el ped, como el escenario WORLD_HUMAN_STAND_IMPATIENT, mantiene al personaje con aspecto natural durante la creación en vez de quedarse en una rígida postura en T.
-- client/camera.lua
local creatorCam = nil
local camAngle = 0.0
local CamPositions = {
bio = {offset = vector3(0.0, 0.9, 0.65), fov = 35.0},
heritage = {offset = vector3(0.0, 0.7, 0.65), fov = 30.0},
features = {offset = vector3(0.0, 0.5, 0.68), fov = 22.0},
overlays = {offset = vector3(0.0, 0.5, 0.68), fov = 22.0},
hair = {offset = vector3(0.0, 0.6, 0.72), fov = 25.0},
clothing = {offset = vector3(0.0, 2.2, 0.30), fov = 45.0},
}
function SetupCreatorCam(ped, step)
local pos = CamPositions[step] or CamPositions.bio
local pedCoords = GetEntityCoords(ped)
local target = pedCoords + vector3(0.0, 0.0, pos.offset.z)
if not creatorCam then
creatorCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
RenderScriptCams(true, true, 500, true, false)
end
local rad = math.rad(camAngle)
local camPos = pedCoords + vector3(
math.sin(rad) * pos.offset.y,
math.cos(rad) * pos.offset.y,
pos.offset.z
)
SetCamCoord(creatorCam, camPos.x, camPos.y, camPos.z)
PointCamAtCoord(creatorCam, target.x, target.y, target.z)
SetCamFov(creatorCam, pos.fov)
SetCamActive(creatorCam, true)
end
Guardado y carga de los datos de personaje
Todos los datos de apariencia del personaje deben persistir en una base de datos para sobrevivir a reinicios y poder cargarse cada vez que el jugador spawnee. Estructura tu tabla para guardar la apariencia completa como un blob JSON junto con campos biográficos como nombre, apellido, fecha de nacimiento, género y nacionalidad. Cuando el jugador termina la creación y confirma su personaje, el cliente envía el objeto completo al servidor, que valida los rangos y lo inserta en la base de datos. En logins posteriores, el servidor recupera los datos durante el flujo de spawn y los reenvía al cliente, que llama a la misma función ApplyAppearance usada en la creación. Para servidores multipersonaje, guarda un identificador de slot para que cada jugador tenga varios personajes con apariencias independientes.
-- server/database.lua
-- Usando oxmysql para operaciones de base de datos
function SaveCharacter(playerId, charData)
local identifier = GetPlayerIdentifier(playerId, 0)
local appearance = json.encode(charData.appearance)
MySQL.insert([[
INSERT INTO characters
(identifier, slot, firstname, lastname,
dateofbirth, gender, nationality, appearance)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
]], {
identifier,
charData.slot or 1,
charData.firstname,
charData.lastname,
charData.dob,
charData.gender,
charData.nationality,
appearance
})
end
function LoadCharacter(playerId, slot)
local identifier = GetPlayerIdentifier(playerId, 0)
local result = MySQL.single.await([[
SELECT * FROM characters
WHERE identifier = ? AND slot = ?
]], {identifier, slot})
if result then
result.appearance = json.decode(result.appearance)
end
return result
end
-- Handler de spawn
RegisterNetEvent('character:loaded', function(slot)
local src = source
local char = LoadCharacter(src, slot)
if char then
TriggerClientEvent('character:applyAppearance', src, char.appearance)
end
end)
Integración de ropa y variaciones de componentes
La ropa en GTA V se gestiona mediante variaciones de componentes y de props. Los componentes cubren partes del cuerpo como torso, piernas, pies, accesorios y camisetas interiores, mientras que los props manejan ítems de unión como sombreros, gafas, pendientes y relojes. La native SetPedComponentVariation toma un ID de componente, ID de drawable, ID de textura y ID de paleta. El reto es que los drawables disponibles difieren entre modelos de ped masculinos y femeninos y algunas combinaciones de drawable y textura no son válidas. Tu NUI necesita consultar el número máximo de drawables para cada componente con GetNumberOfPedDrawableVariations y el máximo de texturas para cada drawable con GetNumberOfPedTextureVariations. Construye tu selector de ropa para poblar dinámicamente las opciones según el género elegido para que los jugadores solo vean combinaciones válidas. Guarda los datos de ropa junto con los de apariencia y aplícalos en el flujo de spawn con las mismas natives de variación de componentes.
Validación y medidas antiabuso
Nunca confíes en datos que vengan del cliente sin validarlos. Jugadores con clientes modificados pueden enviar valores de apariencia fuera de los rangos normales, lo que puede crashear a otros clientes o aprovechar glitches visuales. Valida cada campo en el servidor antes de guardarlo. Los índices de herencia deben estar entre 0 y 45, los valores de mix entre 0.0 y 1.0, los de rasgos faciales entre -1.0 y 1.0 y los índices de overlay dentro del rango válido de cada tipo. Rechaza o limita los valores fuera de rango. Además, aplica rate limiting a los eventos de actualización de apariencia durante la creación para evitar que clientes maliciosos inunden el servidor con cambios en ráfaga. Un cooldown de 100 milisegundos entre actualizaciones es imperceptible para los jugadores legítimos, pero frena el spam automatizado. Por último, protege el flujo de creación detrás de una gestión de sesión adecuada para que los jugadores no puedan disparar eventos de creación fuera del flujo previsto.
