Zasoby TypeScript w FiveM: fxmanifest, typy @citizenfx i esbuild

Napisz zasoby FiveM w TypeScript lub JavaScript: fxmanifest z plikami dist, typy @citizenfx, zbiór esbuild, zdarzenia, setTick i Wait, oraz wywoływanie eksportów Lua.

Chcesz typów, async/await i pakietów npm w swoim skrypcie FiveM, ale twój zasób to tylko pliki .lua. FiveM uruchamia JavaScript we własnym runtime, więc TypeScript działa tak długo, jak jest skompilowany do JavaScript najpierw. Ten przewodnik daje działający układ: manifest, typy, bundler, zdarzenia i ticki, i jak mieszać go z Lua.

Układ projektu

text
my_resource/
  fxmanifest.lua
  package.json
  tsconfig.json
  src/
    client.ts
    server.ts
  dist/            (build output, loaded by FiveM)

Edytujesz src/, budujesz w dist/, a manifest zna tylko dist/.

fxmanifest

lua
fx_version 'cerulean'
game 'gta5'

client_script 'dist/client.js'
server_script 'dist/server.js'

Oba to zwykłe pliki JavaScript. Jeśli twój zasób potrzebuje konkretnej wersji runtime JavaScript lub funkcji Node na serwerze, sprawdź bieżące dokumenty Cfx.re dla opcji manifestu, ponieważ wspierane runtime zmieniały się w czasie.

Porada: jeśli pominiesz kompilację, FiveM mówi, że nie może znaleźć dist/client.js. Zawsze uruchom kompilację przed ensure lub restart.

Zainstaluj typy i bundler

bash
npm init -y
npm install --save-dev typescript esbuild @citizenfx/client @citizenfx/server

@citizenfx/client i @citizenfx/server to pakiety typów dla natives i funkcji Cfx.re po każdej stronie. tsconfig.json, który je używa:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

TypeScript tylko sprawdza typy tutaj (noEmit). esbuild robi faktyczną zbiorkę:

json
{
  "scripts": {
    "build": "esbuild src/client.ts --bundle --outfile=dist/client.js --target=es2020 && esbuild src/server.ts --bundle --outfile=dist/server.js --target=es2020 --platform=node",
    "watch": "npm run build -- --watch",
    "typecheck": "tsc"
  }
}

Zbiórka oznacza, że pakiety npm, które importujesz, są kopiowane do jednego pliku wyjściowego, więc serwer nie potrzebuje node_modules w runtime. Sprawdź dokumenty Cfx.re dla właściwego --platform i --target dla runtime, które używają twoje artefakty.

Zdarzenia

JavaScript ma te same cztery bloki budowlane co Lua, z innymi nazwami:

Lua JavaScript
AddEventHandler on
RegisterNetEvent + handler onNet
TriggerEvent emit
TriggerServerEvent / TriggerClientEvent emitNet
ts
// src/server.ts
onNet('my_resource:requestData', () => {
  const src = (global as any).source as number
  emitNet('my_resource:data', src, { hello: 'world' })
})
ts
// src/client.ts
on('onClientResourceStart', (resourceName: string) => {
  if (GetCurrentResourceName() !== resourceName) return
  emitNet('my_resource:requestData')
})

onNet('my_resource:data', (data: { hello: string }) => {
  console.log(data.hello)
})

Na serwerze gracz, który wyzwolił zdarzenie, jest w globalnym source, jak w Lua. Przeczytaj go do stałej na początku handlera: po await, global może wskazywać na innego gracza. Te same zasady bezpieczeństwa z zabezpieczania zdarzeń serwerowych mają zastosowanie, ponieważ gracz może wyzwolić twój onNet z zewnątrz. Więcej o modelu zdarzeń w zdarzeniach klient i serwer.

Ticki i czekanie

