Назад к блогу
Tutorial8 мин чтения

Меню эмоций и анимаций FiveM: скрипты, готовые для RP

Добавьте богатые эмоции и анимации на ваш сервер FiveM. RPEmotes, Dpemotes, кастомные стили ходьбы и лучшие меню эмоций для погружения в ролевую игру.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Понимание словарей анимаций GTA V

Анимации в GTA V организованы в словари анимаций, которые представляют собой коллекции связанных анимационных клипов. Каждая анимация идентифицируется двумя строками: названием словаря и названием анимации внутри этого словаря. Например, словарь amb@world_human_hang_out_street@female_hold_arm@idle_a содержит анимации бездействия для женских персонажей. Перед воспроизведением любой анимации необходимо запросить словарь с помощью RequestAnimDict и дождитесь, пока он полностью загрузится с помощью HasAnimDictLoaded. Отсутствие запроса словаря анимаций , одна из самых распространенных ошибок новичков, из-за которой анимации не воспроизводятся. GTA V поставляется с тысячами словарей анимаций, охватывающих все от боевых стоек до поз для йоги, и поиск полезных , половина задачи создания полноценного меню эмоций.

Воспроизведение и управление анимациями

Основная функция для воспроизведения анимаций , TaskPlayAnim", принимает ped игрока, словарь, имя анимации, скорость плавного входа, скорость плавного выхода, длительность, флаги и скорость воспроизведения. Флаги анимации контролируют критическое поведение: флаг 1 зацикливает анимацию, флаг 2 останавливает анимацию на последнем кадре, флаг 16 воспроизводит только верхнюю часть тела, позволяя игроку ходить, а флаг 32 разрешает вращение во время анимации. Комбинирование флагов с помощью побитового OR позволяет создавать тонкие поведения. Эмодзи сидения требует флаг 1 для зацикливания и флаг 2 для удержания последнего кадра при прерывании. Эмодзи помахивания лучше всего работает с флагом 49, который сочетает верхнюю часть тела, зацикливание и вращение, позволяя игроку махать, ходя по окружению.

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

Организация эмоций по категориям

Хорошо организованное меню эмоций группирует анимации по интуитивным категориям, которые игроки могут быстро просматривать. Общие категории включают Танцы, Приветствия, Реакции, Позы, Спорт, Сценарии (например, сидение, опирание или лежание), Анимации с объектами (включая объекты вроде сигарет или телефонов), Стили ходьбы , и Мимика лица. Каждая запись эмоции должна содержать словарь, имя анимации, флаги, использование реквизита, хэш модели реквизита (если применимо), индекс кости для крепления реквизита, а также значения смещения и вращения для правильного размещения реквизита. Хранение всего этого в структурированной таблице конфигурации облегчает добавление новых эмоций без изменения логики воспроизведения.

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

Анимации с объектами и прикрепление объектов

Анимации с объектами включают создание игрового объекта и прикрепление его к определенной кости педа игрока. В GTA V есть скелетная система костей, каждая кость имеет уникальный индекс. Распространённые кости для прикрепления включают 57005 для правой руки, 18905 для левой руки, 28422 для области правого пальца (идеально для сигарет), и 31086 для головы. После спавна объекта с CreateObject прикрепите это с помощью AttachEntityToEntity который требует сущность пропа, сущность педа, индекс кости, смещения позиции, смещения вращения и несколько булевых флагов для поведения столкновений и физики. Правильная настройка смещений позиции и вращения требует проб и ошибок, и разные модели педов могут нуждаться в небольших корректировках. Всегда очищайте пропы при отмене эмоции, отслеживая созданный дескриптор сущности и удаляя его.

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

Стили ходьбы и клипы движения

Стили ходьбы изменяют способ передвижения персонажа и применяются с помощью SetPedMovementClipset. В отличие от обычных анимаций, которые заменяют действия, наборы движений сохраняются во всех состояниях локомоции, включая ходьбу, бег и спринт. В GTA V встроено множество наборов движений, таких как MOVE_M@DRUNK@MODERATEDRUNK для пьяной неуклюжести, MOVE_M@GANGSTER@NG для гангстерской походки, MOVE_M@CONFIDENT для уверенной походки, и MOVE_F@HEELS@C для ходьбы на высоких каблуках. Как и словари анимаций, clipsets должны запрашиваться и загружаться перед применением. Предоставьте опцию сброса, чтобы игроки могли вернуться к стандартной ходьбе. Сохраняйте предпочитаемый стиль ходьбы игрока в данных персонажа, чтобы он сохранялся между сессиями и автоматически применялся при входе.

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

Мимика лица

Мимика лица добавляет тонкую эмоциональную глубину ролевым взаимодействиям. GTA V поддерживает анимации лица через SetFacialIdleAnimOverride", который изменяет выражение лица ped в состоянии покоя. Доступные выражения включают mood_normal_1, mood_happy_1, mood_angry_1, mood_injured_1, mood_stressed_1, mood_smug_1 , и mood_sulk_1 среди прочего. Эти выражения сохраняются до очистки с помощью ClearFacialIdleAnimOverride. Сочетание выражения лица с анимацией тела и стилем ходьбы дает игрокам полный контроль над тем, как их персонаж себя проявляет. Персонаж, прислонившийся к стене с самодовольным выражением и уверенной походкой, рассказывает совершенно другую историю, чем тот, кто хромает с травмой и напряженным лицом. Эти мелкие детали поднимают ролевую игру с базовых взаимодействий до богатого воплощения персонажа.

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

Создание интерфейса меню эмоций

Меню эмоций можно реализовать либо как радиальное меню на базе NUI, либо как внутриигровое меню с использованием библиотеки вроде ox_lib или qb-menu. Радиальное меню естественно для эмоций, так как игроки могут быстро переходить между категориями и выбирать эмоцию без чтения длинных списков текста. Привяжите меню к клавише, например F4 или настраиваемая клавиша, и предоставьте команду-резерв, например /emote [name] для прямого доступа. Меню должно показывать название эмоции, необязательный превью-миниатюру и указывать, какие эмоции используют реквизит. Включите систему избранного, которая позволяет игрокам закреплять наиболее используемые эмоции в кольце быстрого доступа, избавляя их от необходимости каждый раз просматривать категории для выполнения обычного действия. Сохраняйте избранное в KVP-хранилище игрока с помощью SetResourceKvp чтобы они сохранялись при перезапусках сервера без необходимости записи в базу данных. Хорошо сделанное меню эмоций становится одной из самых используемых функций на любом ролевом сервере, поэтому вложение времени в интерфейс окупается удовлетворенностью игроков.

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.