Sale on selected scripts — limited time only.

VIP membership — full catalog access, one monthly price.

Browse the shop for current deals.

BlogArtikel
FIG. 01/Development · 05. Dezember 2024

FiveM Lua-Scripting: Der vollständige Einstieg für Einsteiger und Fortgeschrittene

FUsionDev Team18 Minuten Lesezeit05.12.2024Development

Warum Lua — und warum FiveM-Lua anders ist

Lua ist eine leichtgewichtige, schnell erlernbare Skriptsprache die für Game-Scripting optimiert wurde. FiveM nutzt Lua als primäre Sprache für Server- und Client-Scripts. Wer Lua grundlegend versteht, kann innerhalb weniger Wochen eigene FiveM-Scripts schreiben.

Aber Vorsicht: FiveM-Lua ist nicht ganz normales Lua. FiveM erweitert Lua um eine eigene Runtime mit Coroutinen, asynchronen Callbacks und Zugriff auf die GTA V Scripting API (Natives). Wer vorher nur "normales" Lua kannte, muss einige FiveM-spezifische Konzepte neu lernen.

Dieser Guide setzt grundlegende Programmierkenntnisse voraus (Variablen, Funktionen, Schleifen) aber kein spezifisches Lua-Vorwissen.

Development-Umgebung einrichten

Bevor du die erste Zeile Code schreibst, richte eine gute Entwicklungsumgebung ein. Das spart Stunden später.

  • VS Code mit den Extensions: lua-language-server (sumneko), cfxlua-vscode (citizenfx). Diese geben dir Autocomplete für alle FiveM-Natives und Framework-Funktionen.
  • Lokaler Testserver: Installiere einen FiveM-Server lokal auf deinem PC. Das erlaubt schnelle Iterationen ohne Deployment-Overhead.
  • txAdmin: Das offizielle Server-Management-Interface. Macht Ressource-Restarts per Klick möglich statt Server-Neustarts zu erfordern.
  • Git: Versionskontrolle von Anfang an. Du wirst es bereuen wenn du es nicht nutzt.

Die Ressourcen-Struktur verstehen

Jede FiveM-Erweiterung ist eine "Ressource" — ein Ordner mit einer Manifest-Datei und Script-Dateien. Die Manifest-Datei (fxmanifest.lua) ist das Herz jeder Ressource:

-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

author 'Dein Name'
description 'Beschreibung deines Scripts'
version '1.0.0'

-- Shared Scripts laufen auf Client UND Server
shared_scripts {
  '@ox_lib/init.lua',  -- Externe Library einbinden
  'config.lua',
}

-- Client Scripts laufen nur auf Spieler-PCs
client_scripts {
  'client/main.lua',
  'client/ui.lua',
  'client/events.lua',
}

-- Server Scripts laufen nur auf deinem Server
server_scripts {
  '@oxmysql/lib/MySQL.lua',
  'server/main.lua',
  'server/events.lua',
}

-- NUI Files (HTML/CSS/JS für Ingame-UI)
ui_page 'html/index.html'
files {
  'html/index.html',
  'html/style.css',
  'html/app.js',
}

Client vs. Server: Die fundamentalste Unterscheidung

Das Wichtigste das du in FiveM-Entwicklung verstehen musst:

  • Client-Code läuft im GTA V Prozess auf dem PC jedes Spielers. Er hat Zugriff auf den Spielstand, kann Animationen abspielen, die Kamera steuern, UI anzeigen und GTA-Natives aufrufen. Aber: Client-Code kann von Hackern manipuliert werden.
  • Server-Code läuft zentral auf deinem Server-PC. Nur hier sollte sensible Logik stattfinden: Geld hinzufügen, Items vergeben, Datenbankoperationen. Server-Code ist vertrauenswürdig.
Die goldene Regel: Niemals sicherheitskritische Operationen (Geld, Items, Berechtigungen) ausschließlich client-seitig implementieren. Immer über den Server validieren und ausführen.

Ein typischer Exploit nutzt genau diesen Fehler aus: Der Angreifer sendet ein manipuliertes Client-Event das direkt Geld hinzufügt, weil der Entwickler vergessen hat server-seitig zu validieren.

Citizen.CreateThread: Der FiveM Game-Loop

FiveM nutzt Coroutinen für seine Script-Ausführung. Citizen.CreateThread erstellt einen neuen Coroutinen-Thread der parallel zu anderen läuft:

