Wróć do bloga
Guide8 min czytania

System drzewka umiejętności w FiveM: dobrze zaprojektowana progresja poziomów

Dodaj drzewko umiejętności do swojego serwera FiveM. XP, odblokowania, projekt UI, powiązania z pracami i najlepsze skrypty umiejętności, które zapewnią graczom wartościowe cele długoterminowe.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Dlaczego drzewka umiejętności odmieniają serwery Roleplay

Większość serwerów roleplay FiveM opiera się na progresji powiązanej z pracą, gdzie gracz wybiera zawód i od razu otrzymuje dostęp do wszystkich narzędzi oraz umiejętności. Choć to działa, takie podejście nie daje poczucia rozwoju ani długoterminowego zaangażowania. Drzewka umiejętności całkowicie zmieniają tę dynamikę. Wprowadzając progresję w stylu RPG, gracze zdobywają doświadczenie za swoje działania i wydają punkty umiejętności, aby odblokowywać zdolności, perki oraz bonusy pasywne w rozgałęzionych drzewkach talentów. Mechanik, który naprawia setki aut, stopniowo staje się szybszy i bardziej wydajny. Przestępca wielokrotnie otwierający wytrychem zamki nabiera w tym coraz większej wprawy. Tworzy to wartościowy rozwój postaci, który nagradza zaangażowanie i sprawia, że każda postać jest unikalna. Kluczem jest zaprojektowanie systemu wzbogacającego roleplay, zamiast zamieniać serwer w grind rodem z gier MMO, z zachowaniem na tyle subtelnych bonusów, by różnice w umiejętnościach uzupełniały scenariusze RP, a nie dominowały nad nimi.

Projektowanie struktury danych drzewka umiejętności

Dobrze zaprojektowane drzewko umiejętności zaczyna się od przejrzystej struktury danych. Każde drzewko umiejętności należy do kategorii takiej jak walka, rzemiosło, prowadzenie pojazdów, medycyna czy przestępczość. Wewnątrz każdego drzewka poszczególne węzły reprezentują umiejętności, które można odblokować. Każdy węzeł ma unikalny identyfikator, wyświetlaną nazwę, opis, maksymalny poziom, koszt punktowy za poziom, węzły wymagane do wcześniejszego odblokowania oraz rzeczywisty efekt w rozgrywce. Definicje drzewka przechowuj we wspólnym pliku konfiguracyjnym, aby zarówno serwer, jak i klient mogły się do nich odwoływać. Postęp gracza to oddzielna struktura danych śledząca odblokowane węzły i ich poziomy, a także dostępne, niewydane punkty umiejętności. Oddzielenie definicji drzewka od postępów gracza pozwala na aktualizację lub rebalans drzewka bez konieczności migracji danych graczy, o ile identyfikatory węzłów pozostają niezmienne.

-- 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},
            },
        },
    },
}

Śledzenie doświadczenia i zdobywanie poziomów

Doświadczenie powinno być zdobywane organicznie poprzez działania w rozgrywce, a nie przez powtarzalny grind. Śledź doświadczenie dla poszczególnych kategorii drzewka umiejętności, aby wykonywanie pracy mechanika dawało XP mechanika, popełnianie przestępstw dawało XP przestępcy i tak dalej. Zdefiniuj progi doświadczenia dla każdego poziomu, a gdy gracz przekroczy próg, otrzymuje jeden lub więcej punktów umiejętności do wydania w tym drzewku. Zastosuj mechanizm malejących korzyści przy powtarzaniu identycznych czynności, aby zniechęcić do pętli exploitów. Na przykład pierwsza naprawa pojazdu w 10-minutowym oknie czasowym może dawać 50 XP, ale kolejne naprawy 25, potem 10, a potem 5. Nagradza to urozmaiconą rozgrywkę zamiast bezmyślnego powtarzania. Serwer powinien być jedynym autorytetem w przyznawaniu XP i nigdy nie ufać klientowi w kwestii zgłaszania własnych wartości doświadczenia. Wywołuj eventy XP z poziomu logiki serwera po zweryfikowaniu, że czynność rzeczywiście miała miejsce.

-- 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

Stosowanie efektów umiejętności w rozgrywce

