FiveM 声望系统:统计、技能与进阶
为FiveM添加声望和技能进阶。属性公式、解锁、UI条以及让玩家更长时间投入的最佳声望脚本。
Agency Scripts
Agency Scripts 创始人兼首席开发者
为什么声望系统改变角色扮演
大多数 FiveM 服务器将角色进阶视为二元状态:有职业或无职业。缺乏成长、精通或获得的身份感。声望与技能系统根本改变这一点,为玩家提供可衡量的进阶,奖励投入时间和完成的活动。技工修理第百辆车并解锁高级引擎调校时,这成就感真实存在。罪犯积累足够街头声望以使用高级抢劫装备时,有明显的进阶感推动游戏。本文将从零构建完整声望与技能框架,涵盖经验计算、技能树、声望等级、可解锁能力及保持终局玩家参与的声望系统。
玩家技能的数据库架构
任何进度系统的基础是设计良好的数据库模式。你需要表来跟踪个人技能经验值、每个派系或活动的声望分数以及解锁的能力。模式应足够规范化以便查询,但又足够非规范化以避免每次技能检测时进行昂贵的连接。我们使用一个单一的 player_skills 该表使用市民ID和技能名称的复合键,以及一个单独的 player_reputation 用于派系声望的表。此分离保持技能(个人能力)与声誉(NPC和派系对你的看法)区分开来,从而实现更细致的游戏互动。
-- SQL schema
CREATE TABLE IF NOT EXISTS player_skills (
citizenid VARCHAR(50) NOT NULL,
skill_name VARCHAR(50) NOT NULL,
xp INT DEFAULT 0,
level INT DEFAULT 1,
prestige INT DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (citizenid, skill_name)
);
CREATE TABLE IF NOT EXISTS player_reputation (
citizenid VARCHAR(50) NOT NULL,
faction VARCHAR(50) NOT NULL,
reputation INT DEFAULT 0,
tier VARCHAR(20) DEFAULT 'neutral',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (citizenid, faction)
);
经验值计算和升级曲线
一个平坦的经验曲线,即每级需要相同经验,感觉不够有成就感,因为早期等级升级过快,而后期等级感觉无差别。最佳方法是使用多项式曲线,每级所需经验逐渐增加,但增长不至于使最高等级无法达到。常用公式是 requiredXP = baseXP * (level ^ exponent) 基数为 100,指数为 1.5,创建平滑曲线。等级 1 需要 100 XP,等级 10 约需 3,162 XP,等级 50 约需 35,355 XP。这保持了早期进度的快速感,同时使高级别成为需要专注游戏才能达到的真正成就。
-- shared/skills_config.lua
SkillsConfig = {}
SkillsConfig.BaseXP = 100
SkillsConfig.Exponent = 1.5
SkillsConfig.MaxLevel = 50
function SkillsConfig.GetRequiredXP(level)
return math.floor(SkillsConfig.BaseXP * (level ^ SkillsConfig.Exponent))
end
function SkillsConfig.GetTotalXPForLevel(targetLevel)
local total = 0
for i = 1, targetLevel - 1 do
total = total + SkillsConfig.GetRequiredXP(i)
end
return total
end
-- Skill definitions with XP sources
SkillsConfig.Skills = {
driving = {
label = 'Driving',
icon = 'fa-car',
xpSources = {
{ action = 'distance_driven', xpPer = 1, unit = 'per 500m' },
{ action = 'race_won', xpPer = 150, unit = 'per win' },
{ action = 'no_crash_streak', xpPer = 50, unit = 'per 5min' },
}
},
shooting = {
label = 'Marksmanship',
icon = 'fa-crosshairs',
xpSources = {
{ action = 'headshot', xpPer = 25, unit = 'per kill' },
{ action = 'bodyshot', xpPer = 10, unit = 'per kill' },
{ action = 'range_practice', xpPer = 5, unit = 'per target' },
}
},
mechanic = {
label = 'Mechanic',
icon = 'fa-wrench',
xpSources = {
{ action = 'vehicle_repaired', xpPer = 30, unit = 'per repair' },
{ action = 'engine_swap', xpPer = 100, unit = 'per swap' },
{ action = 'custom_tune', xpPer = 75, unit = 'per tune' },
}
},
cooking = {
label = 'Cooking',
icon = 'fa-utensils',
xpSources = {
{ action = 'meal_cooked', xpPer = 20, unit = 'per meal' },
{ action = 'recipe_discovered', xpPer = 50, unit = 'per recipe' },
}
},
stamina = {
label = 'Stamina',
icon = 'fa-running',
xpSources = {
{ action = 'distance_sprinted', xpPer = 1, unit = 'per 200m' },
{ action = 'swimming', xpPer = 2, unit = 'per 100m' },
}
},
}
服务器端经验值管理
所有经验值修改必须在服务器端进行以防止利用。服务器验证每个经验值授予请求,检查速率限制以防止刷屏,应用任何激活的倍数,并将结果持久化到数据库。关键设计决策是每次经验变化是否保存或批量写入。每次微小经验增长保存会严重影响数据库,尤其是被动技能如驾驶距离。解决方案是内存缓存定期刷新数据库,并对重要事件如升级立即保存。这既保证了玩家的实时准确性,也保持数据库负载可控。
-- server/skills_manager.lua
local playerCache = {}
local SAVE_INTERVAL = 60000 -- flush to DB every 60 seconds
-- Load player skills from DB on join
AddEventHandler('playerJoining', function()
local src = source
local citizenid = GetCitizenId(src)
if not citizenid then return end
local skills = MySQL.query.await('SELECT * FROM player_skills WHERE citizenid = ?', {citizenid})
playerCache[citizenid] = {}
for _, row in ipairs(skills or {}) do
playerCache[citizenid][row.skill_name] = {
xp = row.xp,
level = row.level,
prestige = row.prestige,
dirty = false
}
end
end)
function AddSkillXP(citizenid, skillName, amount)
if not SkillsConfig.Skills[skillName] then return end
if not playerCache[citizenid] then return end
local skill = playerCache[citizenid][skillName]
if not skill then
skill = { xp = 0, level = 1, prestige = 0, dirty = false }
playerCache[citizenid][skillName] = skill
end
-- Apply prestige multiplier (each prestige = +5% XP)
local multiplier = 1.0 + (skill.prestige * 0.05)
local finalXP = math.floor(amount * multiplier)
skill.xp = skill.xp + finalXP
skill.dirty = true
-- Check for level up
local required = SkillsConfig.GetRequiredXP(skill.level)
while skill.xp >= required and skill.level < SkillsConfig.MaxLevel do
skill.xp = skill.xp - required
skill.level = skill.level + 1
required = SkillsConfig.GetRequiredXP(skill.level)
-- Notify player of level up
local src = GetPlayerByCitizenId(citizenid)
if src then
TriggerClientEvent('skills:levelUp', src, skillName, skill.level)
end
-- Immediate save on level up
SaveSkill(citizenid, skillName, skill)
end
return skill
end
exports('AddSkillXP', AddSkillXP)
技能树与可解锁能力
技能树在原始升级基础上增加策略层。不同于所有20级机械师完全相同,技能树允许他们专精。一名机械师可能投资点数于引擎性能,解锁涡轮套件,另一名专注车身,获得专属喷漆和涂装。每级获得一点技能点,可在分支树中使用。树结构采用简单的父子关系,解锁子节点需父节点已解锁且达到最低技能等级,防止玩家跳跃至最强能力而忽略基础。
-- shared/skill_trees.lua
SkillTrees = {
mechanic = {
{ id = 'basic_repair', label = 'Basic Repair', reqLevel = 1, parent = nil,
effect = { type = 'speed', value = 1.0 }, desc = 'Standard repair speed' },
{ id = 'fast_repair', label = 'Quick Hands', reqLevel = 5, parent = 'basic_repair',
effect = { type = 'speed', value = 1.3 }, desc = '30% faster repairs' },
{ id = 'engine_diag', label = 'Engine Diagnostics', reqLevel = 10, parent = 'basic_repair',
effect = { type = 'unlock', value = 'engine_scan' }, desc = 'Scan engine health remotely' },
{ id = 'turbo_kit', label = 'Turbo Installation', reqLevel = 20, parent = 'engine_diag',
effect = { type = 'unlock', value = 'install_turbo' }, desc = 'Install turbo kits on vehicles' },
{ id = 'master_tune', label = 'Master Tuner', reqLevel = 35, parent = 'turbo_kit',
effect = { type = 'unlock', value = 'advanced_tune' }, desc = 'Access to advanced ECU tuning' },
{ id = 'body_expert', label = 'Body Expert', reqLevel = 10, parent = 'basic_repair',
effect = { type = 'unlock', value = 'custom_paint' }, desc = 'Unlock exclusive paint options' },
{ id = 'livery_master', label = 'Livery Master', reqLevel = 25, parent = 'body_expert',
effect = { type = 'unlock', value = 'custom_livery' }, desc = 'Create and apply custom liveries' },
},
}
-- server: unlock a skill tree node
function UnlockNode(citizenid, skillName, nodeId)
local tree = SkillTrees[skillName]
if not tree then return false, 'No tree for skill' end
local node = nil
for _, n in ipairs(tree) do
if n.id == nodeId then node = n break end
end
if not node then return false, 'Node not found' end
local skill = GetPlayerSkill(citizenid, skillName)
if not skill or skill.level < node.reqLevel then
return false, 'Level too low'
end
if node.parent then
local parentUnlocked = IsNodeUnlocked(citizenid, skillName, node.parent)
if not parentUnlocked then return false, 'Parent node locked' end
end
MySQL.insert('INSERT INTO player_skill_nodes (citizenid, skill_name, node_id) VALUES (?, ?, ?)',
{citizenid, skillName, nodeId})
return true
end
声望等级与派系声望
声望跟踪组织和派系对玩家的看法,独立于纯技能。玩家可能是驾驶专家,但因犯罪行为在警察派系中声望极低。声望分为多个等级,从敌对到中立再到受尊敬,每个等级解锁不同的对话选项、任务访问和商店价格。提升一个派系的声望可能会降低对立派系的声望,形成有意义的权衡。例如,完成毒品配送会提升卡特尔声望,但降低警察声望。该系统鼓励玩家做出定义角色的选择,而不是同时最大化所有派系。
-- shared/reputation_config.lua
ReputationConfig = {
tiers = {
{ name = 'hostile', minRep = -1000, color = '#ef4444' },
{ name = 'unfriendly', minRep = -500, color = '#f97316' },
{ name = 'neutral', minRep = 0, color = '#94a3b8' },
{ name = 'friendly', minRep = 500, color = '#22c55e' },
{ name = 'honored', minRep = 1500, color = '#3b82f6' },
{ name = 'revered', minRep = 3000, color = '#a855f7' },
},
factions = {
police = { label = 'LSPD', opposing = {'cartel', 'gang_ballas'} },
ems = { label = 'EMS', opposing = {} },
cartel = { label = 'Madrazo Cartel', opposing = {'police'} },
mechanic = { label = 'LS Customs', opposing = {} },
gang_ballas = { label = 'Ballas', opposing = {'police', 'gang_families'} },
gang_families = { label = 'Families', opposing = {'gang_ballas'} },
},
opposingPenalty = 0.5, -- lose 50% of gained rep from opposing factions
}
-- server: modify reputation with faction cascading
function ModifyReputation(citizenid, faction, amount)
local config = ReputationConfig.factions[faction]
if not config then return end
-- Apply to primary faction
AdjustRep(citizenid, faction, amount)
-- Penalize opposing factions
if amount > 0 then
for _, opposing in ipairs(config.opposing) do
local penalty = math.floor(amount * ReputationConfig.opposingPenalty)
AdjustRep(citizenid, opposing, -penalty)
end
end
end
声望系统
当玩家在某项技能达到最高等级后,他们需要理由继续参与该活动。声望系统通过允许玩家将技能重置为1级以换取永久加成来实现:一个显示在名字旁的外观声望徽章,该技能每个声望等级5%的经验加成,以及声望专属解锁内容,如独特车辆改装或稀有制作配方。声望计数无限,但每次声望所需时间更长,因为经验加成使早期等级变得轻松,而曲线在高等级仍然存在。UI中在技能名称旁以罗马数字或星星显示声望计数,清晰展示玩家的投入。
-- server: prestige a maxed skill
function PrestigeSkill(citizenid, skillName)
local skill = playerCache[citizenid] and playerCache[citizenid][skillName]
if not skill then return false, 'Skill not found' end
if skill.level < SkillsConfig.MaxLevel then return false, 'Not max level' end
-- Reset level and XP, increment prestige
skill.level = 1
skill.xp = 0
skill.prestige = skill.prestige + 1
skill.dirty = true
-- Save immediately
SaveSkill(citizenid, skillName, skill)
-- Grant prestige reward
local src = GetPlayerByCitizenId(citizenid)
if src then
TriggerClientEvent('skills:prestige', src, skillName, skill.prestige)
-- Unlock prestige-specific items
if skill.prestige == 1 then
exports.ox_inventory:AddItem(src, 'prestige_badge_'..skillName, 1)
elseif skill.prestige == 5 then
exports.ox_inventory:AddItem(src, 'gold_tool_'..skillName, 1)
end
end
return true, skill.prestige
end
RegisterNetEvent('skills:requestPrestige', function(skillName)
local src = source
local citizenid = GetCitizenId(src)
local success, result = PrestigeSkill(citizenid, skillName)
if success then
lib.notify(src, { title = 'Prestige!', description = ('Prestige %d achieved for %s'):format(result, skillName), type = 'success' })
else
lib.notify(src, { title = 'Cannot Prestige', description = result, type = 'error' })
end
end)
客户端UI和被动经验追踪
客户端处理驾驶距离和冲刺等活动的被动经验值跟踪,定期向服务器发送更新。常见错误是每移动一米发送事件,导致网络拥堵。应在本地变量累计距离,每30秒发送一次批量更新。UI方面,构建一个通过命令或快捷键访问的技能面板,显示所有技能的当前等级、经验进度条、声望计数和技能树。使用React或Vue等现代框架的NUI进行树状图可视化,锁定节点显示为灰色,解锁节点带有发光效果。经验进度条在获得经验时应平滑动画,提供令人满意的视觉反馈,加强进展循环,激励玩家持续发展角色。