Zurück zum Blog
Guide8 Min. Lesezeit

FiveM-Inventarverwaltung: Profi-Tipps für Serverbetreiber

Optimiere dein FiveM-Inventar. Gewichtseinstellungen, Stapelgrößen, Gegenstandsbilder, Lagergestaltung und Tipps erfolgreicher Rollenspielserver.

Agency Scripts

Gründer und leitender Entwickler bei Agency Scripts

Warum die Inventararchitektur wichtig ist

Das Inventarsystem ist das Rückgrat jedes FiveM-Rollenspielservers. Jede Interaktion eines Spielers mit Gegenständen, vom Aufheben einer Waffe bis zum Übergeben eines Schlüssels, läuft über das Inventar. Ein schlecht konzipiertes Inventar führt zu Exploits zur Duplizierung von Gegenständen, Desync-Problemen und frustrierten Spielern, die ihre Ausrüstung verlieren. Die robustesten Inventarsysteme folgen einem serverautoritativen Modell: Der Client zeigt nur an, was der Server vorgibt, und wird niemals dazu verwendet, den eigenen Status zu melden. Allein diese Architekturentscheidung verhindert die meisten Duplizierungs-Exploits, die Server mit clientseitig vertrauenswürdigen Inventaren plagen. Plane bei der Entwicklung deines Inventars zuerst den Datenfluss: Der Server kennt den tatsächlichen Zustand, der Client stellt ihn dar und jede Änderung läuft über ein validiertes Server-Event.

Gewichts- oder Slot-Systeme

Die Wahl zwischen einem gewichts- und einem slotbasierten Inventarsystem prägt das Spielerlebnis grundlegend. Bei einem slotbasierten System enthält jeder Slot einen Gegenstandstyp mit einer maximalen Stapelgröße. Die Gesamtzahl der Slots legt die Tragfähigkeit fest. Bei einem gewichtsbasierte System hat jeder Gegenstand ein Gewicht und der Spieler ein maximales Traggewicht. Viele moderne Frameworks kombinieren beide Ansätze: Sie nutzen Slots zur Organisation, begrenzen die Gesamtkapazität aber anhand des Gewichts. Hier ist ein Beispiel für eine kombinierte Gegenstandsdefinition mit Gewicht und Slots:

-- Shared item definitions (items.lua)
QBCore.Shared.Items = {
    ['water_bottle'] = {
        name = 'water_bottle',
        label = 'Water Bottle',
        weight = 500,        -- grams
        type = 'item',
        image = 'water_bottle.png',
        unique = false,
        useable = true,
        shouldClose = true,
        description = 'A refreshing bottle of water',
        stackSize = 10,      -- max per slot
    },
    ['lockpick'] = {
        name = 'lockpick',
        label = 'Lockpick',
        weight = 200,
        type = 'item',
        image = 'lockpick.png',
        unique = false,
        useable = true,
        shouldClose = true,
        description = 'Used to pick locks',
        stackSize = 5,
    },
}

Die weight Das Feld wird zur präzisen Angabe in Gramm gespeichert, und der Wert stackSize legt fest, wie viele Exemplare dieses Gegenstands in einen Slot passen. Wenn ein Spieler versucht, einen Gegenstand aufzuheben, prüfst du sowohl, ob ein Slot frei ist, als auch, ob das Gesamtgewicht das Maximum überschreiten würde. Diese doppelte Prüfung verhindert, dass Spieler unrealistische Mengen schwerer Gegenstände tragen, selbst wenn noch Slots frei sind.

Serverseitige Validierung und Exploit-Schutz

Jede Inventaraktion muss auf dem Server validiert werden, bevor sie ausgeführt wird. Wenn ein Spieler einen Gegenstand von Slot 3 nach Slot 7 verschiebt, sendet der Client eine Verschiebeanfrage. Der Server überprüft, ob sich der Gegenstand tatsächlich im Quellslot befindet, ob der Zielslot ihn aufnehmen kann und ob die Mengenangaben übereinstimmen. Lass den Client niemals die Anzahl von Gegenständen festlegen oder Gegenstände aus dem Nichts erstellen. Hier ist ein sicherer serverseitiger Handler zum Verschieben:

RegisterNetEvent('inventory:server:moveItem', function(fromSlot, toSlot, fromAmount)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local fromItem = Player.PlayerData.items[fromSlot]
    if not fromItem then
        -- Source slot is empty, possible exploit attempt
        DropPlayer(src, 'Invalid inventory operation')
        return
    end

    if fromAmount > fromItem.amount or fromAmount < 1 then
        DropPlayer(src, 'Invalid inventory amount')
        return
    end

    local toItem = Player.PlayerData.items[toSlot]

    if toItem and toItem.name == fromItem.name and not fromItem.unique then
        -- Stack items together
        local maxStack = QBCore.Shared.Items[fromItem.name].stackSize or 50
        local canStack = maxStack - toItem.amount
        local moveAmount = math.min(fromAmount, canStack)

        if moveAmount > 0 then
            toItem.amount = toItem.amount + moveAmount
            fromItem.amount = fromItem.amount - moveAmount
            if fromItem.amount <= 0 then
                Player.PlayerData.items[fromSlot] = nil
            end
        end
    else
        -- Swap items between slots
        Player.PlayerData.items[toSlot] = fromItem
        Player.PlayerData.items[fromSlot] = toItem
    end

    Player.Functions.SetPlayerData('items', Player.PlayerData.items)
end)

Beachte die DropPlayer Erkennt eindeutig unmögliche Aktionen. Wenn du diese Ereignisse in einer separaten Audit-Tabelle protokollierst, kannst du Exploit-Versuche und Muster identifizieren. Erwäge außerdem, Inventar-Events zu begrenzen, da legitime Spieler selten mehr als ein paar Inventaraktionen pro Sekunde ausführen, während automatisierte Exploits oft schnell Hunderte Anfragen senden.

Gegenstandsmetadaten und einzigartige Gegenstände

Metadaten machen aus einfachen Gegenständen komplexe, einzigartige Objekte. Eine Waffe kann ihre Seriennummer, Haltbarkeit und angebrachte Modifikationen enthalten. Ein Telefon kann die zugewiesene Nummer und einen Verweis auf die Kontaktliste speichern. Lebensmittel können einen Ablaufzeitstempel haben. Metadaten werden als Lua-Tabelle gespeichert, in der Datenbank zu JSON serialisiert und an die jeweilige Gegenstandsinstanz angehängt. Der entscheidende Unterschied besteht zwischen stapelbaren Gegenständen, die dieselben Metadaten teilen, und einzigartigen Gegenständen, die jeweils eine eigene Metadatentabelle erhalten. So erstellst du eine Waffe mit vollständigen Metadaten:

-- Creating a weapon with metadata
function CreateWeaponItem(src, weaponName, serial)
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return false end

    local metadata = {
        serial = serial or GenerateSerial(),
        durability = 100.0,
        ammo = 0,
        attachments = {},
        registered = false,
        registeredTo = nil,
        quality = math.random(85, 100),
        created = os.time(),
    }

    return Player.Functions.AddItem(weaponName, 1, nil, metadata)
end

function GenerateSerial()
    local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    local serial = ''
    for i = 1, 10 do
        local idx = math.random(1, #chars)
        serial = serial .. chars:sub(idx, idx)
    end
    return serial
end

Wenn du Gegenstände mit Metadaten in der NUI anzeigst, übergib die Metadaten zusammen mit den Gegenstandsinformationen, damit die Benutzeroberfläche Details wie Haltbarkeitsbalken, Seriennummern und Qualitätsbewertungen darstellen kann. So entsteht eine stärkere Bindung zwischen Spielern und ihren Gegenständen und fortgeschrittene Rollenspielszenarien wie Waffenregistrierungssysteme oder forensische Ermittlungen, bei denen eine Seriennummer zu ihrem Besitzer zurückverfolgt wird, werden möglich.

Performance von Drag-and-Drop in der NUI

Die Inventar-Benutzeroberfläche gehört zu den performancekritischsten NUI-Elementen, weil Spieler ständig damit interagieren. Vermeide es, bei jeder Aktualisierung das gesamte Inventarraster neu zu rendern. Verwende stattdessen einen Virtual-DOM-Ansatz oder gezielte Elementaktualisierungen, die nur geänderte Slots anpassen. Wenn ein Spieler einen Gegenstand zieht, verarbeite den Drag-Vorgang vollständig in JavaScript mit Maus-Events, anstatt währenddessen Positionsaktualisierungen an den Lua-Client zu senden. Sende nur das endgültige Ablageergebnis als einzelnen NUI-Callback. Hier ist ein performantes Muster für einen Drag-Handler:

// Inventory NUI - performant drag and drop
let draggedItem = null;
let dragElement = null;

document.addEventListener('mousedown', (e) => {
    const slot = e.target.closest('.inv-slot[data-has-item="true"]');
    if (!slot) return;

    draggedItem = {
        slot: parseInt(slot.dataset.slot),
        item: JSON.parse(slot.dataset.itemInfo),
    };

    dragElement = slot.cloneNode(true);
    dragElement.classList.add('dragging-ghost');
    dragElement.style.position = 'fixed';
    dragElement.style.pointerEvents = 'none';
    dragElement.style.zIndex = '9999';
    document.body.appendChild(dragElement);

    moveDragElement(e.clientX, e.clientY);
});

document.addEventListener('mousemove', (e) => {
    if (!dragElement) return;
    moveDragElement(e.clientX, e.clientY);
});

document.addEventListener('mouseup', (e) => {
    if (!draggedItem) return;

    const targetSlot = e.target.closest('.inv-slot');
    if (targetSlot) {
        const toSlot = parseInt(targetSlot.dataset.slot);
        // Send only the final result to Lua
        fetch(`https://${GetParentResourceName()}/moveItem`, {
            method: 'POST',
            body: JSON.stringify({
                fromSlot: draggedItem.slot,
                toSlot: toSlot,
                amount: draggedItem.item.amount,
            }),
        });
    }

    if (dragElement) dragElement.remove();
    draggedItem = null;
    dragElement = null;
});

Verwende für die visuelle Darstellung CSS Grid für das Slot-Layout und vermeide aufwendige CSS-Animationen für Inventargegenstände, da Spieler möglicherweise Dutzende Slots gleichzeitig sehen. Bild-Sprites für Gegenstandssymbole laden schneller als einzelne Bilddateien und reduzieren HTTP-Anfragen beim ersten Öffnen der NUI.

Lager- und Containersysteme

Neben dem persönlichen Inventar benötigen Spieler Zugriff auf externe Lager, etwa Fahrzeugkofferräume, Hauslager und gemeinsam genutzte Organisationslager. Jeder Containertyp sollte eigene Kapazitätsgrenzen und Zugriffsregeln haben. Fahrzeugkofferräume verwenden das Kennzeichen als eindeutige ID, Hauslager verwenden Immobilien-IDs und Joblager kombinieren den Jobnamen mit einer Rangprüfung. Speichere Containerinventare in einer eigenen Datenbanktabelle getrennt von Spielerinventaren, damit Abfragen effizient bleiben:

CREATE TABLE IF NOT EXISTS stash_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    stash_id VARCHAR(100) NOT NULL,
    slot INT NOT NULL,
    item_name VARCHAR(50) NOT NULL,
    amount INT DEFAULT 1,
    metadata LONGTEXT DEFAULT '{}',
    UNIQUE KEY unique_stash_slot (stash_id, slot),
    INDEX idx_stash_id (stash_id)
);

-- Example stash_id values:
-- 'trunk_ABC123'        (vehicle trunk by plate)
-- 'house_42'            (house stash by property id)
-- 'police_evidence_1'   (job stash with identifier)

Wenn ein Spieler einen Container öffnet, lade dessen Inhalt aus der Datenbank und sperre ihn, damit nicht mehrere Spieler gleichzeitig darauf zugreifen können. Verwende eine serverseitige Sperrtabelle, in der erfasst wird, welche Lager-IDs gerade geöffnet sind und von wem. Hebe die Sperre auf, wenn der Spieler den Container schließt oder die Verbindung trennt. So verhinderst du den klassischen Duplizierungs-Exploit, bei dem zwei Spieler denselben Kofferraum öffnen und beide dieselben Gegenstände entnehmen.

