Pourquoi les arbres de compétences transforment les serveurs de jeu de rôle
La plupart des serveurs de jeu de rôle FiveM reposent sur une progression basée sur le travail dans laquelle un joueur choisit une carrière et a instantanément accès à tous ses outils et capacités. Bien que fonctionnelle, cette approche ne procure aucun sentiment de croissance ou d’investissement à long terme. Les arbres de compétences changent fondamentalement cette dynamique. En introduisant une progression de style RPG, les joueurs gagnent de l'expérience grâce à leurs actions et dépensent des points de compétence pour débloquer des capacités, des avantages et des bonus passifs dans des arbres de talents ramifiés. Un mécanicien qui répare des centaines de voitures devient progressivement plus rapide et plus efficace. Un criminel qui crochete les serrures à plusieurs reprises devient meilleur dans ce domaine. Cela crée un développement de personnage significatif qui récompense le dévouement et donne à chaque personnage un sentiment unique. La clé est de concevoir un système qui améliore le jeu de rôle plutôt que de transformer ton serveur en un MMO, en gardant les bonus suffisamment subtils pour que les différences de compétences complètent les scénarios RP plutôt que de les dominer.
Conception de la structure des données de l'arbre de compétences
Un arbre de compétences bien conçu commence par une structure de données propre. Chaque arbre de compétences appartient à une catégorie comme le combat, l'artisanat, la conduite, la médecine ou le crime. Au sein de chaque arbre, des nœuds individuels représentent des compétences qui peuvent être débloquées. Chaque nœud a un identifiant unique, un nom d'affichage, une description, un niveau maximum, un coût en points par niveau, des nœuds prérequis qui doivent être déverrouillés en premier et l'effet de jeu réel qu'il applique. Stockez les définitions d'arborescence dans un fichier de configuration partagé afin que le serveur et le client puissent les référencer. La progression du joueur est une structure de données distincte qui suit les nœuds qu'il a débloqués et à quel niveau, ainsi que ses points de compétence non dépensés disponibles. Garder la définition de l'arborescence séparée de la progression du joueur signifie que tu peux mettre à jour ou rééquilibrer l'arborescence sans migrer les données du joueur, tant que les identifiants de nœud restent stables.
-- 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},
},
},
},
}
Suivi et mise à niveau de l'expérience
L’expérience doit être acquise de manière organique grâce à des actions de jeu plutôt qu’à travers des boucles répétitives. Suivez l'expérience par catégorie d'arbre de compétences afin que l'exécution d'un travail mécanique rapporte de l'XP mécanique, que commettre des crimes rapporte de l'XP criminelle, etc. Définissez des seuils d'expérience pour chaque niveau, et lorsqu'un joueur franchit un seuil, il gagne un ou plusieurs points de compétence à dépenser dans cet arbre. Utilisez des rendements décroissants sur des actions identiques répétées pour décourager les boucles d’exploitation. Par exemple, la première réparation d'un véhicule dans une fenêtre de 10 minutes peut accorder 50 XP, mais les réparations suivantes en accordent 25, puis 10, puis 5. Cela récompense un gameplay varié plutôt qu'une répétition insensée. Le serveur doit être la seule autorité sur les subventions XP, sans jamais faire confiance au client pour rapporter ses propres valeurs d'expérience. Déclenchez des événements XP à partir de la logique côté serveur après avoir vérifié que l'action s'est réellement produite.
-- 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
Appliquer des effets de compétences au gameplay
Les effets de compétences sont là où le système devient tangible pour les joueurs. Chaque nœud déverrouillé modifie un mécanisme de jeu spécifique de manière mesurable. Implémentez un résolveur d'effet central qui prend un identifiant de joueur et un type d'effet, puis calcule le bonus total de tous les nœuds déverrouillés qui contribuent à cet effet. Lorsqu'un autre script a besoin de connaître le bonus de vitesse de réparation d'un joueur, il appelle ce résolveur et récupère un multiplicateur. Cela maintient ton système de compétences découplé des autres ressources. Les autres scripts n'ont pas besoin de connaître les arbres de compétences, les nœuds ou les niveaux ; ils demandent simplement une valeur de bonus par type. Pour les capacités déverrouillables telles que le safecracking ou le turbo tuning, le résolveur renvoie un booléen indiquant si le joueur dispose du nœud requis. Cette architecture facilite l'intégration des compétences dans les scripts existants avec un minimum de modifications de code de leur côté.
-- 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)
Construire l'arbre de compétences NUI
La présentation visuelle de ton arbre de compétences affecte directement la façon dont les joueurs se sentent engagés dans le système de progression. Créez un panneau NUI qui affiche chaque arbre sous la forme d'un graphique de branchement avec des nœuds connectés par des lignes indiquant les prérequis. Les nœuds déverrouillés doivent briller ou clignoter avec une couleur, les nœuds verrouillés mais disponibles doivent apparaître légèrement grisés et les nœuds dont les conditions préalables ne sont pas remplies doivent être grisés. Affichez le niveau actuel du joueur, la barre de progression XP et les points de compétence disponibles bien en évidence en haut. Lorsqu'un joueur clique sur un nœud, affichez sa description, le niveau actuel hors maximum, le coût de mise à niveau et le bonus d'effet par niveau. Incluez une étape de confirmation avant de dépenser des points, car les joueurs voudront peut-être planifier soigneusement leurs constructions. Utilisez des transitions CSS fluides pour les changements d'état afin que la mise à niveau d'un nœud soit gratifiante avec une brève animation flash ou à l'échelle. Pensez à ajouter une option de réinitialisation, soit gratuite, soit avec un coût en devise dans le jeu, afin que les joueurs puissent respécifier leurs builds si la méta change ou s'ils souhaitent essayer un style de jeu différent.
Considérations sur l’équilibrage et la lutte contre les exploits
L'équilibre est la partie la plus difficile de tout système de progression. Si les bonus sont trop élevés, les joueurs de haut niveau deviennent intouchables et les nouveaux joueurs se sentent désespérés. Si les bonus sont trop faibles, personne ne se soucie de la progression. Commencez prudemment avec 5 à 15 pour cent de bonus par compétence au niveau maximum et surveillez les commentaires des joueurs. Implémentez la validation côté serveur pour chaque allocation de points de compétence. Lorsqu'un joueur demande la mise à niveau d'un nœud, vérifiez qu'il a suffisamment de points, vérifiez que toutes les conditions préalables sont remplies et vérifiez que le nœud n'est pas déjà au niveau maximum. Ne faites jamais confiance à l'état du client pour ces contrôles. Ajoutez une journalisation pour toutes les transactions de points de compétence afin de pouvoir détecter et annuler les exploits. Demandes d’allocation de compétences limitées pour empêcher les tentatives d’exploitation rapides. Pour les scénarios compétitifs, envisagez des plafonds de compétences qui limitent le nombre d’arbres dans lesquels un seul personnage peut investir, forçant ainsi des choix significatifs plutôt que de permettre à un joueur de tout maximiser. Cela crée des personnages diversifiés et encourage la coopération entre des joueurs possédant des compétences complémentaires.
Schéma de base de données et stratégie de migration
Concevez ton schéma de base de données pour gérer avec élégance à la fois les données de compétences actuelles et les futures extensions d'arborescence. Stockez les allocations de compétences des joueurs sous forme de lignes individuelles par arbre et par personnage plutôt que dans un seul blob JSON. Cela rend l'interrogation de compétences spécifiques efficace et permet l'agrégation au niveau de la base de données à des fins d'analyse. Incluez des colonnes pour le nom de l'arborescence, le niveau actuel, l'XP totale, les points disponibles et une colonne JSON pour les allocations de nœuds dans cette arborescence. Lorsque tu ajoutes de nouveaux arbres ou nœuds de compétences dans les mises à jour futures, les données des joueurs existants restent intactes car les nouveaux nœuds commencent simplement au niveau zéro. Si tu renommes ou supprimez un nœud, gérez la migration en remboursant les points dépensés dans le pool disponible du joueur pour cet arbre. Versionnez tes définitions d’arborescence afin que la logique de migration sache quelles modifications appliquer. Cette approche avant-gardiste tu évite des migrations de données douloureuses à mesure que ton système de compétences évolue au fil des mois d'exploitation du serveur.
-- 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)
);