-- Einfachster Thread: Läuft jeden Frame
Citizen.CreateThread(function()
  while true do
    Citizen.Wait(0)  -- 0 = nächsten Frame abwarten
    -- Code hier wird ~60x pro Sekunde ausgeführt
    local ped = PlayerPedId()
    local coords = GetEntityCoords(ped)
    print(coords)
  end
end)

-- Thread mit Intervall: Läuft alle 2 Sekunden
Citizen.CreateThread(function()
  while true do
    Citizen.Wait(2000)  -- 2000ms = 2 Sekunden warten
    checkSomething()
  end
end)

-- Thread der einmal läuft und dann stoppt
Citizen.CreateThread(function()
  Citizen.Wait(5000)  -- 5 Sekunden warten
  doSomethingOnce()
  -- Kein while true = Thread endet nach der Funktion
end)

Citizen.Wait gibt die Kontrolle kurz ab und lässt andere Threads und FiveM selbst laufen. Ohne Citizen.Wait in einer while-Schleife würde der Thread den gesamten Prozess blockieren — niemals vergessen.

Events: Kommunikation zwischen Client und Server

Da Client und Server in getrennten Prozessen laufen, kommunizieren sie über Events — Nachrichten die Daten übertragen:

-- ─── CLIENT SIDE ───────────────────────────────────────────

-- Event an Server senden (TriggerServerEvent)
RegisterKeyMapping('openShop', 'Shop öffnen', 'keyboard', 'F5')
RegisterCommand('openShop', function()
  TriggerServerEvent('myShop:playerOpenedShop')
end, false)

-- Event vom Server empfangen
RegisterNetEvent('myShop:shopOpened', false)
AddEventHandler('myShop:shopOpened', function(shopData)
  -- shopData kam vom Server
  openShopUI(shopData)
end)

-- ─── SERVER SIDE ───────────────────────────────────────────

-- Event vom Client empfangen
RegisterNetEvent('myShop:playerOpenedShop', false)
AddEventHandler('myShop:playerOpenedShop', function()
  local src = source  -- 'source' ist die Spieler-Server-ID

  -- Validierung: Hat der Spieler das Recht den Shop zu öffnen?
  local playerCoords = GetEntityCoords(GetPlayerPed(src))
  local shopCoords = vector3(200.0, 300.0, 30.0)
  if #(playerCoords - shopCoords) > 10.0 then
    return  -- Spieler ist nicht nah genug
  end

  -- Shop-Daten aus DB laden und zurücksenden
  local shopData = getShopInventory('general_store')
  TriggerClientEvent('myShop:shopOpened', src, shopData)
end)

GTA V Natives: Die Spielwelt steuern

Natives sind die C++-Funktionen von GTA V die durch das Scripting-System verfügbar gemacht werden. Es gibt tausende davon — von einfachen Koordinaten-Abfragen bis zu komplexen Animations-Systemen.

Die komplette Native-Referenz findest du unter natives.citizenfx.io. Wichtige Kategorien die du früh kennen solltest:

-- ENTITY NATIVES
local ped = PlayerPedId()                              -- Eigener Spieler-Ped
local coords = GetEntityCoords(ped)                    -- Position
local heading = GetEntityHeading(ped)                  -- Richtung in Grad
local health = GetEntityHealth(ped)                    -- Gesundheit (0-200)
SetEntityCoords(ped, 0.0, 0.0, 72.0, false, false, false, false)

-- PED NATIVES
SetPedArmour(ped, 100)                                 -- Rüstung setzen
TaskPlayAnim(ped, 'amb@world_human_hang_out_street@male_b@base', 'base', 1.0, 1.0, -1, 1, 0, false, false, false)
ClearPedTasks(ped)                                     -- Animationen stoppen

-- VEHICLE NATIVES
local vehicle = GetVehiclePedIsIn(ped, false)          -- Aktuelles Fahrzeug
SetVehicleEngineOn(vehicle, true, true, false)         -- Motor an
SetVehicleDoorsLocked(vehicle, 2)                      -- Türen sperren (2 = alle)
local speed = GetEntitySpeed(vehicle) * 3.6            -- Geschwindigkeit in km/h

-- DRAWING NATIVES (nur client-side)
DrawText3d = function(x, y, z, text)
  local onScreen, screenX, screenY = World3dToScreen2d(x, y, z)
  if onScreen then
    SetTextScale(0.35, 0.35)
    SetTextFont(4)
    SetTextColour(255, 255, 255, 255)
    SetTextOutline()
    BeginTextCommandDisplayText('STRING')
    AddTextComponentSubstringPlayerName(text)
    EndTextCommandDisplayText(screenX, screenY)
  end
