Volver al blog
Tutorial9 min de lectura

Menús de gestos y animaciones para FiveM: scripts listos para el roleplay

Añade gestos y animaciones variados a tu servidor FiveM. RPEmotes, Dpemotes, estilos de caminar y menús para una experiencia de roleplay inmersiva.

Agency Scripts

Fundador y desarrollador principal en Agency Scripts

Cómo funcionan los diccionarios de animaciones de GTA V

Las animaciones de GTA V están organizadas en diccionarios de animaciones, que son colecciones de clips relacionados. Cada animación se identifica mediante dos cadenas: el nombre del diccionario y el nombre de la animación dentro de ese diccionario. Por ejemplo, el diccionario amb@world_human_hang_out_street@female_hold_arm@idle_a contiene animaciones de personajes femeninos en posición de reposo de pie. Antes de reproducir cualquier animación, debes solicitar el diccionario con RequestAnimDict y esperar a que termine de cargarse usando HasAnimDictLoaded. No solicitar primero el diccionario es uno de los errores más comunes entre los desarrolladores principiantes, y hace que las animaciones no se reproduzcan sin mostrar ningún error. GTA V incluye miles de diccionarios de animaciones que abarcan desde posturas de combate hasta poses de yoga, y descubrir cuáles son útiles es la mitad del reto al crear un menú de emotes completo.

Reproducir y gestionar animaciones

La función principal para reproducir animaciones es TaskPlayAnim, que acepta el ped del jugador, el diccionario, el nombre de la animación, la velocidad de entrada gradual, la velocidad de salida gradual, la duración, las flags y la velocidad de reproducción. Las flags de animación controlan comportamientos importantes: la flag 1 hace que la animación se repita, la flag 2 detiene la animación en el último fotograma, la flag 16 reproduce solo la parte superior del cuerpo y permite que el jugador camine, y la flag 32 permite girar durante la animación. Combinar flags con OR bit a bit permite crear comportamientos detallados. Un emote de sentarse necesita la flag 1 para repetirse y la flag 2 para mantener el último fotograma si se interrumpe. Un emote de saludar funciona mejor con la flag 49, que combina solo la parte superior del cuerpo con repetición y rotación, lo que permite que el jugador salude mientras camina.

-- Core animation helper functions
local function LoadAnimDict(dict)
    if HasAnimDictLoaded(dict) then return true end
    RequestAnimDict(dict)
    local timeout = GetGameTimer() + 5000
    while not HasAnimDictLoaded(dict) do
        Wait(10)
        if GetGameTimer() > timeout then
            return false
        end
    end
    return true
end

local function PlayEmote(dict, anim, flags, duration)
    local ped = PlayerPedId()
    if IsPedInAnyVehicle(ped, false) then return end
    if not LoadAnimDict(dict) then return end

    flags = flags or 1      -- default: loop
    duration = duration or -1 -- -1 = indefinite

    TaskPlayAnim(ped, dict, anim, 2.0, 2.0, duration, flags, 0, false, false, false)
end

local function StopCurrentEmote()
    local ped = PlayerPedId()
    ClearPedTasks(ped)
    -- Also remove any attached props
    ClearPedSecondaryTask(ped)
end

-- Export for other resources
exports('PlayEmote', PlayEmote)
exports('StopEmote', StopCurrentEmote)

Organizar emotes en categorías

Un menú de emotes bien organizado agrupa las animaciones en categorías intuitivas que los jugadores pueden explorar rápidamente. Entre las categorías comunes se incluyen Bailes, Saludos, Reacciones, Poses, Deportes, Escenarios (como sentarse, apoyarse o tumbarse), Emotes con objetos (que incluyen objetos como cigarrillos o teléfonos), Estilos de caminata, y Expresiones faciales. Cada entrada de emote debe guardar el diccionario, el nombre de la animación, las flags, si usa un objeto, el hash del modelo del objeto si corresponde, el índice del hueso al que se adjunta el objeto, y los valores de desplazamiento y rotación para colocarlo correctamente. Guardar todo esto en una tabla de configuración estructurada facilita añadir nuevos emotes sin modificar la lógica de reproducción.