Nie ma domyślnie pętli ramek. setTick uruchamia funkcję każdą ramkę, a Wait z Lua staje się obietnicą wokół setTimeout:

ts
const Delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))

setTick(async () => {
  const ped = PlayerPedId()
  const [x, y, z] = GetEntityCoords(ped, true)
  // ... check something about the position
  await Delay(500)   // without this, it runs every frame
})

Bez opóźnienia, handler uruchamia się każdą ramkę, ten sam problem co pętla Lua bez Wait(0): zobacz Wait i wątki wyjaśnione. Użyj dłuższego opóźnienia, gdy nic nie musi być sprawdzane każdą ramkę.

Dla jednorazowego opóźnienia pomiń setTick i po prostu await Delay(1000) wewnątrz funkcji async.

Mieszanie z eksportami Lua

Eksporty pracują między językami. Zarejestruj jeden w JavaScript:

ts
exports('getGreeting', (name: string) => `Hello ${name}`)

I wołaj go z zasobu Lua:

lua
local text = exports['my_resource']:getGreeting('Mic')

Drugi kierunek, wywoływanie eksportu Lua z TypeScript, przechodzi przez obiekt exports:

ts
const ok = exports['ox_inventory'].GetItemCount(source, 'bread')

Zadeklaruj kształt sam, jeśli typy nie znają innego zasobu, na przykład za pomocą declare const exports: Record<string, any>. Przeczytaj eksporty w Lua, aby dowiedzieć się, jak zachowują się, gdy docelowy zasób się restartuje.

Buduj, zanim zaczniesz

Uruchom obserwatora podczas pracy i uruchom ponownie zasób w konsoli po każdej zbiórce:

text
npm run watch
restart my_resource

Jeśli restart nic nowego nie robi, sprawdź, czy plik wyjściowy się zmienił, czy manifest na niego wskazuje, i czy zasób nie jest serwowany ze starej kopii w cache/.

Lista kontrolna

Objaw Naprawa
FiveM nie może znaleźć dist/client.js Uruchom kompilację; manifest ładuje wyjście, nie src/
Natives pokazują się jako nieznane w edytorze Zainstaluj @citizenfx/client i @citizenfx/server
import zawodzi w runtime Zbórka z esbuild, aby importy były wstawiane
Handler używa niewłaściwego gracza Przeczytaj source do stałej przed dowolnym await
Wysoka resmon z ticka Dodaj await Delay(ms) do handlera setTick
Lua nie widzi twojej funkcji Zarejestruj ją za pomocą exports('name', fn) i wołaj ją po nazwie zasobu

Szybkie odpowiedzi

Czy mogę używać TypeScript bezpośrednio w zasobie FiveM?

Nie. FiveM uruchamia JavaScript. Piszesz TypeScript, zbiórka go do czystego JavaScript za pomocą narzędzia takiego jak esbuild, i wskazujesz client_script i server_script na pliki wyjściowe.

Czy zasób TypeScript może wywoływać eksporty Lua?

Tak. Eksporty są wspólne między językami: wołaj exports['my_resource'].functionName() z JavaScript, a Lua może wywoływać eksporty, które rejestrujesz za pomocą exports('name', fn) w JavaScript.

Co zastępuje Wait() w JavaScript?

Użyj obietnic wokół setTimeout wewnątrz funkcji async lub handlera setTick. Sama pętla ticków to setTick, a każdy tick powinien czekać opóźnienie, aby nie uruchamiał się każdą ramkę.

Skrypty bez tego problemu

Quest CreatorWizualny edytor questów i dialogów z NPC, budowanych węzeł po węźle w grze.Zobacz skrypt →Item Creator V2Twórz używalne przedmioty z animacjami, propami, efektami i nie tylko — bez pisania kodu.Zobacz skrypt →Shop CreatorZbuduj sklep w niecałą minutę — właściciele, pracownicy, sejfy i napady w zestawie.Zobacz skrypt →

Czytaj dalej