end

Exports: Ressourcen miteinander verbinden

Exports ermöglichen es, Funktionen aus einer Ressource in anderen Ressourcen aufzurufen. Das ist der saubere Weg für Inter-Ressource-Kommunikation:

-- In Ressource "myInventory": Funktion exportieren
exports('addItem', function(playerId, itemName, amount)
  -- Validierung
  if not playerId or not itemName or amount <= 0 then return false end
  -- Item hinzufügen
  addItemToPlayer(playerId, itemName, amount)
  return true
end)

exports('getPlayerInventory', function(playerId)
  return getInventoryForPlayer(playerId)
end)

-- In einer anderen Ressource: Export nutzen
local success = exports['myInventory']:addItem(playerId, 'bread', 5)
if success then
  print('Item erfolgreich hinzugefügt')
end

-- Framework-Exports (ESX/QBCore nutzen das ausgiebig)
local QBCore = exports['qb-core']:GetCoreObject()
local ESX = exports['es_extended']:getSharedObject()

State Bags: Modernes Daten-Sharing

State Bags sind ein moderneres Feature von FiveM das das direkte Setzen von Daten auf Entities (Peds, Fahrzeuge, Spieler) ermöglicht, die automatisch zwischen Server und Clients synchronisiert werden:

-- SERVER: State auf Spieler setzen
local playerState = Player(source).state
playerState:set('job', 'police', true)  -- true = an alle Clients broadcasten
playerState:set('isOnDuty', true, true)

-- CLIENT: State eines Spielers lesen
local targetState = Player(GetPlayerServerId(PlayerId())).state
local job = targetState.job          -- 'police'
local onDuty = targetState.isOnDuty  -- true

-- CLIENT: Auf State-Änderungen reagieren
AddStateBagChangeHandler('isOnDuty', nil, function(bagName, key, value)
  if bagName == ('player:' .. GetPlayerServerId(PlayerId())) then
    updateDutyIndicator(value)
  end
end)

State Bags sind effizienter als Events für Daten die sich häufig ändern und von vielen Clients gleichzeitig gelesen werden müssen, weil sie nicht manuell zu jedem Client gebroadcastet werden müssen.

NUI: Browser-basierte Ingame-UIs

FiveM erlaubt das Einbetten von HTML/CSS/JS-UIs direkt im Spiel. Diese sogenannte NUI (New User Interface) läuft in einem Chromium-Browser über dem Spielgeschehen:

-- client/ui.lua: NUI öffnen und Daten übergeben
function openPlayerMenu(menuData)
  SetNuiFocus(true, true)   -- Maus freigeben, Input sperren
  SendNUIMessage({
    action = 'openMenu',
    data = {
      playerName = GetPlayerName(PlayerId()),
      money = menuData.money,
      job = menuData.job,
      items = menuData.items,
    }
  })
end

-- NUI-Callback: JavaScript sendet Daten zurück zu Lua
RegisterNUICallback('closeMenu', function(data, cb)
  SetNuiFocus(false, false)
  cb({ ok = true })
end)

RegisterNUICallback('buyItem', function(data, cb)
  TriggerServerEvent('myScript:buyItem', data.itemId, data.quantity)
  cb({ ok = true })
end)
// html/app.js: Lua-Events empfangen
window.addEventListener('message', (event) => {
  const { action, data } = event.data;

  if (action === 'openMenu') {
    document.getElementById('player-name').textContent = data.playerName;
    document.getElementById('money').textContent = `€${data.money}`;
    document.getElementById('menu').style.display = 'block';
  }
});

// Lua-Callback aufrufen
function closeMenu() {
  fetch('https://myScript/closeMenu', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ok: true })
  });
}

document.getElementById('close-btn').addEventListener('click', closeMenu);

ox_lib: Die moderne Standard-Library

ox_lib ist die Community-Standard-Library für moderne FiveM-Entwicklung. Sie bietet fertige Lösungen für die häufigsten Aufgaben und ersetzt viele Rad-Neuerfindungen:

-- Context Menu (kein HTML/NUI nötig)
lib.registerContext({
  id = 'police_menu',
  title = 'Polizei-Optionen',
  options = {
    { title = 'Handschellen anlegen', icon = 'handcuffs', event = 'police:cuffPlayer' },
    { title = 'Durchsuchen', icon = 'search', event = 'police:searchPlayer' },
    { title = 'Verhaften', icon = 'jail', event = 'police:arrestPlayer' },
  }
})
lib.showContext('police_menu')