-- Emote configuration (config.lua)
Config = {}

Config.Emotes = {
    dances = {
        {label = 'Dance 1',    dict = 'anim@amb@nightclub@dancers@club_dance_bar_1',    anim = 'high_center',   flags = 1},
        {label = 'Dance 2',    dict = 'anim@amb@nightclub@mini@dance@dance_solo@female@var_a@',  anim = 'high_center',   flags = 1},
        {label = 'Dance 3',    dict = 'anim@amb@nightclub@dancers@crowddance_facedj@',  anim = 'hi_dance_facedj_09_v2_female^1', flags = 1},
        {label = 'Flamenco',   dict = 'special_ped@jessie@idle_latina',                 anim = 'idle_latina',   flags = 1},
    },
    greetings = {
        {label = 'Wave',       dict = 'friends@frj@ig_1',    anim = 'wave_A',         flags = 49, duration = 3000},
        {label = 'Salute',     dict = 'anim@mp_player_intuppersalute',  anim = 'idle_a',  flags = 49, duration = 3000},
        {label = 'Handshake',  dict = 'mp_ped_interaction',  anim = 'handshake_guy_a', flags = 49, duration = 4000},
        {label = 'Fist Bump',  dict = 'mp_ped_interaction',  anim = 'fist_bump',      flags = 49, duration = 3000},
    },
    props = {
        {label = 'Cigarette',  dict = 'amb@world_human_smoking@male@male_a@idle_a', anim = 'idle_c', flags = 49,
            prop = 'prop_cs_ciggy_01', bone = 28422, pos = {0.0, 0.0, 0.0}, rot = {0.0, 0.0, 0.0}},
        {label = 'Coffee',     dict = 'amb@world_human_drinking@coffee@male@idle_a', anim = 'idle_c', flags = 49,
            prop = 'p_amb_coffeecup_01', bone = 28422, pos = {0.0, 0.0, 0.0}, rot = {0.0, 0.0, 0.0}},
        {label = 'Clipboard',  dict = 'amb@world_human_clipboard@male@idle_a', anim = 'idle_c', flags = 49,
            prop = 'p_amb_clipboard_01', bone = 36029, pos = {0.16, 0.08, 0.03}, rot = {-100.0, 0.0, 0.0}},
    },
    scenarios = {
        {label = 'Sit Chair',  scenario = 'PROP_HUMAN_SEAT_CHAIR', isScenario = true},
        {label = 'Lean Wall',  scenario = 'WORLD_HUMAN_LEANING',   isScenario = true},
        {label = 'Guard',      scenario = 'WORLD_HUMAN_GUARD_STAND', isScenario = true},
        {label = 'Jog',        scenario = 'WORLD_HUMAN_JOG_STANDING', isScenario = true},
    },
}

Emotes con objetos y adjunción de objetos

Los emotes con objetos implican generar un objeto del juego y adjuntarlo a un hueso específico del ped del jugador. GTA V tiene un sistema de huesos esqueléticos en el que cada hueso tiene un índice único. Entre los huesos de adjunción más comunes se incluyen 57005 para la mano derecha, 18905 para la mano izquierda, 28422 para la zona del dedo derecho (ideal para cigarrillos), y 31086 para la cabeza. Después de generar el objeto con CreateObject, adjúntalo usando AttachEntityToEntity que requiere la entidad del objeto, la entidad del ped, el índice del hueso, los desplazamientos de posición, los desplazamientos de rotación y varias flags booleanas para el comportamiento de colisión y física. Encontrar los desplazamientos de posición y rotación adecuados requiere prueba y error, y puede que haya que hacer pequeños ajustes según el modelo del ped. Limpia siempre los objetos cuando se cancele el emote, llevando un registro del identificador de la entidad generada y eliminándola.

