Poradniki

Lua w FiveM – od zera do pisania komend

1) Co to jest Lua i dlaczego w FiveM?

Lua to lekki język skryptowy:

  • prosty składniowo (idealny na start),
  • szybki,
  • elastyczny (dużo rzeczy zrobisz krótkim kodem).

W FiveM Lua „gada” z grą przez tzw. natives (funkcje GTA) i eventy (zdarzenia), dzięki czemu możesz:

  • reagować na komendy gracza,
  • rysować UI/HUD,
  • wykonywać akcje na postaci/pojeździe,
  • komunikować się z serwerem i innymi graczami.

2) Gdzie piszemy Lua? (struktura resource’u)

Utwórz resource, np. resources/[local]/cf_lua_intro/ i dodaj trzy pliki:

fxmanifest.lua

fx_version 'cerulean'
game 'gta5'

name 'cf_lua_intro'
author 'Ty'
version '1.0.0'
description 'Lekcja Lua: komendy, eventy, wątki'

client_scripts {
  'client.lua'
}

server_scripts {
  'server.lua'
}

Pamiętaj

  • client.lua – kod u gracza (rysowanie, komendy lokalne, odczyt klawiszy).
  • server.lua – kod na serwerze (logika globalna, broadcasty do graczy, uprawnienia).
  • Dodaj w server.cfg:
    ensure cf_lua_intro

Do testów komunikatów użyj wbudowanego resource chat (zwykle już jest włączony).


3) Budowa komendy w Lua (FiveM)

API (wspólne dla klienta i serwera)

RegisterCommand(
  'nazwa',                                  -- nazwa komendy wpisywanej na czacie
  function(source, args, rawCommand)        -- handler wywoływany po wpisaniu komendy
    -- source: id gracza (na kliencie zwykle 0)
    -- args:   tablica słów po nazwie komendy { "abc", "123" }
    -- rawCommand: cały tekst komendy (np. "nazwa abc 123")
  end,
  false                                     -- restricted: false=każdy, true=tylko ACE
)

Najważniejsze:

  • args to stringi – liczby zamieniaj tonumber(args[i]).
  • Flaga restricted gdy true – komenda wymaga uprawnienia ACE (polecam na serwerze).

4) Pierwsza komenda w kliencie – linia po linii

client.lua

-- /hello: wyświetl wiadomość na czacie
RegisterCommand('hello', function(source, args, raw)
  -- Wywołaj event lokalny czatu (resource "chat")
  TriggerEvent('chat:addMessage', {
    color = { 255, 255, 255 },             -- kolor tekstu
    multiline = true,
    args = { 'CF', 'Cześć! Uczę się Lua w FiveM 🚀' } -- [prefiks, treść]
  })
end, false)

Co tu się dzieje?

  • Rejestrujemy komendę hello.
  • W handlerze odpalamy event czatu z parametrami wiadomości.
  • false ⇒ każdy gracz może jej użyć.

Test: Wejdź na serwer → wpisz /hello.


5) Komenda z argumentami – parsowanie i walidacja

Zrobimy /announce <tekst>: gracz na kliencie wysyła treść do serwera, a serwer rozsyła wszystkim.

client.lua

-- /announce <tekst>  -> wyślij do serwera, by ogłosił wszystkim
RegisterCommand('announce', function(_, args, raw)
  local text = table.concat(args, ' ')
  if text == nil or text == '' then
    TriggerEvent('chat:addMessage', { args = { 'CF', 'Użycie: /announce <tekst>' } })
    return
  end
  TriggerServerEvent('cf:announce', text)   -- klient -> serwer
end, false)

server.lua

-- Odbierz ogłoszenie i rozgłośnij wszystkim
RegisterNetEvent('cf:announce', function(text)
  local src = source                         -- kto wysłał
  -- Prosta walidacja długości
  if type(text) ~= 'string' or #text > 200 then return end

  -- Do wszystkich graczy (-1)
  TriggerClientEvent('chat:addMessage', -1, {
    color = { 255, 200, 0 },
    args = { 'OGŁOSZENIE', text }
  })
end)