-- Progress Bar
lib.progressBar({
  duration = 5000,
  label = 'Fahrzeug wird repariert...',
  useWhileDead = false,
  canCancel = true,
  disable = { car = true },
  anim = { dict = 'mini@repair', clip = 'fixing_a_ped' },
}, function(cancelled)
  if not cancelled then
    TriggerServerEvent('mechanic:vehicleRepaired', GetVehiclePedIsIn(PlayerPedId(), false))
  end
end)

-- Notification
lib.notify({
  title = 'Benachrichtigung',
  description = 'Fahrzeug erfolgreich repariert',
  type = 'success',  -- success, error, inform, warning
  duration = 5000,
})

Dein erstes vollständiges Script: Ein einfacher Shop

Zum Abschluss ein minimales, aber vollständiges Script das alle Konzepte verbindet — ein einfacher In-Game-Shop:

-- config.lua (shared)
Config = {}
Config.ShopLocation = vector3(25.76, -1348.03, 29.50)
Config.ShopRadius = 3.0
Config.Items = {
  { name = 'bread', label = 'Brot', price = 10 },
  { name = 'water', label = 'Wasser', price = 5 },
  { name = 'bandage', label = 'Verband', price = 25 },
}
-- client/main.lua
local shopOpen = false

Citizen.CreateThread(function()
  while true do
    local dist = #(GetEntityCoords(PlayerPedId()) - Config.ShopLocation)

    if dist < Config.ShopRadius then
      DrawText3d(Config.ShopLocation.x, Config.ShopLocation.y, Config.ShopLocation.z + 1.0,
        '[E] Shop öffnen')

      if IsControlJustReleased(0, 38) and not shopOpen then -- E-Taste
        TriggerServerEvent('simpleShop:openRequest')
      end
      Citizen.Wait(0)
    else
      Citizen.Wait(500)
    end
  end
end)

RegisterNetEvent('simpleShop:openShop', false)
AddEventHandler('simpleShop:openShop', function(items)
  shopOpen = true
  -- UI öffnen mit items
  SetNuiFocus(true, true)
  SendNUIMessage({ action = 'openShop', items = items })
end)

RegisterNUICallback('buyItem', function(data, cb)
  TriggerServerEvent('simpleShop:buy', data.itemName, data.quantity)
  cb({})
end)

RegisterNUICallback('closeShop', function(_, cb)
  shopOpen = false
  SetNuiFocus(false, false)
  cb({})
end)
-- server/main.lua
RegisterNetEvent('simpleShop:openRequest', false)
AddEventHandler('simpleShop:openRequest', function()
  local src = source
  TriggerClientEvent('simpleShop:openShop', src, Config.Items)
end)

RegisterNetEvent('simpleShop:buy', false)
AddEventHandler('simpleShop:buy', function(itemName, quantity)
  local src = source
  local Player = QBCore.Functions.GetPlayer(src)
  if not Player then return end

  -- Item in Config suchen
  local item = nil
  for _, v in ipairs(Config.Items) do
    if v.name == itemName then item = v break end
  end
  if not item then return end

  local totalCost = item.price * quantity
  if Player.Functions.GetMoney('cash') < totalCost then
    TriggerClientEvent('ox_lib:notify', src, {
      type = 'error', description = 'Nicht genug Geld.'
    })
    return
  end

  Player.Functions.RemoveMoney('cash', totalCost, 'shop-purchase')
  Player.Functions.AddItem(itemName, quantity)
  TriggerClientEvent('ox_lib:notify', src, {
    type = 'success',
    description = quantity .. 'x ' .. item.label .. ' gekauft.'
  })
end)

Nächste Schritte

Mit diesen Grundlagen kannst du eigene Scripts entwickeln. Der beste nächste Schritt: Lies den Quellcode gut geschriebener Open-Source-Scripts. ox_lib, ox_inventory und die offiziellen QBCore-Scripts sind exzellente Referenzen für moderne Code-Struktur. Fange klein an, baue inkrementell und teste nach jeder Änderung. Die FiveM-Community in Discord-Servern wie CFX.re Community ist hilfsbereit bei spezifischen Fragen.

FIG. 03/Shop

Bereit für den nächsten Schritt?

Entdecke unsere Script-Kollektion — optimiert, getestet und mit direktem Support.