FiveM NPC对话系统:脚本分支对话
赋予您的 FiveM NPC 真实个性。对话树、语音台词、任务钩子及最佳脚本,适合深度剧情驱动的角色扮演服务器。
Agency Scripts
Agency Scripts 创始人兼首席开发者
为什么 NPC 对话很重要
NPC对话系统将静态行人转变为赋予服务器个性的活生角色,引导玩家完成活动。没有对话,NPC不过是打开菜单的交互目标。拥有合适的对话系统后,他们成为任务发布者、讲故事者、态度鲜明的商店老板,以及根据玩家声望或职业不同做出反应的线人。对话系统为服务器特定的背景故事、分支剧情和影响未来互动的玩家选择创造机会。想象一下,当玩家接近机械师NPC时,NPC会称呼玩家名字,评论其车辆状况,并根据玩家是回头客还是首次访问者提供不同服务,这种沉浸感会让玩家更投入服务器,激励他们探索标准刷刷循环之外的内容。
对话树数据结构
任何对话系统的基础是定义对话流程的数据结构。对话树由节点组成,每个节点包含NPC文本和一组玩家响应选项,链接到其他节点。该树结构支持分支对话、基于玩家状态的条件路径和返回早期对话点的循环。设计对话节点为数据驱动,使服务器工作人员无需接触Lua代码即可创建新对话。以下是实用的对话树格式:
Config.Dialogues = {
['mechanic_greeting'] = {
npcName = 'Tony the Mechanic',
nodes = {
['start'] = {
text = "Hey there! Car giving you trouble, or are you just here for a tune-up?",
animation = 'WORLD_HUMAN_WELDING',
options = {
{
label = "I need repairs",
next = 'repairs',
condition = function(player)
return IsPlayerInVehicle(player)
end
},
{
label = "What services do you offer?",
next = 'services'
},
{
label = "Just browsing, thanks",
next = 'goodbye'
},
}
},
['repairs'] = {
text = "Let me take a look... Yeah, your engine's seen better days. I can fix it up for $500. What do you say?",
options = {
{ label = "Fix it up", next = 'repair_accept', action = 'repair_vehicle' },
{ label = "Too expensive", next = 'haggle' },
{ label = "Never mind", next = 'goodbye' },
}
},
['haggle'] = {
text = "Look, parts aren't cheap. But since you seem like a decent person, I can do $350. Final offer.",
options = {
{ label = "Deal!", next = 'repair_accept', action = 'repair_vehicle_discount' },
{ label = "I'll pass", next = 'goodbye' },
}
},
['services'] = {
text = "I do repairs, custom paint jobs, performance tuning, and tire changes. What catches your eye?",
options = {
{ label = "Tell me about tuning", next = 'tuning_info' },
{ label = "Back to start", next = 'start' },
}
},
['repair_accept'] = {
text = "Alright, give me a minute... Done! She's running smooth now. Take care of her out there.",
options = {
{ label = "Thanks, Tony!", next = 'end' },
}
},
['goodbye'] = {
text = "No worries. Come back anytime you need help with your ride!",
options = {
{ label = "See you around", next = 'end' },
}
},
}
},
}
该 condition function on 选项允许你根据玩家当前状态动态显示或隐藏选择。只有当玩家乘车到达时才显示修理选项,只有当玩家达到正确阶段时才显示任务相关选项,VIP 选项可以限制特定权限的玩家使用。 action 字段在选择特定选项时触发服务器端函数,将对话选择与游戏结果连接。
生成和管理NPC
对话NPC需要可靠生成,放置在固定位置,并且免受GTA世界混乱的影响。生成对话NPC时,需要请求模型,创建ped,将其设置为任务实体以防止GTA清理系统删除,冻结其位置以防走动,并使其无敌以防玩家杀死任务给予者。使用集中式NPC管理器,在玩家附近生成ped,无玩家范围内时销毁,以节省繁忙服务器的内存:
local spawnedNPCs = {}
function SpawnDialogueNPC(npcId, config)
if spawnedNPCs[npcId] then return end
local model = GetHashKey(config.model)
RequestModel(model)
while not HasModelLoaded(model) do Wait(10) end
local ped = CreatePed(0, model, config.coords.x, config.coords.y,
config.coords.z, config.heading, false, true)
SetEntityInvincible(ped, true)
SetBlockingOfNonTemporaryEvents(ped, true)
FreezeEntityPosition(ped, true)
SetPedFleeAttributes(ped, 0, false)
SetPedCombatAttributes(ped, 46, true)
SetPedCanRagdoll(ped, false)
SetEntityAsMissionEntity(ped, true, true)
SetModelAsNoLongerNeeded(model)
-- Play idle animation if configured
if config.scenario then
TaskStartScenarioInPlace(ped, config.scenario, 0, true)
end
spawnedNPCs[npcId] = { entity = ped, config = config }
return ped
end
结合 SetBlockingOfNonTemporaryEvents 和 FreezeEntityPosition 确保环境事件如附近爆炸、警察追逐或攻击性玩家不会导致您的NPC逃跑、反击或进入布娃娃状态。没有这些保护,玩家可能会遇到被路过车辆撞击后躺在地上抽搐的任务给予者,完全破坏您努力营造的沉浸感。
电影摄像机系统
对话期间的摄像头工作将体验从菜单交互提升为电影时刻。对话开始时,创建一个聚焦NPC面部并略有偏移的摄像头,使用景深模糊背景以吸引注意力。随着对话进展切换摄像头角度,玩家做选择时切换到玩家视角,NPC回应时切换回NPC视角。GTA原生摄像头系统允许你完全控制位置、旋转、视野和景深:
local dialogueCam = nil
function StartDialogueCamera(npcPed)
local npcCoords = GetEntityCoords(npcPed)
local npcHeading = GetEntityHeading(npcPed)
local playerPed = PlayerPedId()
-- Calculate camera position offset from NPC face
local angleRad = math.rad(npcHeading + 160)
local camX = npcCoords.x + (math.sin(angleRad) * 1.5)
local camY = npcCoords.y + (math.cos(angleRad) * 1.5)
local camZ = npcCoords.z + 0.6
dialogueCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
SetCamCoord(dialogueCam, camX, camY, camZ)
PointCamAtPedBone(dialogueCam, npcPed, 31086, 0.0, 0.0, 0.1, true) -- Head bone
-- Depth of field for cinematic look
SetCamNearDof(dialogueCam, 0.5)
SetCamFarDof(dialogueCam, 3.5)
SetCamDofStrength(dialogueCam, 0.6)
SetCamUseShallowDofMode(dialogueCam, true)
SetCamFov(dialogueCam, 40.0) -- Tighter shot
SetCamActive(dialogueCam, true)
RenderScriptCams(true, true, 800, true, false)
-- Disable player controls during dialogue
SetPlayerControl(PlayerId(), false, 0)
-- Make player face NPC
TaskTurnPedToFaceEntity(playerPed, npcPed, 1000)
end
function StopDialogueCamera()
if dialogueCam then
RenderScriptCams(false, true, 500, true, false)
DestroyCam(dialogueCam, true)
dialogueCam = nil
SetPlayerControl(PlayerId(), true, 0)
end
end
该 PointCamAtPedBone native 特别强大,因为它锁定摄像机焦点在 NPC 头部,无论细微动画如何移动,保持对话框架一致。 RenderScriptCams 创建平滑的摄像机淡入淡出效果,而非突兀切换,建议尝试 500ms 到 1000ms 之间的值,以找到适合您服务器节奏的感觉。
字幕显示系统
字幕系统以视觉吸引的方式在屏幕底部呈现NPC对话文本,模仿剧情驱动游戏的对话显示。不是一次性显示整段文本,而是实现打字机效果,逐字显示,营造NPC正在主动说话的错觉。使用NUI显示字幕,因为它允许完全控制字体、颜色、动画和位置的CSS。每当对话节点激活时,将文本、NPC名字及任何情绪标签发送至NUI框架,影响显示样式:
// Subtitle display JavaScript (html/subtitles.js)
let typewriterTimeout = null;
window.addEventListener('message', (event) => {
const data = event.data;
if (data.action === 'showDialogue') {
clearTimeout(typewriterTimeout);
const container = document.getElementById('subtitle-container');
const nameEl = document.getElementById('npc-name');
const textEl = document.getElementById('dialogue-text');
const optionsEl = document.getElementById('dialogue-options');
container.style.display = 'block';
nameEl.textContent = data.npcName;
textEl.textContent = '';
optionsEl.innerHTML = '';
// Typewriter effect
let charIndex = 0;
const fullText = data.text;
function typeNext() {
if (charIndex < fullText.length) {
textEl.textContent += fullText[charIndex];
charIndex++;
typewriterTimeout = setTimeout(typeNext, 30);
} else {
// Show options after text completes
showOptions(data.options);
}
}
typeNext();
}
if (data.action === 'hideDialogue') {
document.getElementById('subtitle-container').style.display = 'none';
}
});
function showOptions(options) {
const optionsEl = document.getElementById('dialogue-options');
options.forEach((opt, index) => {
const btn = document.createElement('button');
btn.className = 'dialogue-option';
btn.innerHTML = `${index + 1} ${opt.label}`;
btn.onclick = () => {
fetch(`https://${GetParentResourceName()}/selectOption`, {
method: 'POST',
body: JSON.stringify({ index: index })
});
};
optionsEl.appendChild(btn);
});
}
为字幕容器设置半透明深色背景、圆角和与服务器主题匹配的细微渐变边框。将其定位在屏幕底部中央,留有足够内边距,避免与小地图或其他HUD元素重叠。添加键盘快捷键,允许玩家按数字键快速选择选项,无需点击,更符合对话流程的自然感。
任务系统集成
当对话系统连接到任务框架时,变得真正强大。对话树的 action 响应选项中的字段提供了执行任务逻辑的挂钩点。当玩家通过对话接受任务时,动作处理器应在玩家的任务日志中创建任务条目,设置所需的路径点或目标,并通过后续对话交互跟踪进度。将任务进度按玩家存储在数据库中,以便他们断线后能从中断处继续。设计任务为状态机,每个状态对应一个对话节点和一组必须完成的目标,完成后下一个对话才可用:
-- Server-side quest actions triggered by dialogue choices
local QuestActions = {
['accept_delivery_job'] = function(src, npcId)
local Player = QBCore.Functions.GetPlayer(src)
local citizenid = Player.PlayerData.citizenid
-- Create quest entry
MySQL.insert(
'INSERT INTO player_quests (citizenid, quest_id, stage, started_at) VALUES (?, ?, ?, NOW())',
{citizenid, 'tony_delivery_1', 'pickup'}
)
-- Set waypoint for pickup location
TriggerClientEvent('quest:client:setWaypoint', src, {
coords = vector3(482.5, -1311.2, 29.2),
blipSprite = 501,
blipColor = 5,
label = 'Package Pickup'
})
TriggerClientEvent('QBCore:Notify', src, 'Quest started: Special Delivery', 'success')
end,
['complete_delivery'] = function(src, npcId)
local Player = QBCore.Functions.GetPlayer(src)
local citizenid = Player.PlayerData.citizenid
MySQL.update(
'UPDATE player_quests SET stage = ?, completed_at = NOW() WHERE citizenid = ? AND quest_id = ?',
{'completed', citizenid, 'tony_delivery_1'}
)
Player.Functions.AddMoney('cash', 1500, 'quest-delivery-reward')
TriggerClientEvent('QBCore:Notify', src, 'Quest complete! Reward: $1,500', 'success')
end,
}
使用任务阶段动态修改可用对话节点。当玩家完成送货后返回 Tony, 对话系统会检查任务阶段并呈现带有奖励的完成对话,而非初始问候。这创造了自然的对话流程,使 NPC 认可玩家进度并做出相应反应,让世界感觉更具响应性和生机。
NPC动画和表情
静止不动的 NPC 说话时显得机械且破坏沉浸感。为对话系统添加动画支持,使 NPC 在交谈时做手势、表情和反应。GTA V 拥有庞大的动画字典库,涵盖手势、面部表情和肢体语言,可在对话特定节点触发。为每个对话行分配动画。NPC 传递好消息时,播放愉快的挥手动画;讨论严肃话题时,使用严肃的双臂交叉姿势。玩家回应间隙,循环思考或等待动画。也可使用面部动画 natives,如 SetFacialIdleAnimOverride 更改 NPC 的休息表情以匹配对话情绪,使其显得高兴、生气、害怕或困惑。结合身体和面部动画以获得最逼真的表现,并始终在游戏中测试动画,因为某些动画字典在不同的行人模型上表现不同,适用于男性行人的动画可能会在女性行人上出现剪裁或不自然。
性能与最佳实践
跨地图生成数十个 NPC 的对话系统需谨慎管理性能。仅在玩家处于渲染距离内(通常50-100米)生成 NPC,玩家远离时销毁。使用单线程管理所有 NPC 生成距离,避免为每个 NPC 创建线程,显著降低客户端开销。对话树数据在资源启动时缓存,避免对话中读取文件。保持对话摄像机切换流畅,但避免频繁创建销毁摄像机,因为摄像机操作有明显性能成本。多玩家同时与同一 NPC 互动时,每位玩家应拥有独立对话实例,状态存储于玩家而非 NPC 实体。玩家断开连接或中途离开对话时,清理所有对话资源,销毁摄像机,释放 NUI 焦点,恢复玩家控制。测试系统时考虑最坏情况:玩家游戏中途崩溃或 Alt-F4 退出时,系统应能检测并优雅清理,防止摄像机残留或控制锁定。