Krok po kroku:

  1. Klient zbiera tekst (argstable.concat).
  2. Waliduje pusty string.
  3. TriggerServerEvent wysyła na serwer.
  4. Serwer odbiera (RegisterNetEvent + handler), weryfikuje i rozsyła TriggerClientEvent do -1 (wszyscy).

6) Komenda z liczbami – przykład teleportu

Zrobimy /tp <x> <y> <z> (do testów na lokalnym serwerze!)

client.lua (dopisz pod poprzednimi)

-- /tp x y z -> teleportuj lokalnego gracza
RegisterCommand('tp', function(_, args)
  if #args < 3 then
    TriggerEvent('chat:addMessage', { args = { 'CF', 'Użycie: /tp <x> <y> <z>' } })
    return
  end

  -- Konwersja string -> number (zbieraj błędy)
  local x = tonumber(args[1])
  local y = tonumber(args[2])
  local z = tonumber(args[3])
  if not x or not y or not z then
    TriggerEvent('chat:addMessage', { args = { 'CF', 'Błędne liczby. Przykład: /tp -268.0 -957.0 31.2' } })
    return
  end

  local ped = PlayerPedId()
  SetEntityCoordsNoOffset(ped, x + 0.0, y + 0.0, z + 0.0, false, false, false)
end, false)

Co tłumaczymy:

  • Walidacja argumentów – minimalna liczba i typy.
  • Natives: PlayerPedId() (daje handle postaci), SetEntityCoordsNoOffset(...) (ustawia pozycję).

W produkcji takie komendy zabezpieczasz (nie każdy ma mieć TP).


7) Wątki (CreateThread) i „tick”

Wątki służą do czynności powtarzalnych (np. rysowanie na ekranie co klatkę).

Przykład: prosty „HUD współrzędnych” włączany/wyłączany komendą

local showCoords = false

RegisterCommand('coords', function()
  showCoords = not showCoords
end, false)

CreateThread(function()
  while true do
    if showCoords then
      local p = GetEntityCoords(PlayerPedId())
      -- Rysowanie małego napisu w lewym górnym rogu
      SetTextFont(0); SetTextScale(0.35, 0.35); SetTextOutline()
      SetTextEntry("STRING")
      AddTextComponentString(string.format("X: %.2f  Y: %.2f  Z: %.2f", p.x, p.y, p.z))
      DrawText(0.015, 0.015)
      Wait(0)                 -- gdy aktywne: rysuj co klatkę
    else
      Wait(250)               -- gdy nieaktywne: śpij dłużej (oszczędzaj FPS)
    end
  end
end)

Ważne zasady wydajności:

  • Jeżeli coś nie musi działać co klatkę, zwiększaj Wait(...).
  • Gdy warunek „nieaktywny” – dawaj dłuższy Wait, żeby nie budzić wątku niepotrzebnie.

8) Uprawnienia ACE (restricted)

Jeśli komenda w RegisterCommand ma true na końcu, wymaga ACE:

-- server.lua
RegisterCommand('helloall', function(src, args, raw)
  TriggerClientEvent('chat:addMessage', -1, { args = { 'CF', 'Wiadomość dla wszystkich' } })
end, true)  -- restricted

W server.cfg dopisz np.:

add_ace group.admin command.helloall allow
add_principal identifier.steam:TWÓJHEX group.admin

Dzięki temu tylko wskazani gracze (albo grupa) mogą używać tej komendy.


9) Najczęstsze pułapki (i szybkie remedia)

  • Resource się nie ładuje → sprawdź ścieżkę i nazwę fxmanifest.lua (nie .txt).
  • Komendy „nie istnieją” → czy resource jest ensure/start? Użyj refresh, start, status.
  • Brak wiadomości czatu → czy masz włączony resource chat?
  • Zawieszenia/zużycie CPU → unikaj wielu pętli Wait(0) bez warunku; dawaj dłuższe Wait.
  • Argsy „puste” → waliduj #args i typy (używaj tonumber).
  • Brak efektu natives → upewnij się, że wywołujesz je po kliencie (nie wszystko działa na serwerze).

