FiveM Custom HUD erstellen: NUI Tutorial von Grund auf (2026)
Lerne, wie du ein eigenes FiveM HUD mit NUI von Grund auf baust. Vollständige Code-Beispiele für HTML, CSS, Lua und JavaScript — inklusive Tacho, Hunger/Durst, Framework-Integration und Performance-Optimierung.

Jeder FiveM-Server hat ein HUD. Die meisten nutzen dieselben vorgefertigten Scripts — qb-hud, ps-hud oder was auch immer mit dem Framework mitgeliefert wurde. Wer sein eigenes HUD von Grund auf mit NUI baut, bekommt etwas, das kein fertiges Script bieten kann: vollständige Kontrolle über jedes Pixel, jede Animation und jeden Datenpunkt auf dem Bildschirm. Diese Anleitung führt dich durch den gesamten Prozess — von einem leeren Ordner bis zu einem vollständig funktionsfähigen HUD mit Gesundheit, Rüstung, Tacho und Framework-integriertem Hunger und Durst. Alle Code-Beispiele sind vollständig und direkt verwendbar.
Den FiveM HUD Vergleich empfehle ich, wenn du erst prüfen willst, ob fertige Optionen für deinen Server ausreichen, bevor du selbst baust.
Warum ein eigenes HUD bauen?
Bevor wir in den Code einsteigen, lohnt es sich zu verstehen, was der Eigenbau gegenüber der Installation eines fertigen HUDs tatsächlich bringt.
Volle Design-Kontrolle. Fertige HUDs haben festgelegte Layouts, Farbschemata und Elementgrößen. Sie anzupassen bedeutet, gegen fremdes CSS und fremde Lua-Logik anzukämpfen. Wenn du dein eigenes baust, fängst du mit einem leeren Blatt an — du entscheidest, wo alles sitzt, wie es aussieht und wie es animiert wird.
Performance-Optimierung. Du schreibst genau die Update-Schleifen, die du brauchst — und nichts weiter. Kein toter Code von Features, die du nicht nutzt, keine versteckten Intervalle, die Daten abrufen, die dein Server gar nicht trackt. Ein selbst geschriebenes HUD kann schlanker sein als jedes fertige Script.
Einzigartige Server-Identität. Dein HUD ist eines der ersten Dinge, die ein neuer Spieler sieht. Ein unverwechselbares, sorgfältig gestaltetes UI signalisiert, dass dein Server Wert auf Qualität legt. Spieler erinnern sich an Server mit einer konsistenten visuellen Identität.
NUI wirklich verstehen. Das NUI-System zu beherrschen ist grundlegend für jeden FiveM-Entwickler. Phone-Apps, Menüs, Loading Screens, Radial Menüs — sie alle nutzen dieselbe Architektur. Wer sie durch ein HUD-Projekt versteht, öffnet sich den Weg zu allem anderen.
Voraussetzungen
Du brauchst einen laufenden FiveM-Server. Darüber hinaus:
- VS Code — oder ein anderer Editor, aber VS Code mit den Lua- und HTML-Erweiterungen bietet Syntax-Highlighting und Autovervollständigung für beide Seiten des Stacks.
- Grundkenntnisse in Lua — du musst Variablen, Funktionen und Tables verstehen. Du musst kein Lua-Experte sein.
- HTML-, CSS- und JavaScript-Grundlagen — die NUI-Ebene ist eine Webseite. Wenn du eine einfache statische Seite bauen kannst, reicht das aus.
- Einen Testserver, den du schnell neustarten kannst — während der Entwicklung wirst du die Resource häufig neustarten.
NUI verstehen
NUI steht für Network User Interface. Es ist das System, das FiveM verwendet, um Web-Inhalte über das Spiel zu rendern. FiveM bettet einen Chromium-Browser (CEF — Chromium Embedded Framework) ein, der HTML, CSS und JavaScript parallel zum Spiel rendert. Dein HUD ist buchstäblich eine Webseite, die über GTA V gezeichnet wird.
Der Kommunikationsfluss funktioniert so:
Lua zu JavaScript: Dein clientseitiges Lua-Script liest Spieldaten (Gesundheit, Geschwindigkeit, Hungerwerte) und schickt sie mit SendNUIMessage an die NUI-Ebene. Das löst im Browser ein message-Event aus, und dein JavaScript empfängt die Daten als einfaches JavaScript-Objekt.
JavaScript zu Lua: Wenn das NUI etwas zurück ans Spiel senden muss (zum Beispiel wenn ein Spieler ein Menü schließt), ruft JavaScript fetch('https://resource-name/callback-name', { method: 'POST', body: JSON.stringify(data) }) auf. Das löst auf der Lua-Seite einen registrierten NUI-Callback aus.
Für ein HUD nutzt du hauptsächlich die Lua-zu-JavaScript-Kommunikation — der Spielzustand fließt in den Browser und aktualisiert das DOM. Das HUD muss kaum etwas zurück an Lua schicken.
Ein wichtiger Unterschied zum regulären Browser: NUI ist kein normaler Browser-Tab. Es kann keine beliebigen Netzwerkanfragen ins Internet stellen. Es läuft im Kontext der Resource und kommuniziert mit dem Spiel über das oben beschriebene fetch-Protokoll. Deshalb solltest du externe Bibliotheken lokal einbinden statt von einem CDN zu laden.
Projektstruktur aufsetzen
Erstelle einen Ordner im resources-Verzeichnis deines FiveM-Servers. Nenne ihn mein-hud. Die Ordnerstruktur, die du aufbaust, sieht so aus:
resources/
└── mein-hud/
├── fxmanifest.lua
├── client.lua
└── nui/
├── index.html
├── style.css
└── script.js
Die fxmanifest.lua ist das Resource-Manifest — sie teilt FiveM mit, welche Dateien zu dieser Resource gehören und wie sie verwendet werden. Hier ist das vollständige Manifest:
fx_version 'cerulean'
game 'gta5'
name 'mein-hud'
description 'Custom NUI HUD'
version '1.0.0'
ui_page 'nui/index.html'
files {
'nui/index.html',
'nui/style.css',
'nui/script.js'
}
client_scripts {
'client.lua'
}
Die ui_page-Direktive zeigt auf die HTML-Datei, die als NUI-Overlay gerendert wird. Der files-Block teilt FiveM mit, welche Dateien in die Resource einbezogen werden, damit sie über die NUI-Ebene zugänglich sind. Füge ensure mein-hud zu deiner server.cfg hinzu und starte den Server neu. Die Resource lädt, aber noch erscheint nichts auf dem Bildschirm — die HTML-Seite existiert, hat aber noch keinen Inhalt.
Schritt 1: HTML-Vorlage
Die HTML-Datei definiert die Struktur des HUDs. Sie muss nicht komplex sein — die DOM-Elemente, die du hier definierst, werden von deinem JavaScript mit Live-Daten befüllt.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HUD</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="hud-container">
<!-- Statusleisten: Gesundheit, Rüstung, Hunger, Durst -->
<div id="status-bars">
<div class="bar-row">
<span class="bar-icon">♥</span>
<div class="bar-track">
<div class="bar-fill" id="health-bar"></div>
</div>
</div>
<div class="bar-row">
<span class="bar-icon">⛊</span>
<div class="bar-track">
<div class="bar-fill" id="armor-bar"></div>
</div>
</div>
<div class="bar-row">
<span class="bar-icon">🍖</span>
<div class="bar-track">
<div class="bar-fill" id="hunger-bar"></div>
</div>
</div>
<div class="bar-row">
<span class="bar-icon">💧</span>
<div class="bar-track">
<div class="bar-fill" id="thirst-bar"></div>
</div>
</div>
</div>
<!-- Tacho: nur im Fahrzeug sichtbar -->
<div id="speedometer" class="hidden">
<div id="speed-value">0</div>
<div id="speed-unit">km/h</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Die Struktur ist bewusst minimal gehalten. Vier Bar-Zeilen übernehmen Gesundheit, Rüstung, Hunger und Durst. Ein separates Tacho-Element zeigt die Fahrzeuggeschwindigkeit und versteckt sich, wenn der Spieler zu Fuß ist. Die hidden-Klasse am Tacho startet es unsichtbar — JavaScript schaltet diese Klasse je nach Fahrzeugstatus um.
Schritt 2: CSS-Styling
Das CSS regelt Layout, Farben und Animationen. Die wichtigste Anforderung für jedes HUD-Overlay: der Hintergrund muss vollständig transparent sein — du legst UI-Elemente über die Spielwelt, ersetzt sie nicht.
/* Reset und Basis */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
width: 100vw;
height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
user-select: none;
}
/* Haupt-Container — unten links verankert */
#hud-container {
position: fixed;
bottom: 40px;
left: 40px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Statusleisten-Block */
#status-bars {
display: flex;
flex-direction: column;
gap: 6px;
}
.bar-row {
display: flex;
align-items: center;
gap: 8px;
}
.bar-icon {
font-size: 13px;
width: 16px;
text-align: center;
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.8));
}
.bar-track {
width: 140px;
height: 8px;
background: rgba(0, 0, 0, 0.45);
border-radius: 4px;
overflow: hidden;
backdrop-filter: blur(2px);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.bar-fill {
height: 100%;
border-radius: 4px;
width: 100%;
transition: width 0.3s ease, background-color 0.4s ease;
}
/* Gesundheitsleiste — Farbkodierung */
#health-bar {
background: linear-gradient(90deg, #e74c3c, #e67e22);
}
/* Rüstungsleiste */
#armor-bar {
background: linear-gradient(90deg, #3498db, #2980b9);
}
/* Hungerleiste */
#hunger-bar {
background: linear-gradient(90deg, #e67e22, #f39c12);
}
/* Durstleiste */
#thirst-bar {
background: linear-gradient(90deg, #00bcd4, #0097a7);
}
/* Kritische Gesundheit — Leiste pulsiert */
#health-bar.critical {
background: #e74c3c;
animation: pulse-critical 0.8s ease-in-out infinite;
}
@keyframes pulse-critical {
0%, 100% { opacity: 1; }
50% { opacity: 0.55; }
}
/* Tacho */
#speedometer {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 10px 18px;
backdrop-filter: blur(4px);
min-width: 90px;
transition: opacity 0.3s ease;
}
#speed-value {
font-size: 32px;
font-weight: 700;
color: #fff;
line-height: 1;
text-shadow: 0 0 12px rgba(255, 255, 255, 0.3);
}
#speed-unit {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 2px;
}
/* Hilfsmittel */
.hidden {
display: none !important;
}
Ein paar Details sind besonders wichtig. Der body-Hintergrund ist transparent — das ist für jedes HUD-Overlay unverzichtbar. user-select: none verhindert Textauswahl, die im Spiel seltsam aussehen würde. Die Bar-Füllungen verwenden CSS transition für sanfte Updates — wenn JavaScript die Breite ändert, animiert sie sich statt zu springen. Die kritische Pulsanimation bei niedrigem Leben wird ausgelöst, wenn JavaScript dem Gesundheits-Bar die critical-Klasse hinzufügt.
Schritt 3: Client-Lua-Script
Das Lua-Client-Script erledigt die schwere Arbeit: Spielzustand in einem Timer lesen und an die NUI-Ebene weiterleiten. Das Script führt alle 200 Millisekunden eine Schleife aus, liest Gesundheit, Rüstung und Fahrzeugdaten und ruft SendNUIMessage auf, um die Daten an JavaScript weiterzuleiten.
-- client.lua
local isHudVisible = true
local lastHealth = -1
local lastArmor = -1
local lastSpeed = -1
local lastHunger = -1
local lastThirst = -1
-- Native GTA HUD-Komponenten ausblenden, die wir ersetzen
local function nativeHudAusblenden()
-- Gesundheitsleiste ausblenden (Komponente 1)
HideHudComponentThisFrame(1)
-- Rüstungsleiste ausblenden (Komponente 2)
HideHudComponentThisFrame(2)
-- Fahrzeugname ausblenden (Komponente 6)
HideHudComponentThisFrame(6)
}
-- GTA-Gesundheit (0-200) in Prozent (0-100) umrechnen
-- GTA Baseline ist 100, Maximum ist 200
local function getHealthPercent(ped)
local health = GetEntityHealth(ped)
-- Gesundheit liegt zwischen 100 (am Leben, kein HP) und 200 (volle HP)
local percent = math.max(0, math.min(100, (health - 100)))
return percent
end
-- Haupt-HUD-Aktualisierungsschleife
CreateThread(function()
while true do
Wait(200) -- Alle 200ms aktualisieren, nicht jeden Frame
if isHudVisible then
local ped = PlayerPedId()
local imFahrzeug = IsPedInAnyVehicle(ped, false)
-- Gesundheit und Rüstung lesen
local health = getHealthPercent(ped)
local armor = GetPedArmour(ped) -- Bereits 0-100
-- Geschwindigkeit lesen (nur im Fahrzeug sinnvoll)
local speed = 0
if imFahrzeug then
local vehicle = GetVehiclePedIsIn(ped, false)
-- GetEntitySpeed gibt m/s zurück, mal 3,6 für km/h
speed = math.floor(GetEntitySpeed(vehicle) * 3.6)
end
-- Hunger und Durst lesen (Framework-spezifisch — siehe Schritt 6)
local hunger = getHunger()
local thirst = getThirst()
-- Delta-Prüfung: NUI-Nachricht nur senden, wenn sich Werte geändert haben
local changed = (
health ~= lastHealth or
armor ~= lastArmor or
speed ~= lastSpeed or
hunger ~= lastHunger or
thirst ~= lastThirst
)
if changed then
lastHealth = health
lastArmor = armor
lastSpeed = speed
lastHunger = hunger
lastThirst = thirst
SendNUIMessage({
type = 'updateHud',
health = health,
armor = armor,
speed = speed,
imFahrzeug = imFahrzeug,
hunger = hunger,
thirst = thirst
})
end
end
end
end)
-- Separate enge Schleife nur zum Ausblenden nativer HUD-Elemente
-- Muss jeden Frame laufen, damit die nativen Elemente nicht zurückflackern
CreateThread(function()
while true do
Wait(0)
if isHudVisible then
nativeHudAusblenden()
end
end
end)
-- HUD-Sichtbarkeit umschalten (optionaler Befehl)
RegisterCommand('togglehud', function()
isHudVisible = not isHudVisible
SendNUIMessage({
type = 'toggleHud',
visible = isHudVisible
})
end, false)
Die Delta-Prüfung ist das wichtigste Performance-Detail. Ohne sie wird alle 200ms ein SendNUIMessage-Aufruf abgesetzt, unabhängig davon, ob sich etwas geändert hat. Mit ihr werden JavaScript-DOM-Updates nur ausgelöst, wenn die Daten sich tatsächlich geändert haben. Über eine lange Spielsitzung reduziert das die unnötige Arbeit erheblich.
Die zwei separaten Threads — einer alle 200ms für Datenupdates und einer mit Wait(0) für das Ausblenden nativer HUD-Elemente — sind bewusst so gewählt. HideHudComponentThisFrame muss jeden Frame aufgerufen werden, sonst rendert GTA die native Komponente im nächsten Frame wieder. Die 200ms-Schleife kümmert sich um die Daten; die Pro-Frame-Schleife um die Ausblend-Aufrufe.
Schritt 4: JavaScript
Die JavaScript-Datei empfängt Nachrichten von Lua und aktualisiert das DOM entsprechend. Halte diese Datei fokussiert: Daten empfangen, Elemente aktualisieren, sonst nichts.
// script.js
// DOM-Referenzen einmalig cachen — nicht bei jedem Update neu abfragen
const healthBar = document.getElementById('health-bar');
const armorBar = document.getElementById('armor-bar');
const hungerBar = document.getElementById('hunger-bar');
const thirstBar = document.getElementById('thirst-bar');
const speedometer = document.getElementById('speedometer');
const speedValue = document.getElementById('speed-value');
const hudContainer = document.getElementById('hud-container');
/**
* Eine Bar-Fill auf den angegebenen Prozentwert aktualisieren.
* Fügt unterhalb des Schwellenwerts eine 'critical'-Klasse für visuelle Warnung hinzu.
*/
function updateBar(element, percent, criticalThreshold = 25) {
element.style.width = `${Math.max(0, Math.min(100, percent))}%`;
if (percent <= criticalThreshold) {
element.classList.add('critical');
} else {
element.classList.remove('critical');
}
}
/**
* Alle eingehenden NUI-Nachrichten vom Lua-Client-Script verarbeiten.
*/
window.addEventListener('message', (event) => {
const data = event.data;
if (data.type === 'updateHud') {
// Statusleisten aktualisieren
updateBar(healthBar, data.health);
updateBar(armorBar, data.armor, 0); // Rüstung: kein kritischer Zustand
updateBar(hungerBar, data.hunger);
updateBar(thirstBar, data.thirst);
// Tacho-Sichtbarkeit und Wert aktualisieren
if (data.imFahrzeug) {
speedometer.classList.remove('hidden');
speedValue.textContent = data.speed;
} else {
speedometer.classList.add('hidden');
}
}
if (data.type === 'toggleHud') {
hudContainer.style.display = data.visible ? 'flex' : 'none';
}
});
Das Cachen von DOM-Referenzen am Anfang ist entscheidend. document.getElementById ist nicht teuer, aber bei jedem 200ms-Update summiert sich das über eine lange Spielsitzung. Referenzen einmalig beim Laden der Seite cachen und überall wiederverwenden.
Die updateBar-Funktion klemmt den Wert zwischen 0 und 100, bevor sie ihn als Breitenangabe in Prozent anwendet. Das verhindert Layout-Probleme, wenn ein Framework einen Wert leicht außerhalb des Bereichs sendet.
Schritt 5: Tacho
Der Tacho ist bereits im obigen Code integriert, aber hier ein genauerer Blick auf die Geschwindigkeitsberechnung und wie du bei Bedarf sowohl km/h als auch mph anzeigen kannst.
-- In client.lua, erweiterter Fahrzeugdaten-Abschnitt
local function getFahrzeugDaten(ped)
if not IsPedInAnyVehicle(ped, false) then
return {
imFahrzeug = false,
speed = 0,
speedMph = 0,
tankstand = 0,
gang = 0
}
end
local vehicle = GetVehiclePedIsIn(ped, false)
local rohGeschwindigkeit = GetEntitySpeed(vehicle) -- Meter pro Sekunde
-- Umrechnungsfaktoren
local speedKmh = math.floor(rohGeschwindigkeit * 3.6)
local speedMph = math.floor(rohGeschwindigkeit * 2.237)
-- Tankstand (0,0 bis 1,0 — mal 100 für Prozent)
local tankstand = math.floor(GetVehicleFuelLevel(vehicle))
-- Aktueller Gang (0 = Rückwärts, 1-8 = Gänge)
local gang = GetVehicleCurrentGear(vehicle)
return {
imFahrzeug = true,
speed = speedKmh,
speedMph = speedMph,
tankstand = tankstand,
gang = gang
}
end
// In script.js — Fahrzeugabschnitt erweitern
function updateTacho(fahrzeugDaten) {
if (!fahrzeugDaten.imFahrzeug) {
speedometer.classList.add('hidden');
return;
}
speedometer.classList.remove('hidden');
// Zwischen km/h und mph wechseln basierend auf Spieler-Einstellung
// Einstellung in localStorage speichern
const imperial = localStorage.getItem('hud-imperial') === 'true';
speedValue.textContent = imperial ? fahrzeugDaten.speedMph : fahrzeugDaten.speed;
document.getElementById('speed-unit').textContent = imperial ? 'mph' : 'km/h';
}
Die Einheit-Präferenz in localStorage zu speichern ist ein nützliches Muster — sie bleibt sitzungsübergreifend erhalten, ohne einen Server-Roundtrip oder Datenbankeinträge zu benötigen.
Schritt 6: Hunger und Durst integrieren
Hunger- und Durstwerte liegen in den Spielerdaten des Frameworks, nicht in nativen GTA-Funktionen. Der Ansatz unterscheidet sich zwischen QBCore und ESX. Füge diese Funktionen zu deiner client.lua hinzu:
-- client.lua — Framework-Integration
-- ============================================================
-- QBCore-Integration
-- ============================================================
local QBCore = nil
-- QBCore-Export sicher abrufen (gibt nil zurück, wenn nicht installiert)
if GetResourceState('qb-core') == 'started' then
QBCore = exports['qb-core']:GetCoreObject()
end
local function getHungerQB()
if not QBCore then return 100 end
local playerData = QBCore.Functions.GetPlayerData()
if playerData and playerData.metadata then
return playerData.metadata.hunger or 100
end
return 100
end
local function getThirstQB()
if not QBCore then return 100 end
local playerData = QBCore.Functions.GetPlayerData()
if playerData and playerData.metadata then
return playerData.metadata.thirst or 100
end
return 100
end
-- ============================================================
-- ESX-Integration
-- ============================================================
local ESX = nil
if GetResourceState('es_extended') == 'started' then
ESX = exports['es_extended']:getSharedObject()
end
local function getHungerESX()
if not ESX then return 100 end
local playerData = ESX.GetPlayerData()
-- ESX speichert Hunger/Durst über esx_status oder esx_basicneeds
-- Das genaue Feld hängt davon ab, welches Needs-Script du verwendest
if playerData and playerData.metadata then
return playerData.metadata.hunger or 100
end
return 100
end
local function getThirstESX()
if not ESX then return 100 end
local playerData = ESX.GetPlayerData()
if playerData and playerData.metadata then
return playerData.metadata.thirst or 100
end
return 100
end
-- ============================================================
-- Framework automatisch erkennen und einheitliche Funktionen bereitstellen
-- ============================================================
local function getHunger()
if QBCore then
return getHungerQB()
elseif ESX then
return getHungerESX()
end
-- Fallback: Exports lesen, wenn ein anderes Needs-Script diese bereitstellt
local ok, val = pcall(function()
return exports['qs-inventory']:GetHunger()
end)
return ok and val or 100
end
local function getThirst()
if QBCore then
return getThirstQB()
elseif ESX then
return getThirstESX()
end
local ok, val = pcall(function()
return exports['qs-inventory']:GetThirst()
end)
return ok and val or 100
end
Die pcall-Wrapper um Export-Aufrufe sind defensive Programmierung für die FiveM-Umgebung. Wenn der Export nicht existiert, fängt pcall den Fehler ab, anstatt dein HUD-Script zum Absturz zu bringen. Das ist besonders nützlich bei der Entwicklung, wenn du mit einem unvollständigen Server-Setup testest.
Wenn dein Server ein eigenes Needs-Script mit Exports verwendet, ersetze die Fallback-pcall-Blöcke durch Aufrufe dieser spezifischen Exports. Das Muster ist immer dasselbe: eine Zahl zwischen 0 und 100 zurückgeben.
Performance-Optimierung
Ein schlecht geschriebenes HUD ist eine der häufigsten Ursachen für clientseitige Performance-Probleme auf FiveM-Servern. Hier sind die Regeln, die du einhalten solltest.
Niemals jeden Frame aktualisieren. Ein 200ms-Intervall (5 Updates pro Sekunde) ist für ein HUD mehr als ausreichend. Gesundheit, Rüstung, Hunger und Durst ändern sich nicht schnell genug, um Frame-genaues Polling zu rechtfertigen. Reserviere den Pro-Frame-Thread ausschließlich für Aufrufe, die wirklich jeden Frame laufen müssen — wie HideHudComponentThisFrame.
Delta-Prüfung vor dem Senden. Die Delta-Prüfung im Lua-Script verhindert SendNUIMessage-Aufrufe, wenn sich die Daten nicht geändert haben. Das ist wichtig, weil SendNUIMessage die Lua-Table zu JSON serialisiert und ein JavaScript-Event auslöst — beides ist nicht kostenlos.
-- Gut: nur senden, wenn sich etwas geändert hat
if health ~= lastHealth or armor ~= lastArmor then
SendNUIMessage({ type = 'updateHud', health = health, armor = armor })
lastHealth = health
lastArmor = armor
end
-- Schlecht: jeden Tick senden, egal was
SendNUIMessage({ type = 'updateHud', health = health, armor = armor })
DOM-Operationen in JavaScript minimieren. Fasse DOM-Schreibvorgänge zusammen. Im obigen JavaScript passieren alle Updates in einem einzigen Event-Handler, der einmal pro Lua-Update ausgelöst wird. Vermeide kleine, verstreute Update-Funktionen, die jeweils unabhängig das DOM berühren.
CSS-Transitions statt JavaScript-Animationen verwenden. Die sanften Bar-Animationen kommen von transition: width 0.3s ease im CSS, nicht von JavaScript. CSS-Transitions sind hardwarebeschleunigt und aus Script-Perspektive kostenlos. JavaScript-Animationsschleifen mit setInterval oder manuellen requestAnimationFrame-Aufrufen sind für einfache Wert-Übergänge unnötig.
HUD-Update-Schleife bei Pause deaktivieren. Wenn der Spieler das Pause-Menü öffnet, ist kein HUD-Update nötig. Füge eine Prüfung hinzu:
-- In deinem Update-Thread
if IsPlayerDead(PlayerId()) or IsPauseMenuActive() then
Wait(500) -- Deutlich verlangsamen bei Pause oder Tod
goto continue
end
Keine JavaScript-Objekte bei jedem Update erstellen. Der DOM-Query-Cache am Anfang von script.js behandelt das bereits. Darüber hinaus: Vermeide Muster, bei denen bei jedem Message-Event eine neue Array- oder Objektstruktur aufgebaut wird — bearbeite bestehende DOM-Elemente direkt.
Debugging
NUI zu debuggen ist einer der Bereiche, in dem neue FiveM-Entwickler stecken bleiben. Die Werkzeuge sind vorhanden — man muss nur wissen, wo man schauen muss.
CEF Remote DevTools. Wenn dein FiveM-Client mit dem entsprechenden Flag für CEF-Debugging läuft (oder wenn dein Server es aktiviert hat), kannst du http://localhost:13172 in einem Chrome- oder Chromium-Browser auf demselben Rechner öffnen. Das öffnet eine vollständige Chrome DevTools-Instanz, die mit der NUI-Ebene verbunden ist. Du bekommst den Elements-Panel, die Konsole, den Network-Tab und den JavaScript-Debugger — genau was du für reguläre Webentwicklung verwenden würdest.
console.log in NUI. In deinem JavaScript schreibt console.log in die CEF DevTools-Konsole, nicht in die FiveM-Konsole. Nutze es frei während der Entwicklung:
window.addEventListener('message', (event) => {
console.log('NUI-Nachricht empfangen:', event.data);
// ... Rest des Handlers
});
Browser-Mock-Tests. Öffne nui/index.html direkt in deinem Desktop-Browser. Das HUD wird gerendert, empfängt aber keine echten Spieldaten. Du kannst die Update-Logik manuell auslösen, um Layout und Styling zu testen:
// Während der Entwicklung ans Ende von script.js anhängen
// Vor dem Release entfernen
if (!window.invokeNative) {
// Wir sind im Browser, nicht in FiveM — Testdaten einspeisen
setTimeout(() => {
window.dispatchEvent(new MessageEvent('message', {
data: {
type: 'updateHud',
health: 75,
armor: 40,
speed: 80,
imFahrzeug: true,
hunger: 60,
thirst: 30
}
}));
}, 500);
}
Das ermöglicht schnelle Iterationen an HTML und CSS, ohne bei jeder Änderung die FiveM-Resource neustarten zu müssen.
Häufige Fehler und Lösungen:
- HUD erscheint gar nicht — Prüfe, ob
ui_pageinfxmanifest.luaauf den richtigen Dateipfad relativ zum Resource-Root zeigt. Kontrolliere die FiveM-Konsole auf Fehler zu nicht gefundenen Dateien. - JavaScript läuft nicht — Stelle sicher, dass das
<script>-Tag am Ende vonindex.htmlden korrekten Pfad referenziert. Prüfe die CEF-Konsole auf Syntaxfehler. - NUI-Nachrichten kommen nicht an — Bestätige, dass die Resource gestartet ist (
ensure mein-hudinserver.cfg). Überprüfe, ob dastype-Feld inSendNUIMessagemit dem übereinstimmt, was JavaScript immessage-Event-Handler prüft. - Native HUD-Elemente noch sichtbar — Die
HideHudComponentThisFrame-Aufrufe müssen jeden Frame passieren, nicht einmalig. Stelle sicher, dass der Pro-Frame-Thread mitWait(0)läuft. - Performance-Warnung in der F8-Konsole — Dein Update-Thread läuft wahrscheinlich zu häufig. Erhöhe das
Wait-Intervall oder füge das Delta-Prüfungs-Muster hinzu.
Moderner Ansatz: React oder Vue mit Vite
Alles bisher Gezeigte verwendet reines HTML, CSS und JavaScript — ein absolut valider Ansatz für die meisten HUDs. Wenn dein HUD in der Komplexität wächst (mehrere Seiten, Benutzereinstellungen, bedingte Darstellung, komplexes State-Management), lohnt sich ein modernes Frontend-Framework.
Warum React oder Vue für komplexe HUDs? Reacts Komponentenmodell und State-Management machen es deutlich einfacher, über ein HUD mit vielen interaktiven Elementen nachzudenken. State-Änderungen rendern automatisch nur die betroffenen Teile der UI. Vues Reaktivitätssystem funktioniert ähnlich. Beide Ökosysteme bieten ausgereifte Werkzeuge.
fivem-nui-react-lib. Das Community-gepflegte fivem-nui-react-lib-Paket bietet React-Hooks, die das NUI-Nachrichten- und Callback-System abstrahieren. Statt eines rohen addEventListener nutzt du typisierte Hooks:
import { useNuiEvent } from 'fivem-nui-react-lib';
interface HudDaten {
health: number;
armor: number;
speed: number;
imFahrzeug: boolean;
}
export const HudKomponente: React.FC = () => {
const [hudDaten, setHudDaten] = useState<HudDaten>({
health: 100,
armor: 0,
speed: 0,
imFahrzeug: false
});
useNuiEvent<HudDaten>('updateHud', (data) => {
setHudDaten(data);
});
return (
<div className="hud-container">
<HealthBar value={hudDaten.health} />
<ArmorBar value={hudDaten.armor} />
{hudDaten.imFahrzeug && <Tacho speed={hudDaten.speed} />}
</div>
);
};
Vite Build-Setup. Vite übernimmt das Bundling und bietet nahezu sofortiges Hot Module Replacement während der Entwicklung. Deine fxmanifest.lua zeigt auf die Vite-Build-Ausgabe (dist/index.html), und du führst vite dev während der Entwicklung aus — Änderungen in deinen React-Komponenten spiegeln sich fast sofort im Spiel wider, ohne die Resource neustarten zu müssen.
Die Build-Ausgabe ist reines HTML, CSS und JavaScript — die FiveM-Resource selbst muss nichts über React wissen. Das bedeutet, die NUI-Kommunikation bleibt identisch; nur der Entwicklungsworkflow ändert sich.
Für ein kleines HUD mit ein paar Leisten und einem Tacho ist Vanilla-JS schneller aufzusetzen und einfacher zu verstehen. Greife zu React oder Vue, wenn du 10+ UI-Komponenten, mehrere HUD-Seiten oder komplexen State hast, der zwischen Komponenten geteilt werden muss.
Open-Source HUDs zum Studieren
Produktiven HUD-Code zu lesen ist einer der schnellsten Wege, das eigene Verständnis zu verbessern. Diese Repositories sind es wert, studiert zu werden:
-
ox_hud —
github.com/overextended/ox_hud— Das HUD des Overextended-Teams. Saubere Architektur, gut optimiert, native Integration mit dem Overextended-Stack. Besonders empfehlenswert: der Lua-zu-JS-Datenfluss und die Struktur der Update-Intervalle. -
qb-hud —
github.com/qbcore-framework/qb-hud— Das QBCore-Standard-HUD. Gute Referenz für Framework-spezifisches Daten-Lesen (QBCore PlayerData, Metadata-Felder). Einfach genug, um es schnell zu verstehen. -
nh-keyboard —
github.com/nerohiro/nh-keyboard— Auffällig durch sein einzigartiges visuelles Design. Zeigt, wie man unkonventionelle HUD-Layouts erstellt, die vom Standard-Bottom-Corner-Bar-Muster abweichen. -
17mov_HUD —
github.com/17MOV/17mov_HUD— Ein modernes, poliertes HUD mit sanften Animationen. Studiere die CSS-Animationstechniken und wie das Tacho-Zeiger-Konzept umgesetzt ist. -
r_hud —
github.com/rockdude229/r_hud— Minimalistisch und sehr gut lesbarer Code. Guter Ausgangspunkt, wenn du eine saubere Implementierung ohne Feature-Überladung verstehen möchtest. -
mHud —
github.com/MichaelCode04/mHud— Zeigt einen Ansatz zur Organisation größerer HUD-Codebasen über mehrere Dateien. -
0r-hud —
github.com/0resmon/0r-hud— Zeigt ein kreisförmiges/radiales Layout. Nützlich zu studieren, wenn du über rechteckige Leisten-Designs hinausgehen möchtest. -
wasabi_hud —
github.com/wasabirobby/wasabi_hud— Starkes Beispiel für Framework-agnostisches Design. Das Konfigurationssystem zur Framework-Erkennung ist besonders lesenswert.
Wer lieber ein professionell entwickeltes Premium-HUD mit erweiterten Features und Support möchte, findet im VertexMods Shop HUD-Scripts, die für Server mit hoher Spielerzahl ausgelegt sind.
Häufige Fragen
Muss ich React oder Vue kennen, um ein FiveM-HUD zu bauen?
Nein. Reines JavaScript ist für die meisten HUDs völlig ausreichend. React und Vue werden nützlich, wenn ein HUD zu einer komplexen Mehrseitenoberfläche mit vielen interaktiven Elementen wird. Beginne mit Vanilla JS und migriere zu einem Framework nur dann, wenn du es wirklich brauchst.
Kann mein HUD die Performance anderer Spieler beeinträchtigen oder nur meine eigene?
NUI läuft clientseitig, innerhalb des FiveM-Clients jedes Spielers. Ein schlecht geschriebenes HUD betrifft nur den Spieler, der es ausführt. Wenn deine Resource jedoch serverseitige Komponenten hat (was ein einfaches HUD nicht haben sollte), können diese alle Spieler betreffen.
Wie füge ich eine Minimap zu meinem eigenen HUD hinzu?
Die native GTA-Minimap ist eine separate HUD-Komponente (Komponente 0), die vom HUD-System von FiveM verwaltet wird. Du kannst sie über SetRadarZoom und LockMinimapAngle positionieren. Alternativ bieten mehrere Community-Resources eine eigene Minimap-NUI-Komponente — ox_minimap wird am häufigsten verwendet.
Warum ist mein HUD beim Server-Beitritt unsichtbar, erscheint aber nach ein paar Sekunden?
Die NUI-Seite lädt, nachdem das Client-Script gestartet hat. Wenn dein Lua-Script Nachrichten sendet, bevor die HTML-Seite vollständig geladen ist, gehen diese Nachrichten verloren. Füge am Anfang deines Client-Scripts ein Wait(2000) hinzu, bevor die Update-Schleife beginnt, damit die NUI-Seite bereit ist.
Kann ich externe Schriften oder Icon-Libraries verwenden?
Ja, aber bündele sie lokal. Kopiere die Schriftdateien in deinen nui/-Ordner und referenziere sie mit @font-face in deinem CSS. Lade nicht von Google Fonts oder einem CDN — NUI kann keine externen Netzwerkanfragen in dem üblichen Sinne stellen, und CDN-Anfragen können fehlschlagen oder Konsolenfehler verursachen.
Wie zeige ich das HUD nur an, wenn der Spieler gespawnt ist?
Sende ein SendNUIMessage({ type = 'setVisible', visible = false }) beim Ressourcenstart oder im onClientResourceStart-Event, und setze es erst sichtbar, nachdem der Spieler in die Welt gespawnt ist. Die meisten Frameworks feuern ein Event wie QBCore:Client:OnPlayerLoaded oder esx:playerLoaded, in das du dich einklinken kannst.
Meine Leisten-Animationen sehen ruckelig aus. Wie glättet man das?
Prüfe, ob deine CSS-transition-Eigenschaft am bar-fill-Element sitzt, nicht am bar-track-Container. Überprüfe außerdem, dass du style.width nicht mehrfach pro JavaScript-Event-Handler aufrufst — schreibe die Breite einmal pro Update-Zyklus.
Gibt es einen Performance-Unterschied zwischen einer großen Nachricht und vielen kleinen?
Eine größere, gebündelte Nachricht ist effizienter. Jeder SendNUIMessage-Aufruf serialisiert Daten zu JSON und löst ein JavaScript-Event aus — beides ist nicht kostenlos. Fasse alle HUD-Daten in eine einzige Nachricht pro Update-Zyklus zusammen, statt separate Nachrichten für Gesundheit, Rüstung, Geschwindigkeit usw. zu senden.