-- Prop emote system
local currentProp = nil

local function PlayPropEmote(emoteData)
    local ped = PlayerPedId()
    if IsPedInAnyVehicle(ped, false) then return end

    -- Clean up any existing prop
    if currentProp and DoesEntityExist(currentProp) then
        DeleteEntity(currentProp)
        currentProp = nil
    end

    if not LoadAnimDict(emoteData.dict) then return end

    -- Spawn and attach prop
    local model = GetHashKey(emoteData.prop)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(10) end

    local pos = GetEntityCoords(ped)
    currentProp = CreateObject(model, pos.x, pos.y, pos.z, true, true, false)
    SetModelAsNoLongerNeeded(model)

    local boneIndex = GetPedBoneIndex(ped, emoteData.bone)
    AttachEntityToEntity(
        currentProp, ped, boneIndex,
        emoteData.pos[1], emoteData.pos[2], emoteData.pos[3],
        emoteData.rot[1], emoteData.rot[2], emoteData.rot[3],
        true, true, false, true, 1, true
    )

    -- Play animation
    TaskPlayAnim(ped, emoteData.dict, emoteData.anim,
        2.0, 2.0, -1, emoteData.flags or 49, 0, false, false, false)
end

-- Cleanup on emote cancel
local function CleanupProps()
    if currentProp and DoesEntityExist(currentProp) then
        DeleteEntity(currentProp)
        currentProp = nil
    end
end

Estilos de caminata y clips de movimiento

Los estilos de caminata cambian la forma en que se mueve el personaje del jugador y se aplican usando SetPedMovementClipset. A diferencia de las animaciones normales, que reemplazan acciones, los clipsets de movimiento persisten en todos los estados de locomoción, incluidos caminar, correr y esprintar. GTA V incluye docenas de clipsets de movimiento integrados, como MOVE_M@DRUNK@MODERATEDRUNK para caminar tambaleándose como un borracho, MOVE_M@GANGSTER@NG para caminar con actitud de gánster, MOVE_M@CONFIDENT para caminar con confianza, y MOVE_F@HEELS@C para caminar con tacones altos. Al igual que los diccionarios de animaciones, los clipsets deben solicitarse y cargarse antes de aplicarlos. Ofrece una opción para restablecer el estilo y permitir que los jugadores vuelvan a la forma de caminar predeterminada. Guarda el estilo de caminar preferido del jugador en los datos de su personaje para que persista entre sesiones y se vuelva a aplicar automáticamente cuando inicie sesión.

-- Walking style system
local currentWalkStyle = nil

Config.WalkStyles = {
    {label = 'Default',     clipset = nil},
    {label = 'Drunk',       clipset = 'MOVE_M@DRUNK@MODERATEDRUNK'},
    {label = 'Gangster',    clipset = 'MOVE_M@GANGSTER@NG'},
    {label = 'Confident',   clipset = 'MOVE_M@CONFIDENT'},
    {label = 'Tough',       clipset = 'MOVE_M@TOUGH_GUY@'},
    {label = 'Femme',       clipset = 'MOVE_F@FEMME@'},
    {label = 'Heels',       clipset = 'MOVE_F@HEELS@C'},
    {label = 'Injured',     clipset = 'move_m@injured'},
    {label = 'Hobo',        clipset = 'MOVE_M@HOBO@A'},
    {label = 'Brave',       clipset = 'MOVE_M@BRAVE@A'},
}

local function SetWalkStyle(clipset)
    local ped = PlayerPedId()
    ResetPedMovementClipset(ped, 0.2)

    if not clipset then
        currentWalkStyle = nil
        return
    end

    RequestClipSet(clipset)
    local timeout = GetGameTimer() + 3000
    while not HasClipSetLoaded(clipset) do
        Wait(10)
        if GetGameTimer() > timeout then return end
    end

    SetPedMovementClipset(ped, clipset, 0.2)
    currentWalkStyle = clipset