Efekty umiejętności to miejsce, w którym system staje się namacalny dla graczy. Każdy odblokowany węzeł modyfikuje konkretną mechanikę rozgrywki w mierzalny sposób. Zaimplementuj centralny resolver efektów, który przyjmuje identyfikator gracza oraz typ efektu, a następnie oblicza łączny bonus ze wszystkich odblokowanych węzłów wpływających na ten efekt. Gdy inny skrypt potrzebuje poznać bonus gracza do prędkości naprawy, wywołuje ten resolver i otrzymuje mnożnik. Dzięki temu Twój system umiejętności pozostaje odseparowany od innych zasobów. Inne skrypty nie muszą wiedzieć nic o drzewkach umiejętności, węzłach czy poziomach; pytają po prostu o wartość bonusu według typu. W przypadku umiejętności do odblokowania, takich jak otwieranie sejfów czy tuning turbo, resolver zwraca wartość logiczną informującą, czy gracz posiada wymagany węzeł. Taka architektura ułatwia integrację umiejętności z istniejącymi skryptami przy minimalnych zmianach w ich kodzie.

-- 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)

Tworzenie NUI drzewka umiejętności

Wizualna prezentacja drzewka umiejętności bezpośrednio wpływa na zaangażowanie graczy w system progresji. Zbuduj panel NUI, który wyświetla każde drzewko jako rozgałęziony graf z węzłami połączonymi liniami pokazującymi wymagania wstępne. Odblokowane węzły powinny świecić lub pulsować kolorem, zablokowane, ale dostępne powinny być lekko przyciemnione, a węzły z niespełnionymi wymaganiami poszarzałe. Na samej górze wyraźnie pokazuj aktualny poziom gracza, pasek postępu XP oraz dostępne punkty umiejętności. Gdy gracz kliknie węzeł, wyświetl jego opis, aktualny poziom w stosunku do maksymalnego, koszt ulepszenia i premię do efektu na poziom. Dodaj krok potwierdzenia przed wydaniem punktów, ponieważ gracze mogą chcieć starannie zaplanować swoje buildy. Użyj płynnych przejść CSS dla zmian stanów, aby ulepszenie węzła dawało satysfakcję dzięki krótkiemu błyskowi lub animacji skalowania. Rozważ dodanie opcji resetu, darmowej lub płatnej w walucie z gry, aby gracze mogli zresetować punkty, jeśli meta się zmieni lub zechcą wypróbować inny styl gry.

Kwestie balansu i ochrony przed exploitami

Balans to najtrudniejsza część każdego systemu progresji. Jeśli bonusy są zbyt silne, gracze na wysokim poziomie stają się nietykalni, a nowi gracze czują się bezradni. Jeśli bonusy są zbyt słabe, nikt nie dba o progresję. Zacznij ostrożnie od 5 do 15 procent bonusu na umiejętność na maksymalnym poziomie i monitoruj opinie graczy. Wdróż walidację po stronie serwera dla każdego przydziału punktów umiejętności. Gdy gracz żąda ulepszenia węzła, sprawdź, czy ma wystarczająco dużo punktów, czy wszystkie wymagania wstępne są spełnione i czy węzeł nie osiągnął już maksymalnego poziomu. Nigdy nie ufaj stanowi klienta przy tych weryfikacjach. Dodaj logowanie wszystkich transakcji punktów umiejętności, aby móc wykrywać i cofać exploity. Ograniczaj częstotliwość zapytań o przydział umiejętności, aby zapobiec próbom exploitów z szybkim spamowaniem. W scenariuszach rywalizacyjnych rozważ limity umiejętności (skill caps), które ograniczają liczbę drzewek, w które jedna postać może zainwestować, wymuszając istotne wybory zamiast pozwalać jednemu graczowi na wymaksowanie wszystkiego. Tworzy to różnorodne buildy postaci i zachęca do współpracy między graczami o uzupełniających się umiejętnościach.

Schemat bazy danych i strategia migracji

Zaprojektuj schemat bazy danych tak, aby bez problemu obsługiwał zarówno bieżące dane umiejętności, jak i przyszłe rozszerzenia drzewek. Przechowuj przydziały umiejętności gracza jako pojedyncze wiersze na drzewko dla każdej postaci, zamiast pojedynczego blobu JSON. Zapewnia to wydajne odpytywanie o konkretne umiejętności i umożliwia agregację na poziomie bazy danych do celów analitycznych. Uwzględnij kolumny na nazwę drzewka, aktualny poziom, łączną liczbę XP, dostępne punkty oraz kolumnę JSON na przydziały węzłów w ramach tego drzewka. Gdy dodasz nowe drzewka umiejętności lub węzły w przyszłych aktualizacjach, istniejące dane graczy pozostaną nienaruszone, ponieważ nowe węzły po prostu startują od poziomu zero. Jeśli zmienisz nazwę węzła lub go usuniesz, przeprowadź migrację, zwracając wydane punkty do dostępnej puli gracza dla danego drzewka. Wersjonuj definicje drzewek, aby logika migracji wiedziała, które zmiany zastosować. To przyszłościowe podejście uchroni cię przed uciążliwymi migracjami danych w miarę rozwoju systemu umiejętności przez kolejne miesiące działania serwera.

-- 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)
);

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.