返回博客
Tutorial4 分钟阅读

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允许动画期间旋转。通过按位或组合标志可以创建细致的行为。坐姿表情需要标志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)

将表情动作分类管理

一个组织良好的表情菜单,将动画分组到直观的类别中,玩家可以快速浏览。常见类别包括 舞蹈动作, 问候, 反应, 姿势, Sports, 场景 (如坐下、倚靠或躺下), 道具表情 (涉及香烟或电话等物品), 步行动作,和 面部表情. 每个表情条目应存储字典、动画名称、标志、是否使用道具、道具模型哈希(如适用)、道具附着的骨骼索引,以及用于正确放置道具的偏移和旋转值。将所有这些存储在结构化配置表中,使添加新表情无需修改播放逻辑。

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

道具表情与物体附加

道具表情涉及生成游戏对象并将其附加到玩家 ped 的特定骨骼。GTA V 具有骨骼系统,每个骨骼有唯一索引。常见附加骨骼包括 57005 用于右手, 18905 用于左手, 28422 用于右手指区域(适合香烟),以及 31086 用于头部。在使用后生成道具 CreateObject,使用 AttachEntityToEntity 需要 prop 实体、ped 实体、骨骼索引、位置偏移、旋转偏移及多个布尔标志以控制碰撞和物理行为。正确设置位置和旋转偏移需要反复试验,不同 ped 模型可能需微调。取消动作时务必通过跟踪生成的实体句柄并删除它来清理道具。

-- 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 获取支持、更新以及新功能预告。