10) Cały przykładowy resource (sklejka)

fxmanifest.lua

fx_version 'cerulean'
game 'gta5'

name 'cf_lua_intro'
author 'Ty'
version '1.0.0'

client_scripts { 'client.lua' }
server_scripts { 'server.lua' }

client.lua

-- /hello
RegisterCommand('hello', function(_, args, raw)
  TriggerEvent('chat:addMessage', {
    color = {255,255,255},
    multiline = true,
    args = {'CF','Cześć! Uczę się Lua w FiveM 🚀'}
  })
end, false)

-- /announce <tekst>  (klient -> serwer)
RegisterCommand('announce', function(_, args)
  local text = table.concat(args, ' ')
  if not text or text == '' then
    TriggerEvent('chat:addMessage', { args = {'CF','Użycie: /announce <tekst>'} })
    return
  end
  TriggerServerEvent('cf:announce', text)
end, false)

-- /tp x y z
RegisterCommand('tp', function(_, args)
  if #args < 3 then
    TriggerEvent('chat:addMessage', { args = {'CF','Użycie: /tp <x> <y> <z>'} })
    return
  end
  local x,y,z = tonumber(args[1]), tonumber(args[2]), tonumber(args[3])
  if not x or not y or not z then
    TriggerEvent('chat:addMessage', { args = {'CF','Błędne liczby. Przykład: /tp -268.0 -957.0 31.2'} })
    return
  end
  SetEntityCoordsNoOffset(PlayerPedId(), x+0.0, y+0.0, z+0.0, false, false, false)
end, false)

-- HUD współrzędnych: /coords przełącza
local showCoords = false
RegisterCommand('coords', function() showCoords = not showCoords end, false)

CreateThread(function()
  while true do
    if showCoords then
      local p = GetEntityCoords(PlayerPedId())
      SetTextFont(0); SetTextScale(0.35,0.35); SetTextOutline()
      SetTextEntry("STRING"); AddTextComponentString(string.format("X: %.2f  Y: %.2f  Z: %.2f", p.x,p.y,p.z))
      DrawText(0.015, 0.015)
      Wait(0)
    else
      Wait(250)
    end
  end
end)

server.lua

-- Broadcast ogłoszenia do wszystkich
RegisterNetEvent('cf:announce', function(text)
  local src = source
  if type(text) ~= 'string' or #text == 0 or #text > 200 then return end
  TriggerClientEvent('chat:addMessage', -1, {
    color = {255,200,0},
    args = {'OGŁOSZENIE', text}
  })
end)

-- Przykład komendy restricted (ACE)
RegisterCommand('helloall', function(src)
  TriggerClientEvent('chat:addMessage', -1, { args = {'CF','Serwer mówi cześć!'} })
end, true)

11) Ćwiczenia (polecam zrobić od razu)

  1. /me <tekst> (client) – wypisz tekst nad postacią jako 3D-napis (użyj prostego DrawText3D na GetEntityCoords + offset).
  2. /bring <id> (server) – przenieś wybranego gracza do admina (server sprawdza ACE, server wysyła do klienta tego gracza event „ustaw koordy”).
  3. /speed <kmh> (client) – jeśli siedzisz w pojeździe, ustaw jego prędkość maksymalną (konwersja km/h → m/s).

12) Co dalej?

  • Przejdź do NUI (HTML/CSS/JS) i podepnij przyciski pod RegisterNUICallback.
  • Naucz się korzystać z exports (wołanie funkcji z innego resource’u).
  • Jeśli planujesz framework: ESX / QBCore – tam też Lua, ale z gotową warstwą „pracy/ekonomii”.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *