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.