FiveM 纹身与理发系统:自定义脚本
为您的FiveM城市添加纹身店和理发店。摄像机角度、UI、预约流程以及适合角色扮演的顶级定制脚本。
Agency Scripts
Agency Scripts 创始人兼首席开发者
理解行人外观组件
在 FiveM 中构建纹身和理发店系统需要深入了解 GTA V 如何处理 ped(角色)外观。外观系统分为几个组件类别:头部覆盖层用于瑕疵、面部毛发、眉毛和化妆等特征,drawable 组件用于服装和发型,纹身装饰层叠在 ped 模型上。每种组件类型使用不同的 native 函数并遵循自己的索引系统。头部覆盖层使用索引 0-12 控制瑕疵(0)、面部毛发(1)、眉毛(2)、老化(3)、化妆(4)、腮红(5)和口红(8)等特征。发型通过 drawable 组件系统在索引 2 处设置头发。纹身使用完全独立的装饰系统,带有集合名称和哈希值。在构建 UI 之前,需要绘制每个组件的所有可用选项,包括当前 ped 模型可用的变体数量,因为男性和女性角色有不同的可用样式。使用 GetNumHeadOverlayValues(overlayIndex) 查询运行时每种覆盖类型存在多少选项。
角色预览摄像头系统
优秀的理发和纹身体验依赖玩家能实时看到所选内容。构建自定义摄像机系统,聚焦相关身体部位,玩家浏览选项时切换视角。编辑发型时,摄像机聚焦头部。选择躯干纹身时,拉远摄像机显示上半身。处理腿部纹身时,调整摄像机框架下半身。使用脚本摄像机原生函数,实现视角间平滑过渡。以下是一个管理多级缩放和身体聚焦区域的摄像机系统:
local customCam = nil
local camActive = false
local CameraPresets = {
head = {offset = vector3(0.0, 0.65, 0.65), fov = 35.0},
face = {offset = vector3(0.0, 0.45, 0.62), fov = 25.0},
torso = {offset = vector3(0.0, 1.2, 0.2), fov = 45.0},
legs = {offset = vector3(0.0, 1.5, -0.5), fov = 50.0},
full = {offset = vector3(0.0, 2.5, 0.2), fov = 50.0},
}
function SetupPreviewCamera(preset)
local ped = PlayerPedId()
local pedCoords = GetEntityCoords(ped)
local pedHeading = GetEntityHeading(ped)
local headingRad = math.rad(pedHeading)
local cam = CameraPresets[preset] or CameraPresets.full
local camX = pedCoords.x + cam.offset.y * math.sin(-headingRad)
local camY = pedCoords.y + cam.offset.y * math.cos(-headingRad)
local camZ = pedCoords.z + cam.offset.z
if customCam then
local newCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
SetCamCoord(newCam, camX, camY, camZ)
PointCamAtCoord(newCam, pedCoords.x, pedCoords.y, pedCoords.z + cam.offset.z)
SetCamFov(newCam, cam.fov)
SetCamActiveWithInterp(newCam, customCam, 800, 1, 1)
Wait(800)
DestroyCam(customCam, false)
customCam = newCam
else
customCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
SetCamCoord(customCam, camX, camY, camZ)
PointCamAtCoord(customCam, pedCoords.x, pedCoords.y, pedCoords.z + cam.offset.z)
SetCamFov(customCam, cam.fov)
SetCamActive(customCam, true)
RenderScriptCams(true, true, 500, true, false)
end
camActive = true
end
function DestroyPreviewCamera()
if customCam then
RenderScriptCams(false, true, 500, true, false)
DestroyCam(customCam, false)
customCam = nil
camActive = false
end
end
添加鼠标或模拟摇杆旋转支持,使玩家在预览更改时可旋转角色。当NUI未占用鼠标时捕获鼠标移动,并将其应用为玩家角色的朝向旋转。此功能让玩家在购买前从多个角度检查纹身,显著提升购物体验。自定义期间冻结玩家角色位置,并播放待机动画,使角色在浏览时保持自然状态。
发型和胡须定制
发型和胡须定制分别使用 drawable 组件和头部覆盖系统。发型通过 SetPedComponentVariation 使用组件索引 2(头发)和索引 1(纹理/颜色变化)。发色通过单独的方式应用 SetPedHairColor 接受主色索引和高亮色索引。胡须样式使用头部覆盖系统的索引 1,透明度值控制面部毛发的浓淡。构建 NUI 以显示每种可用样式的缩略图或名称,玩家点击选项时实时更新 ped。以下是应用发型和胡须变化并实时预览的 Lua 逻辑:
function ApplyHairStyle(ped, styleIndex, textureIndex, primaryColor, highlightColor)
SetPedComponentVariation(ped, 2, styleIndex, textureIndex or 0, 0)
SetPedHairColor(ped, primaryColor, highlightColor)
end
function ApplyBeardStyle(ped, styleIndex, opacity, color)
SetPedHeadOverlay(ped, 1, styleIndex, opacity or 1.0)
SetPedHeadOverlayColor(ped, 1, 1, color, color)
end
function ApplyEyebrows(ped, styleIndex, opacity, color)
SetPedHeadOverlay(ped, 2, styleIndex, opacity or 1.0)
SetPedHeadOverlayColor(ped, 2, 1, color, color)
end
function GetAvailableHairStyles(ped)
local styles = {}
local numStyles = GetNumberOfPedDrawableVariations(ped, 2)
for i = 0, numStyles - 1 do
local numTextures = GetNumberOfPedTextureVariations(ped, 2, i)
table.insert(styles, {
index = i,
textures = numTextures,
label = 'Style ' .. (i + 1)
})
end
return styles
end
function GetAvailableBeardStyles(ped)
local styles = {}
local numStyles = GetNumHeadOverlayValues(1)
for i = 0, numStyles - 1 do
table.insert(styles, {
index = i,
label = 'Beard ' .. (i + 1)
})
end
return styles
end
NUI界面应以视觉调色板呈现发色,而非数字索引,将GTA内部颜色索引映射到实际RGB值。包含胡须不透明度滑块,允许玩家微调面部毛发浓密度,从浅胡茬到浓密胡须。打开理发菜单前保存玩家之前的外观,点击取消时可恢复所有更改,防止误操作导致玩家损失金钱。
纹身覆盖系统
GTA V中的纹身使用装饰系统,通过集合和覆盖哈希对将纹理覆盖层叠加到NPC模型上。每个纹身由集合名称(如“mpairraces_overlays”)和覆盖名称(如“MP_Airraces_Tattoo_000_M”)定义,共同标识特定纹身资源。使用 AddPedDecorationFromHashes 并使用 ClearPedDecorations. 困难之处在于没有原生方法移除单个纹身,因此当玩家想移除一个纹身时,您需要清除所有装饰并重新应用除被移除纹身外的所有装饰。这意味着您必须在数据模型中维护玩家当前纹身的完整列表。以下是管理纹身集合和应用的方法:
-- Tattoo database structure
local TattooZones = {
head = {label = 'Head', camera = 'face'},
torso = {label = 'Torso', camera = 'torso'},
left_arm = {label = 'Left Arm', camera = 'torso'},
right_arm = {label = 'Right Arm', camera = 'torso'},
left_leg = {label = 'Left Leg', camera = 'legs'},
right_leg = {label = 'Right Leg', camera = 'legs'},
back = {label = 'Back', camera = 'torso'}
}
-- Available tattoos per zone (abbreviated, real list has hundreds)
Config.Tattoos = {
torso = {
{collection = 'mpairraces_overlays', overlay = 'MP_Airraces_Tattoo_000_M',
label = 'Wing Design', price = 500, zone = 'torso'},
{collection = 'mpbiker_overlays', overlay = 'MP_MP_Biker_Tat_000_M',
label = 'Flame Skull', price = 750, zone = 'torso'},
},
left_arm = {
{collection = 'mpchristmas2_overlays', overlay = 'MP_Xmas2_M_Tat_000',
label = 'Sleeve Art', price = 600, zone = 'left_arm'},
}
}
function ApplyAllTattoos(ped, tattooList)
ClearPedDecorations(ped)
for _, tattoo in ipairs(tattooList) do
local collectionHash = GetHashKey(tattoo.collection)
local overlayHash = GetHashKey(tattoo.overlay)
AddPedDecorationFromHashes(ped, collectionHash, overlayHash)
end
end
function PreviewTattoo(ped, tattoo, currentTattoos)
local previewList = {}
for _, t in ipairs(currentTattoos) do
table.insert(previewList, t)
end
table.insert(previewList, tattoo)
ApplyAllTattoos(ped, previewList)
end
function RemoveTattoo(ped, tattooIndex, currentTattoos)
local newList = {}
for i, t in ipairs(currentTattoos) do
if i ~= tattooIndex then
table.insert(newList, t)
end
end
ApplyAllTattoos(ped, newList)
return newList
end
GTA V的纹身目录包含数百条目,分布在多个DLC合集。请在配置中按身体区域组织,以便NUI按区域过滤。对于每个纹身,根据玩家的ped模型检查应使用男性还是女性变体,因为大多数纹身有分别以_M或_F结尾的资产名。玩家在目录中悬停纹身时显示预览,确认选择时才收费。
化妆和面部彩绘系统
化妆使用头部覆盖系统的多个索引:腮红(5)、口红(8)以及胸毛或肤色覆盖层以实现额外效果。每种化妆类型有多种样式选项和一个控制强度的不透明度滑块。在理发店 NUI 中构建化妆部分,展示每个类别及其可用颜色的视觉样本。化妆的颜色系统使用 SetPedHeadOverlayColor 带有颜色类型参数:类型0为默认颜色,类型1为发色(用于胡须和眉毛),类型2为化妆颜色。化妆专门使用类型2,提供包括各种肤色、红色、粉色和戏剧性色彩的化妆调色板。为每个化妆组件添加不透明度滑块,使玩家能实现细腻自然或大胆戏剧的妆容。将所有化妆设置与玩家外观数据一起存储,以便在会话间保持,并在玩家家中提供镜子交互,允许他们无需去商店即可调整化妆。
支付系统与定价
支付系统需要跟踪玩家会话期间的更改,并根据所提供的服务计算总费用。玩家打开商店菜单时,快照当前外观。更改时,跟踪每项修改,累积至会话购物车。确认并退出时,收取所有更改的总费用。取消时,恢复快照。实现分级定价,基础理发低于高级造型,纹身价格根据大小和复杂度变化。以下是会话跟踪和支付系统:
local sessionChanges = {}
local originalAppearance = nil
function StartCustomizationSession()
local ped = PlayerPedId()
originalAppearance = CaptureFullAppearance(ped)
sessionChanges = {}
end
function TrackChange(category, item, price)
-- Remove previous change in same category if exists
for i = #sessionChanges, 1, -1 do
if sessionChanges[i].category == category then
table.remove(sessionChanges, i)
end
end
table.insert(sessionChanges, {
category = category,
item = item,
price = price
})
UpdateCartUI()
end
function UpdateCartUI()
local total = 0
local items = {}
for _, change in ipairs(sessionChanges) do
total = total + change.price
table.insert(items, {
label = change.category .. ': ' .. change.item,
price = change.price
})
end
SendNUIMessage({
action = 'updateCart',
items = items,
total = total
})
end
function ConfirmAndPay()
local total = 0
for _, change in ipairs(sessionChanges) do
total = total + change.price
end
TriggerServerEvent('appearance:server:pay', total, sessionChanges)
end
function CancelSession()
if originalAppearance then
RestoreFullAppearance(PlayerPedId(), originalAppearance)
end
sessionChanges = {}
DestroyPreviewCamera()
end
在服务器端,验证玩家是否有足够资金后再提交外观更改。如果支付失败,通知客户端恢复更改。考虑提供套餐优惠,例如理发和修剪胡须一起比单项服务总价更便宜,激励玩家一次性使用全部服务。
服装保存与衣柜集成
扩展理发和纹身系统,增加服装保存功能,允许玩家将完整外观配置存储为命名预设。每个预设包含玩家外观的完整状态,包括发型与颜色、面部毛发、化妆及所有激活的纹身。玩家可在家中衣柜或回访商店时切换保存的造型。将服装存储在数据库中,关联玩家的公民ID,使用包含所有外观数据的 JSON 数据块。限制每位玩家保存的服装数量,或作为付费功能提供额外槽位。玩家家中的衣柜交互应呈现简洁的 NUI 面板,列出保存的服装并带有预览功能,临时应用每套造型供玩家查看后决定是否使用。此功能将个人外观系统与住房系统结合,使两者价值相辅相成。
商店位置与 NPC 理发师集成
定义地图上多个商店位置,每个位置可能提供不同的服务和价格倍数。南区的经济理发店可能提供低价基础理发,高端 Vinewood 沙龙则收取高价但提供独家发型和染色。使用目标系统或接近标记,在玩家靠近理发椅或纹身站时触发商店菜单。使用 TaskStartScenarioInPlace 带有理发店的坐姿场景,或让他们在纹身时站在特定位置。使用合适的ped模型在每个地点生成NPC理发师或纹身师,并分配闲置动画,使其看起来正在工作。当玩家与商店互动时,NPC可在玩家附近播放工作动画,模拟服务过程,增加视觉沉浸感。会话结束后,NPC返回闲置位置。在数据库中跟踪每个商店的收入,支持玩家拥有商店玩法,创业者可购买并管理自己的理发店或纹身店,设定价格并雇佣NPC员工甚至其他玩家作为员工。