Система дерева навыков FiveM: Правильное развитие уровня
Добавьте дерево навыков на ваш сервер FiveM. Опыт, разблокировки, дизайн интерфейса, привязки к профессиям и лучшие скрипты навыков для значимых долгосрочных целей игроков.
Agency Scripts
Основатель и ведущий разработчик Agency Scripts
Почему деревья навыков трансформируют ролевые серверы
Большинство ролевых серверов FiveM используют прогрессию на основе профессий, где игрок выбирает карьеру и сразу получает доступ ко всем её инструментам и способностям. Хотя это работает, такой подход не даёт ощущения роста или долгосрочных вложений. Деревья навыков меняют эту динамику кардинально. Вводя RPG-стиль прогрессии, игроки зарабатывают опыт через свои действия и тратят очки навыков на разблокировку способностей, бонусов и пассивных эффектов в разветвлённых талантах. Механик, ремонтирующий сотни машин, постепенно становится быстрее и эффективнее. Преступник, который часто вскрывает замки, становится в этом лучше. Это создаёт значимое развитие персонажа, которое вознаграждает усердие и делает каждого персонажа уникальным. Главное , разработать систему, которая улучшает ролевую игру, а не превращает сервер в MMO-гринд, сохраняя бонусы достаточно тонкими, чтобы различия в навыках дополняли RP-сценарии, а не доминировали в них.
Проектирование структуры данных дерева навыков
Хорошо продуманное дерево навыков начинается с чистой структуры данных. Каждое дерево принадлежит категории, например, бой, крафт, вождение, медицина или криминал. Внутри дерева отдельные узлы представляют навыки, которые можно разблокировать. Каждый узел имеет уникальный идентификатор, отображаемое имя, описание, максимальный уровень, стоимость очков за уровень, обязательные предварительные узлы и фактический игровой эффект. Храните определения деревьев в общей конфигурации, чтобы сервер и клиент могли к ним обращаться. Прогресс игрока , отдельная структура данных, отслеживающая, какие узлы разблокированы и на каком уровне, а также доступные неиспользованные очки навыков. Отделение определения дерева от прогресса позволяет обновлять или балансировать дерево без миграции данных игроков, при условии стабильности идентификаторов узлов.
-- shared/skill_trees.lua
SkillTrees = {
mechanic = {
label = 'Mechanic',
icon = 'fa-wrench',
nodes = {
quickFix = {
label = 'Quick Fix',
desc = 'Reduce vehicle repair time',
maxLevel = 5,
cost = 1,
prereqs = {},
effect = {type = 'repair_speed', perLevel = 0.10},
},
engineExpert = {
label = 'Engine Expert',
desc = 'Repair engines to higher condition',
maxLevel = 3,
cost = 2,
prereqs = {'quickFix'},
effect = {type = 'engine_quality', perLevel = 0.15},
},
bodyworkMaster = {
label = 'Bodywork Master',
desc = 'Repair body damage more effectively',
maxLevel = 3,
cost = 2,
prereqs = {'quickFix'},
effect = {type = 'body_quality', perLevel = 0.15},
},
turboTuner = {
label = 'Turbo Tuner',
desc = 'Unlock performance tuning abilities',
maxLevel = 1,
cost = 5,
prereqs = {'engineExpert'},
effect = {type = 'unlock_tuning', perLevel = 1},
},
},
},
criminal = {
label = 'Street Smarts',
icon = 'fa-mask',
nodes = {
lockpicking = {
label = 'Lockpicking',
desc = 'Pick locks faster with fewer failures',
maxLevel = 5,
cost = 1,
prereqs = {},
effect = {type = 'lockpick_speed', perLevel = 0.12},
},
silentStep = {
label = 'Silent Step',
desc = 'Reduce noise while crouching',
maxLevel = 3,
cost = 2,
prereqs = {'lockpicking'},
effect = {type = 'noise_reduction', perLevel = 0.20},
},
safecracker = {
label = 'Safecracker',
desc = 'Attempt to crack safes',
maxLevel = 1,
cost = 4,
prereqs = {'lockpicking'},
effect = {type = 'unlock_safecrack', perLevel = 1},
},
},
},
}
Отслеживание опыта и уровней
Опыт должен зарабатываться органично через игровые действия, а не через повторяющиеся циклы гринда. Отслеживайте опыт по категориям дерева навыков, чтобы выполнение механических работ приносило опыт механика, совершение преступлений , опыт преступника и так далее. Определите пороги опыта для каждого уровня, и когда игрок пересекает порог, он получает один или несколько очков навыков для траты в этом дереве. Используйте убывающую отдачу при повторении одинаковых действий, чтобы предотвратить эксплойт-циклы. Например, первый ремонт транспортного средства за 10 минут может дать 50 XP, последующие , 25, затем 10, затем 5. Это поощряет разнообразный геймплей вместо бессмысленного повторения. Сервер должен быть единственным источником выдачи XP, никогда не доверяя клиенту сообщать собственные значения опыта. Запускайте события XP из серверной логики после проверки факта совершения действия.
-- server/experience.lua
local XP_THRESHOLDS = {0, 100, 300, 600, 1000, 1500, 2200, 3000, 4000, 5500}
local recentActions = {}
function GrantXP(playerId, tree, amount, actionType)
local identifier = GetPlayerIdentifier(playerId, 0)
local now = os.time()
-- Diminishing returns for repeated actions
local key = identifier .. ':' .. tree .. ':' .. actionType
if not recentActions[key] then
recentActions[key] = {count = 0, resetAt = now + 600}
end
local ra = recentActions[key]
if now > ra.resetAt then
ra.count = 0
ra.resetAt = now + 600
end
ra.count = ra.count + 1
local multiplier = math.max(0.1, 1.0 - (ra.count - 1) * 0.25)
local finalXP = math.floor(amount * multiplier)
-- Update database
MySQL.update([[
UPDATE character_skills SET xp = xp + ?
WHERE identifier = ? AND tree = ?
]], {finalXP, identifier, tree})
-- Check for level up
local data = MySQL.single.await([[
SELECT xp, level, points FROM character_skills
WHERE identifier = ? AND tree = ?
]], {identifier, tree})
if data then
local nextThreshold = XP_THRESHOLDS[data.level + 2]
if nextThreshold and data.xp >= nextThreshold then
local newLevel = data.level + 1
MySQL.update([[
UPDATE character_skills
SET level = ?, points = points + 1
WHERE identifier = ? AND tree = ?
]], {newLevel, identifier, tree})
TriggerClientEvent('skills:levelUp', playerId, tree, newLevel)
end
end
TriggerClientEvent('skills:xpGained', playerId, tree, finalXP)
end
Применение эффектов навыков к игровому процессу
Эффекты навыков , это то, где система становится ощутимой для игроков. Каждый разблокированный узел изменяет конкретный игровой механизм измеримым образом. Реализуйте центральный резолвер эффектов, который принимает идентификатор игрока и тип эффекта, затем вычисляет общий бонус от всех разблокированных узлов, влияющих на этот эффект. Когда другой скрипт хочет узнать бонус скорости ремонта игрока, он вызывает этот резолвер и получает множитель. Это держит вашу систему навыков отделённой от других ресурсов. Другим скриптам не нужно знать о деревьях навыков, узлах или уровнях; они просто запрашивают бонус по типу. Для разблокируемых способностей, таких как вскрытие сейфов или турбо-тюнинг, резолвер возвращает булево значение, указывает ли игрок на наличие нужного узла. Такая архитектура облегчает интеграцию навыков в существующие скрипты с минимальными изменениями кода.
-- server/effects.lua
local playerSkillCache = {}
function GetSkillBonus(playerId, effectType)
local identifier = GetPlayerIdentifier(playerId, 0)
local cache = playerSkillCache[identifier]
if not cache then return 0.0 end
local total = 0.0
for treeName, treeDef in pairs(SkillTrees) do
for nodeId, nodeDef in pairs(treeDef.nodes) do
if nodeDef.effect.type == effectType then
local unlocked = cache[treeName] and cache[treeName][nodeId] or 0
total = total + (nodeDef.effect.perLevel * unlocked)
end
end
end
return total
end
function HasSkillUnlock(playerId, effectType)
return GetSkillBonus(playerId, effectType) > 0
end
-- Export for other resources
exports('GetSkillBonus', GetSkillBonus)
exports('HasSkillUnlock', HasSkillUnlock)
-- Example: another resource checking repair speed
-- local bonus = exports['skillsystem']:GetSkillBonus(source, 'repair_speed')
-- local repairTime = baseTime * (1.0 - bonus)
Создание дерева навыков NUI
Визуальное представление вашего дерева навыков напрямую влияет на вовлеченность игроков в систему прогрессии. Создайте панель NUI, которая отображает каждое дерево в виде разветвленного графа с узлами, соединенными линиями, показывающими требования. Открытые узлы должны светиться или пульсировать цветом, заблокированные, но доступные , выглядеть слегка затемненными, а узлы с невыполненными требованиями , быть серыми. Показывайте текущий уровень игрока, индикатор прогресса XP и доступные очки навыков в верхней части. При клике на узел отображайте его описание, текущий уровень из максимума, стоимость улучшения и бонус эффекта за уровень. Включите шаг подтверждения перед тратой очков, так как игроки могут захотеть тщательно планировать билды. Используйте плавные CSS-переходы для изменений состояния, чтобы улучшение узла ощущалось наградой с кратковременным вспышкой или анимацией масштабирования. Рассмотрите возможность добавления опции сброса, бесплатной или за внутриигровую валюту, чтобы игроки могли пересобирать билды, если мета меняется или они хотят попробовать другой стиль игры.
Вопросы баланса и защиты от эксплойтов
Баланс , самая сложная часть любой системы прогрессии. Если бонусы слишком сильны, игроки высокого уровня становятся неприкосновенными, а новички чувствуют безысходность. Если бонусы слишком слабы, никому не интересно развиваться. Начинайте консервативно с бонусов 5-15 процентов за навык на максимальном уровне и отслеживайте отзывы игроков. Реализуйте серверную проверку для каждого распределения очков навыков. Когда игрок запрашивает повышение узла, проверьте, что у него достаточно очков, что выполнены все требования, и что узел не достиг максимального уровня. Никогда не доверяйте состоянию клиента для этих проверок. Добавьте логирование всех транзакций с очками навыков, чтобы обнаруживать и откатывать эксплойты. Ограничьте частоту запросов на распределение очков, чтобы предотвратить попытки эксплойтов с быстрой отправкой. Для соревновательных сценариев рассмотрите лимиты навыков, ограничивающие количество деревьев, в которые может инвестировать один персонаж, заставляя делать осознанный выбор, а не позволять максимизировать всё. Это создаёт разнообразные билды персонажей и поощряет сотрудничество между игроками с дополняющими навыками.
Схема базы данных и стратегия миграции
Разработайте схему базы данных, которая грамотно обрабатывает как текущие данные навыков, так и будущие расширения деревьев. Храните распределение навыков игрока в виде отдельных строк для каждого дерева и персонажа, а не в одном JSON-блоке. Это делает запросы по конкретным навыкам эффективными и позволяет выполнять агрегацию на уровне базы данных для аналитики. Включите столбцы с названием дерева, текущим уровнем, общим XP, доступными очками и JSON-столбец для распределения узлов внутри дерева. При добавлении новых деревьев или узлов в будущих обновлениях данные игроков остаются целыми, так как новые узлы просто начинаются с нуля. Если вы переименовываете или удаляете узел, обработайте миграцию, возвращая потраченные очки в доступный пул игрока для этого дерева. Версионируйте определения деревьев, чтобы логика миграции знала, какие изменения применять. Такой продуманный подход избавит вас от болезненных миграций данных по мере развития вашей системы навыков в течение месяцев работы сервера.
-- SQL schema
CREATE TABLE IF NOT EXISTS character_skills (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(64) NOT NULL,
char_slot INT DEFAULT 1,
tree VARCHAR(32) NOT NULL,
level INT DEFAULT 0,
xp INT DEFAULT 0,
points INT DEFAULT 0,
allocations JSON DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_char_tree (identifier, char_slot, tree),
INDEX idx_identifier (identifier)
);