end

exports('SetWalkStyle', SetWalkStyle)
exports('GetWalkStyle', function() return currentWalkStyle end)

Expresiones faciales

Las expresiones faciales añaden una sutil profundidad emocional a las interacciones de roleplay. GTA V permite usar animaciones faciales mediante SetFacialIdleAnimOverride; esta función cambia la expresión facial de reposo del ped. Entre las expresiones disponibles se incluyen mood_normal_1, mood_happy_1, mood_angry_1, mood_injured_1, mood_stressed_1, mood_smug_1, y mood_sulk_1 , entre otras. Estas expresiones se mantienen hasta que se borran con ClearFacialIdleAnimOverride. Combinar una expresión facial con una animación corporal y un estilo de caminar permite a los jugadores controlar por completo cómo se presenta su personaje. Un personaje apoyado contra una pared, con una expresión arrogante y un caminar seguro, cuenta una historia muy distinta a la de uno que cojea por una lesión y tiene el rostro tenso. Estos pequeños detalles elevan el roleplay, de interacciones básicas a una representación rica del personaje.

-- Facial expression system
local currentExpression = nil

Config.Expressions = {
    {label = 'Normal',    expression = 'mood_Normal_1'},
    {label = 'Happy',     expression = 'mood_Happy_1'},
    {label = 'Angry',     expression = 'mood_Angry_1'},
    {label = 'Injured',   expression = 'mood_Injured_1'},
    {label = 'Stressed',  expression = 'mood_Stressed_1'},
    {label = 'Smug',      expression = 'mood_smug_1'},
    {label = 'Sulking',   expression = 'mood_sulk_1'},
    {label = 'Sleeping',  expression = 'pose_injured_1'},
    {label = 'Scared',    expression = 'mood_scared_1'},
}

local function SetExpression(expression)
    local ped = PlayerPedId()
    ClearFacialIdleAnimOverride(ped)
    if expression then
        SetFacialIdleAnimOverride(ped, expression, nil)
        currentExpression = expression
    else
        currentExpression = nil
    end
end

exports('SetExpression', SetExpression)

-- Shared emotes: sync with nearby players
RegisterNetEvent('emote:syncShared', function(senderId, dict, anim)
    local senderPed = GetPlayerPed(GetPlayerFromServerId(senderId))
    if not DoesEntityExist(senderPed) then return end
    local myPed = PlayerPedId()
    local dist = #(GetEntityCoords(myPed) - GetEntityCoords(senderPed))
    if dist > 3.0 then return end
    if not LoadAnimDict(dict) then return end
    TaskPlayAnim(myPed, dict, anim, 2.0, 2.0, -1, 1, 0, false, false, false)
end)

Crear la interfaz del menú de emotes

El menú de emotes puede implementarse como un menú radial basado en NUI o como un menú dentro del juego con una biblioteca como ox_lib o qb-menu. Un menú radial resulta natural para los emotes, porque los jugadores pueden recorrer las categorías rápidamente y seleccionar uno sin tener que leer largas listas de texto. Asigna el menú a una tecla como F4 o a una tecla configurable, y ofrece una alternativa mediante un comando, como /emote [name] para acceder directamente. El menú debe mostrar el nombre del emote, una miniatura de vista previa opcional e indicar qué emotes usan objetos. Incluye un sistema de favoritos que permita a los jugadores fijar sus emotes más usados en un anillo de acceso rápido, para que no tengan que recorrer las categorías cada vez que quieran realizar una acción habitual. Guarda los favoritos en el almacenamiento KVP del jugador mediante SetResourceKvp para que se conserven al reiniciar el servidor sin tener que escribir en la base de datos. Un menú de emotes bien diseñado se convierte en una de las funciones más usadas de cualquier servidor de roleplay, así que dedicar tiempo a la interfaz mejora la satisfacción de los jugadores.

¿Listo para empezar?

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