Inventarsynchronisierung und Persistenz

Damit die Inventardaten im Serverspeicher, in der Datenbank und in der Clientanzeige synchron bleiben, ist eine sorgfältige Abstimmung nötig. Speichere das Spielerinventar regelmäßig in der Datenbank, nicht nach jeder einzelnen Änderung, um die Datenbanklast durch Schreibvorgänge zu reduzieren. Für die meisten Server eignet sich ein Speicherintervall von 30 bis 60 Sekunden. Speichere außerdem immer beim Trennen der Spielerverbindung und beim Herunterfahren des Servers, indem du das playerDropped Ereignis und einen Shutdown-Handler verwendest. Implementiere ein Dirty-Flag-System, das nur dann in die Datenbank schreibt, wenn sich das Inventar seit dem letzten Speichern tatsächlich geändert hat:

local inventoryDirty = {}

-- Mark inventory as needing save
function MarkDirty(citizenid)
    inventoryDirty[citizenid] = true
end

-- Periodic save loop
CreateThread(function()
    while true do
        Wait(30000) -- 30 seconds
        for citizenid, dirty in pairs(inventoryDirty) do
            if dirty then
                local Player = QBCore.Functions.GetPlayerByCitizenId(citizenid)
                if Player then
                    SaveInventoryToDatabase(citizenid, Player.PlayerData.items)
                end
                inventoryDirty[citizenid] = nil
            end
        end
    end
end)

AddEventHandler('playerDropped', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if Player then
        local citizenid = Player.PlayerData.citizenid
        if inventoryDirty[citizenid] then
            SaveInventoryToDatabase(citizenid, Player.PlayerData.items)
            inventoryDirty[citizenid] = nil
        end
    end
end)

Fasse auf der Clientseite NUI-Aktualisierungen zusammen, damit mehrere schnelle Inventaränderungen, etwa der Erhalt mehrerer Gegenstände beim Crafting, eine einzige UI-Aktualisierung auslösen und nicht für jeden Gegenstand eine eigene. So verhinderst du das Flackern, das Spieler sehen, wenn Gegenstände einzeln in ihrem Inventarraster erscheinen.

Performanceüberwachung und Optimierung

Überwache die Performance deines Inventarsystems anhand wichtiger Messwerte: durchschnittliche Dauer von Datenbankspeicherungen, Ereignisverarbeitungsrate und NUI-Renderfrequenz. Verwende den FiveM-Profiler, um Engpässe in deinen Inventar-Callbacks zu finden. Häufige Performance-Fallen sind das Durchlaufen aller Spielerinventare in jedem Frame für distanzabhängige Funktionen wie das Aufheben von Gegenständen am Boden, das unnötige Serialisieren großer Metadatenobjekte und aktive NUI-Frames, obwohl das Inventar geschlossen ist. Verwende für Gegenstände am Boden ein räumliches Rastersystem, das nur Gegenstände in der Zelle des Spielers überprüft, statt jeden abgelegten Gegenstand auf dem Server zu durchsuchen. Cache die Gegenstandsdefinitionen in einer Nachschlagetabelle, die nach Gegenstandsname indiziert ist, damit Eigenschaften mit einer O(1)-Hash-Abfrage statt einer O(n)-Array-Suche abgerufen werden. Ziehe außerdem eine Inventarpaginierung für Container mit sehr großer Kapazität in Betracht. Lade dabei nur die sichtbaren Slots und rufe weitere Zeilen ab, wenn der Spieler scrollt. So bleiben sowohl Datenbankabfragen als auch NUI-Rendering selbst bei Lagerhäusern mit Hunderten Slots ressourcenschonend.

Bereit loszulegen?

Hol dir die Scripts in unserem Shop, oder komm auf den Discord für Support, Updates und einen Blick auf das, was kommt.