FiveM-Fähigkeitenbaum-System: Levelaufstieg richtig umgesetzt
Ergänze deinen FiveM-Server um einen Fähigkeitenbaum mit EP, Freischaltungen, Benutzeroberfläche, Job-Bezug und sinnvollen Langzeitzielen.
Agency Scripts
Gründer und leitender Entwickler bei Agency Scripts
Warum Skill Trees Roleplay-Server verändern
Die meisten FiveM-Rollenspielserver setzen auf einen berufsabhängigen Fortschritt: Spieler wählen einen Beruf und erhalten sofort Zugriff auf alle zugehörigen Werkzeuge und Fähigkeiten. Dieser Ansatz funktioniert, vermittelt aber weder ein Gefühl von Entwicklung noch langfristiges Engagement. Skilltrees verändern diese Dynamik grundlegend. Mit Fortschritt im RPG-Stil sammeln Spieler durch ihre Aktionen Erfahrung und geben Skillpunkte aus, um Fähigkeiten, Perks und passive Boni in verzweigten Talentbäumen freizuschalten. Ein Mechaniker, der Hunderte Autos repariert, wird nach und nach schneller und effizienter. Ein Krimineller, der immer wieder Schlösser knackt, wird darin besser. So entsteht eine bedeutungsvolle Charakterentwicklung, die Engagement belohnt und jeden Charakter einzigartig wirken lässt. Entscheidend ist, ein System zu entwickeln, das das Rollenspiel bereichert, statt deinen Server in einen MMO-Grind zu verwandeln. Die Boni sollten subtil genug sein, damit Skillunterschiede RP-Szenarien ergänzen, statt sie zu dominieren.
Die Datenstruktur des Skilltrees entwerfen
Ein gut gestalteter Skilltree beginnt mit einer übersichtlichen Datenstruktur. Jeder Skilltree gehört zu einer Kategorie wie Kampf, Handwerk, Fahren, Medizin oder Kriminalität. In jedem Baum stehen einzelne Knoten für freischaltbare Skills. Jeder Knoten hat eine eindeutige Kennung, einen Anzeigenamen, eine Beschreibung, eine maximale Stufe, Kosten pro Stufe in Skillpunkten, vorausgesetzte Knoten, die zuerst freigeschaltet werden müssen, und den konkreten Gameplay-Effekt, den er auslöst. Speichere die Baumdefinitionen in einer gemeinsamen Konfigurationsdatei, damit Server und Client darauf zugreifen können. Der Fortschritt eines Spielers ist eine separate Datenstruktur, die festhält, welche Knoten er freigeschaltet hat und auf welcher Stufe sie sind, sowie wie viele nicht ausgegebene Skillpunkte verfügbar sind. Wenn du die Baumdefinition vom Spielerfortschritt trennst, kannst du den Baum aktualisieren oder neu ausbalancieren, ohne Spielerdaten migrieren zu müssen, solange die Knotenkennungen stabil bleiben.
-- 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},
},
},
},
}
Erfahrung erfassen und Stufenaufstiege
Erfahrung sollte auf natürliche Weise durch Gameplay-Aktionen verdient werden und nicht durch sich wiederholende Grind-Schleifen. Erfasse Erfahrung für jede Skilltree-Kategorie separat, damit Mechanikerarbeit Mechaniker-EP gibt, begangene Verbrechen kriminelle EP und so weiter. Lege Erfahrungsschwellen für jede Stufe fest. Überschreitet ein Spieler eine Schwelle, erhält er einen oder mehrere Skillpunkte, die er in diesem Baum ausgeben kann. Verwende abnehmende Erträge bei wiederholten identischen Aktionen, um Exploit-Schleifen zu verhindern. Zum Beispiel könnte die erste Fahrzeugreparatur innerhalb eines 10-Minuten-Zeitraums 50 EP geben, die folgenden Reparaturen dann 25, 10 und 5. So wird abwechslungsreiches Gameplay belohnt, statt sinnloser Wiederholung. Der Server sollte allein darüber entscheiden, wann EP vergeben werden, und darf dem Client niemals eigene Erfahrungswerte abnehmen. Löse EP-Ereignisse serverseitig aus, nachdem du überprüft hast, dass die Aktion tatsächlich stattgefunden hat.
-- 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
Skill-Effekte im Gameplay anwenden
Durch Skill-Effekte wird das System für Spieler greifbar. Jeder freigeschaltete Knoten verändert eine bestimmte Gameplay-Mechanik auf messbare Weise. Implementiere einen zentralen Effekt-Resolver, der eine Spielerkennung und einen Effekttyp entgegennimmt und dann den Gesamtbonus aller freigeschalteten Knoten berechnet, die zu diesem Effekt beitragen. Wenn ein anderes Script den Reparaturgeschwindigkeitsbonus eines Spielers abfragen muss, ruft es diesen Resolver auf und erhält einen Multiplikator zurück. So bleibt dein Skillsystem von anderen Ressourcen entkoppelt. Andere Scripts müssen weder Skilltrees noch Knoten oder Stufen kennen. Sie fragen einfach einen Bonuswert nach Typ ab. Bei freischaltbaren Fähigkeiten wie dem Knacken von Tresoren oder dem Tunen von Turbos gibt der Resolver einen booleschen Wert zurück, der angibt, ob der Spieler den erforderlichen Knoten freigeschaltet hat. Mit dieser Architektur lassen sich Skills mit minimalen Codeänderungen in bestehende Scripts integrieren.
-- 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)
Den Skilltree als NUI umsetzen
Die visuelle Gestaltung deines Skilltrees wirkt sich direkt darauf aus, wie sehr sich Spieler für das Fortschrittssystem interessieren. Erstelle ein NUI-Panel, das jeden Baum als verzweigtes Diagramm mit durch Linien verbundenen Knoten darstellt, die Voraussetzungen sichtbar machen. Freigeschaltete Knoten sollten farbig leuchten oder pulsieren, gesperrte, aber verfügbare Knoten sollten leicht abgedunkelt erscheinen und Knoten mit noch nicht erfüllten Voraussetzungen ausgegraut sein. Zeige die aktuelle Stufe des Spielers, den EP-Fortschrittsbalken und verfügbare Skillpunkte gut sichtbar oben an. Klickt ein Spieler auf einen Knoten, zeige seine Beschreibung, die aktuelle Stufe im Verhältnis zur Höchststufe, die Kosten für die Verbesserung und den Bonus pro Stufe an. Baue eine Bestätigung ein, bevor Punkte ausgegeben werden, da Spieler ihre Builds möglicherweise sorgfältig planen möchten. Verwende flüssige CSS-Übergänge für Zustandsänderungen, damit sich eine Verbesserung durch ein kurzes Aufleuchten oder eine Skalierungsanimation belohnend anfühlt. Ziehe eine Zurücksetzen-Option in Betracht, kostenlos oder gegen Spielwährung, damit Spieler ihre Builds neu verteilen können, wenn sich die Meta ändert oder sie einen anderen Spielstil ausprobieren möchten.
Balancing und Maßnahmen gegen Exploits
Das Balancing ist der schwierigste Teil jedes Fortschrittssystems. Sind die Boni zu stark, werden Spieler mit hoher Stufe unbesiegbar und neue Spieler verlieren jede Hoffnung. Sind die Boni zu schwach, interessiert sich niemand für den Fortschritt. Beginne vorsichtig mit Boni von 5 bis 15 Prozent pro Skill auf der Höchststufe und berücksichtige das Feedback der Spieler. Validiere jede Skillpunktverteilung serverseitig. Wenn ein Spieler eine Verbesserung für einen Knoten anfordert, prüfe, ob er genug Punkte hat, ob alle Voraussetzungen erfüllt sind und ob der Knoten noch nicht auf der Höchststufe ist. Vertraue bei diesen Prüfungen niemals dem Clientstatus. Protokolliere alle Skillpunkt-Transaktionen, damit du Exploits erkennen und rückgängig machen kannst. Begrenze die Häufigkeit von Anfragen zur Skillverteilung, um schnelle Exploit-Versuche zu verhindern. Ziehe für kompetitive Szenarien Skilllimits in Betracht, die beschränken, in wie viele Bäume ein einzelner Charakter investieren kann. So müssen Spieler sinnvolle Entscheidungen treffen, statt alles maximieren zu können. Das sorgt für vielfältige Charakter-Builds und fördert die Zusammenarbeit zwischen Spielern mit sich ergänzenden Skills.
Datenbankschema und Migrationsstrategie
Entwirf dein Datenbankschema so, dass es sowohl aktuelle Skilldaten als auch zukünftige Erweiterungen der Bäume problemlos unterstützt. Speichere Skillverteilungen von Spielern als einzelne Zeilen pro Baum und Charakter statt als einzelnes JSON-Objekt. So kannst du gezielt nach Skills suchen und Daten für Analysen auf Datenbankebene aggregieren. Füge Spalten für den Baumnamen, die aktuelle Stufe, die gesamten EP, verfügbare Punkte und eine JSON-Spalte für die Knotenverteilungen innerhalb des jeweiligen Baums hinzu. Wenn du in zukünftigen Updates neue Skilltrees oder Knoten hinzufügst, bleiben bestehende Spielerdaten intakt, da neue Knoten einfach auf Stufe null beginnen. Benennst oder entfernst du einen Knoten, berücksichtige die Änderung bei der Migration, indem du die ausgegebenen Punkte dem verfügbaren Punktepool des Spielers für diesen Baum zurückerstattest. Versioniere deine Baumdefinitionen, damit die Migrationslogik weiß, welche Änderungen anzuwenden sind. Mit dieser vorausschauenden Planung ersparst du dir aufwendige Datenmigrationen, während sich dein Skillsystem über Monate hinweg weiterentwickelt.
-- 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)
);