FiveM: кастомные анимации и синхронизация пропов
Поставил кастомную анимацию курения или обыска карманов — у тебя в игре всё гладко, а на стриме модератора персонаж дёргается, зависает в воздухе или проп в руке вообще не появляется. Кастомные анимации и пропы — одна из тех тем на FiveM, где локальный тест обманчив: всё, что происходит только на твоём клиенте, может выглядеть идеально, а расползётся именно в синхронизации между игроками. Разберём, как правильно паковать и грузить .ycd, чем обычный TaskPlayAnim отличается от синхронизированной сцены на несколько педов, как прикреплять проп в руку так, чтобы его видели все, при чём тут animpostfx, и откуда берётся большинство рассинхронов.
Содержание
- Локальная анимация vs синхронизация: что видно только тебе, а что — всем
- Упаковка кастомных анимаций: .ycd, fxmanifest и стриминг
- Синхронизированные сцены для нескольких игроков
- Пропы в руках: создание, attach и сетевая регистрация объекта
- animpostfx: экранные эффекты поверх анимационных сцен
- Частые причины рассинхрона и как их ловить
Локальная анимация vs синхронизация: что видно только тебе, а что — всем
Первое, что стоит держать в голове: TaskPlayAnim, вызванный на одном клиенте, по умолчанию воспроизводится локально и реплицируется остальным через обычную сетевую синхронизацию педа (позиция, поворот, скелет передаются периодически, а не кадр в кадр). Для одиночных анимаций вроде эмоута, курения или сидения на стуле этого достаточно — рассинхронизация в доли секунды игроку не критична.
Проблема начинается там, где в сцене участвуют два и больше педа одновременно и важна точная синхронность — задержание с наручниками, обыск, передача предмета из рук в руки, любая RP-сцена «два актёра касаются друг друга». Здесь обычного TaskPlayAnim на каждом клиенте не хватает: даже если оба запустят анимацию «в одну и ту же секунду», сетевая задержка и разное время загрузки словаря дадут заметный разъезд поз, и будет видно, что персонажи не совпадают руками в кадре.
Для таких сцен в GTA V/FiveM есть отдельный механизм — синхронизированные сцены (NetworkStartSynchronisedScene), которые движок специально держит согласованными между участниками сцены на уровне сетевого протокола, а не просто как отдельные анимации на педах.
Упаковка кастомных анимаций: .ycd, fxmanifest и стриминг
Если сервер ещё не поднят вообще — базовая структура resources/, server.cfg и первый запуск разобраны в статье про установку FiveM-сервера с нуля, здесь предполагается, что это уже сделано. Кастомная анимация — это файл .ycd (Animation Clip Dictionary), который кладётся в ресурс как обычный стрим-ассет, рядом с моделями и картами. Структура ресурса:
resources/[anims]/custom_anims/
├── fxmanifest.lua
└── stream/
└── custom@smoking.ycd
В fxmanifest.lua файл достаточно перечислить в files {} — специальной data_file-директивы для анимационных словарей, в отличие от MLO-карт, не требуется:
fx_version 'cerulean'
game 'gta5'
files {
'stream/custom@smoking.ycd'
}
На клиенте анимационный словарь запрашивается стандартным паттерном — запросить, подождать загрузки, только потом проигрывать:
local dict = 'custom@smoking'
RequestAnimDict(dict)
while not HasAnimDictLoaded(dict) do
Wait(0)
end
TaskPlayAnim(PlayerPedId(), dict, 'smoke_loop', 8.0, -8.0, -1, 49, 0, false, false, false)
Флаг 49 в примере — битовая маска (looping + upper body в зависимости от анимации), значения зависят от конкретного клипа — их стоит подбирать под клип, а не копировать бездумно из чужого скрипта. Имя словаря custom@smoking выбрано для наглядности — реальное имя зависит от того, как автор назвал .ycd при экспорте, и не всегда совпадает с именем файла: иногда его приходится доставать из самого клипа инструментами для работы с ресурсами GTA V, если автор не указал имя в описании.
Частая ошибка новичков — вызывать TaskPlayAnim сразу после RequestAnimDict, без ожидания HasAnimDictLoaded. На быстром SSD это может «прокатывать» локально, но у игрока с медленным диском или дальней нодой анимация либо не запустится, либо запустится с рывком — клиент пытается проиграть клип из словаря, которого физически ещё нет в памяти. Общий подход к установке и структуре кастомных ресурсов подробнее разобран в статье про установку кастомных скриптов на FiveM.
Поднять сервер FiveM (GTA V) за пару минут
Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.
Создать серверСинхронизированные сцены для нескольких игроков
Когда в сцене участвует больше одного педа и важна точная согласованность движений (обыск, надевание наручников, драка по сценарию), используется NetworkStartSynchronisedScene — сцена создаётся один раз на инициирующем клиенте и передаёт остальным участникам общий сетевой идентификатор сцены, к которому они присоединяются:
local scenePos = GetEntityCoords(targetPed)
local sceneId = NetworkStartSynchronisedScene(
scenePos.x, scenePos.y, scenePos.z,
0.0, 0.0, GetEntityHeading(targetPed),
true, false, 1.0, 0.0, 0.0, 0
)
NetworkAddPedToSynchronisedScene(PlayerPedId(), sceneId, dict, 'search_anim', 1.5, -4.0, 1, 0, 1000.0, 0)
NetworkAddPedToSynchronisedScene(targetPed, sceneId, dict, 'searched_anim', 1.5, -4.0, 1, 0, 1000.0, 0)
NetworkStartSynchronisedScene(sceneId)
Важный нюанс — sceneId нужно донести до второго клиента, если сцену запускает не он сам: обычно через TriggerServerEvent → TriggerClientEvent, передавая сетевые ID участников (NetworkGetNetworkIdFromEntity), а не локальные хендлы педов — на другом клиенте локальный хендл будет другим числом. Именно здесь чаще всего теряют синхронность: если участник присоединяется к сцене на пару тиков позже (задержка сети или ещё не догрузился словарь), сцена стартует не одновременно, и видно рассинхрон в первые доли секунды.
Пропы в руках: создание, attach и сетевая регистрация объекта
Проп в руке — почти всегда отдельная сущность (object), которую нужно создать, прикрепить к кости скелета и, если проп должен быть виден другим игрокам, явно сделать сетевой. Ключевая ошибка, из-за которой проп виден только тебе — создание объекта без флага networked:
local propHash = GetHashKey('prop_cs_cigar_01')
RequestModel(propHash)
while not HasModelLoaded(propHash) do
Wait(0)
end
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
local prop = CreateObject(propHash, coords.x, coords.y, coords.z, true, true, false)
local boneIndex = GetPedBoneIndex(ped, 28422) -- правая кисть
AttachEntityToEntity(prop, ped, boneIndex,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
true, true, false, true, 1, true)
SetModelAsNoLongerNeeded(propHash)
Третий и четвёртый аргументы CreateObject (isNetwork, netMissionEntity) определяют, станет ли объект сетевым и получит ли он владельца-миссию. Если оставить isNetwork = false, объект физически существует только на создавшем его клиенте, и остальные игроки его не увидят — без ошибки в консоли, поэтому баг часто списывают на «глюк синхронизации», хотя проп изначально не был сетевым.
Проверить регистрацию просто: на другом клиенте должен появиться валидный NetworkGetNetworkIdFromEntity(prop) — если сущность не сетевая, у остальных клиентов её вообще нет в локальном списке entity. Модели пропов, как и MLO-карты, тоже стримятся ресурсом — если параллельно ставишь кастомные интерьеры, пригодится статья про установку кастомных MLO-карт, там тот же принцип стриминга через files {}.
Удаление тоже важно делать через сетевого владельца: DeleteEntity(prop), вызванный не-владельцем, может не сработать мгновенно на всех клиентах — движок сначала должен передать право на удаление, и разница в пару кадров будет заметна (проп «висит» в воздухе ещё немного после того, как анимация закончилась).
animpostfx: экранные эффекты поверх анимационных сцен
animpostfx — система постобработки экрана (AnimpostfxPlay / AnimpostfxStop), которую Rockstar использует для катсцен: размытие, затемнение краёв кадра, цветокоррекция во время диалогов или заскриптованных действий. В RP-скриптах её часто вешают поверх кастомных анимационных сцен для кинематографичности — например, fade при вскрытии сейфа или затемнение периферии во время допроса:
AnimpostfxPlay('MinigameEndNeutral', 0, false)
-- ...сцена идёт...
AnimpostfxStop('MinigameEndNeutral')
Граблина именно для темы синхронизации: имена эффектов animpostfx — готовый список, зашитый в саму игру, а не то, что можно добавить своим ресурсом. Своего постпроцессинг-эффекта не завести — только вызывать существующие по имени, и если имя с опечаткой или несуществующее, native молча не сработает без ошибки в консоли.
Второй момент — AnimpostfxPlay клиентский и не реплицируется автоматически. Если нужно, чтобы эффект увидели оба участника сцены, native нужно вызывать на каждом клиенте отдельно, обычно через TriggerClientEvent от сервера. Полагаться на «эффект сам разошлётся вместе со сценой» нельзя — не разойдётся, и второй игрок увидит сцену без эффекта, что и есть типичный источник жалоб «у меня по-другому выглядело».
Частые причины рассинхрона и как их ловить
Если коротко — почти весь рассинхрон анимаций и пропов на FiveM сводится к нескольким повторяющимся причинам:
- Проп создан без
isNetwork = true. Самая частая причина «у меня проп есть, у остальных нет» — см. раздел выше. TaskPlayAnimвызван до полной загрузки словаря на одном из клиентов — у игрока с более медленным стримингом (слабый диск, дальняя нода, забитый канал) анимация запускается позже или обрывается.- OneSync и дистанция культинга сущностей. По умолчанию сервер не реплицирует далёкие сущности всем подряд — если игрок вне зоны видимости в момент старта сцены, а потом быстро телепортируется или подъезжает, проп/анимация может «доехать» с задержкой, потому что клиенту сначала нужно получить саму сущность.
- Гонка по времени присоединения к сцене. Второй участник может присоединиться на кадр-два позже инициатора — решается либо явной задержкой перед стартом (короткий countdown, отправленный всем участникам заранее), либо проверкой
HasAnimDictLoadedна всех участниках до фактического старта. - Владелец сетевой сущности сменился в процессе сцены. FiveM может передавать владение между клиентами (например, если инициатор вышел из зоны видимости) — если логика жёстко завязана на неизменного владельца,
DeleteEntity/FreezeEntityPositionот не-владельца могут отработать с задержкой или не отработать вовсе. - Забытая уборка ресурсов. Пропы, оставшиеся висеть после обрыва сцены (краш скрипта, ранний выход игрока), нужно чистить явно по событию отключения или таймауту — иначе они копятся как мусорные networked-сущности и создают лишнюю нагрузку на синхронизацию у всех, кто рядом.
Диагностика начинается с того же приёма, что и с обычными конфликтами ресурсов: воспроизвести баг на минимальном сетапе (два клиента, без сторонних скриптов рядом) и логировать момент старта анимации/сцены на каждом клиенте отдельно, чтобы увидеть реальный разброс по времени. Метод бинарного деления server.cfg для поиска виновника среди множества ресурсов подробно описан в статье про конфликт ресурсов и скриптов на FiveM — он же работает и для поиска ресурса, ломающего синхронизацию анимаций. Если баги всплывают только при большом онлайне — вероятнее всего дело в культинге и дистанции стриминга, а не в логике самой анимации.
Поднять сервер FiveM (GTA V) за пару минут
Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.
Создать серверЧастые вопросы
Можно ли синхронизировать анимацию без NetworkStartSynchronisedScene, просто вызвав TaskPlayAnim на всех клиентах через событие?
Для одиночной анимации одного педа — да, так обычно и делают (эмоуты, действия в одиночку). Для сцены с двумя и более педами лучше синхронизированная сцена — простой TaskPlayAnim на каждом клиенте даст видимый разъезд поз.
Проп исчезает у части игроков через несколько секунд после появления — почему?
Чаще всего культинг по дистанции в OneSync либо забытый SetModelAsNoLongerNeeded/утечка ссылок на модель. Стоит также проверить, не удаляет ли её случайно другой скрипт по совпадающему хешу модели.
Как понять, что именно .ycd не грузится, а не ошибка в самом TaskPlayAnim?
Проверь HasAnimDictLoaded в цикле с логом и таймаутом — если словарь не грузится, проблема в стриминге ресурса (не попал в files {}, опечатка в имени) или в конфликте с одноимённым ресурсом.
Нужно ли обязательно использовать animpostfx, если сцена уже синхронизирована через NetworkStartSynchronisedScene?
Нет, это чисто визуальное дополнение, а не часть механизма синхронизации самой анимации. Можно обойтись без него — сцена от этого не сломается.
Можно ли прикрепить кастомный (не ванильный) 3D-проп в руку так же, как ванильный?
Да, если проп упакован как обычный стрим-ассет (модель + .ytd текстуры) — логика CreateObject/AttachEntityToEntity та же, разница только в том, что модель грузится из своего ресурса, а не из базовых архивов игры.