FiveM TypeScript-Ressourcen: fxmanifest, @citizenfx Types und esbuild

Schreibe FiveM-Ressourcen in TypeScript oder JavaScript: fxmanifest mit dist-Dateien, @citizenfx Types, ein esbuild-Bundle, Events, setTick und Wait, und rufe Lua-Exports auf.

Du möchtest Types, async/await und npm-Pakete in deinem FiveM-Skript, aber deine Ressource sind nur .lua-Dateien. FiveM führt JavaScript in seiner eigenen Runtime aus, daher funktioniert TypeScript, solange es zuerst zu JavaScript kompiliert wird. Dieses Handbuch gibt das funktionsfähige Layout: Manifest, Types, Bundler, Events und Ticks und wie man es mit Lua mischt.

Projektlayout

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

Du bearbeitest src/, buildest in dist/, und das Manifest kennt nur dist/.

Das fxmanifest

lua
fx_version 'cerulean'
game 'gta5'

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

Beide sind einfache JavaScript-Dateien. Wenn deine Ressource eine bestimmte JavaScript-Laufzeitversion oder Node-Funktionen auf dem Server benötigt, überprüfe die aktuellen Cfx.re-Dokumente für die Manifest-Optionen, da sich die unterstützten Laufzeiten im Laufe der Zeit geändert haben.

Tipp: Wenn du den Build auslässt, sagt FiveM, dass es dist/client.js nicht finden kann. Führe den Build immer vor ensure oder restart aus.

Installiere Types und einen Bundler

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

@citizenfx/client und @citizenfx/server sind die Type-Pakete für die natives und Cfx.re-Funktionen auf jeder Seite. Ein tsconfig.json, das sie verwendet:

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

TypeScript prüft hier nur Typen (noEmit). esbuild führt das eigentliche Bundling durch:

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"
  }
}

Bundling bedeutet npm-Pakete, die du importierst, werden in die einzelne Ausgabedatei kopiert, sodass der Server node_modules zur Laufzeit nicht benötigt. Überprüfe die Cfx.re-Dokumente für die richtige --platform und --target für die Laufzeit, die deine Artefakte verwenden.

Events

JavaScript hat die gleichen vier Bausteine wie Lua, mit anderen Namen:

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)
})

Auf dem Server befindet sich der Spieler, der das Event ausgelöst hat, im globalen source, wie in Lua. Lese es am Anfang des Handlers in eine Konstante: Nach einem await kann sich die globale auf einen anderen Spieler beziehen. Die gleichen Sicherheitsregeln aus Sicherung von Server-Events gelten, da der Spieler dein onNet von außen auslösen kann. Mehr über das Event-Modell in Client- und Server-Events.

Ticks und Warten

Es gibt standardmäßig keine Frame-Schleife. setTick führt eine Funktion auf jedem Frame aus, und Wait von Lua wird zu einem Promise um 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
})

Ohne Verzögerung läuft der Handler jeden Frame, das gleiche Problem wie eine Lua-Schleife ohne Wait(0): Siehe Wait und Threads erklärt. Verwende eine längere Verzögerung, wenn nichts auf jedem Frame überprüft werden muss.

Für eine einmalige Verzögerung überspringe setTick und warte einfach await Delay(1000) innerhalb einer async-Funktion.

Mischen mit Lua-Exports

Exports funktionieren sprachübergreifend. Registriere einen in JavaScript:

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

Und rufe ihn von einer Lua-Ressource auf:

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

Die andere Richtung, das Aufrufen eines Lua-Exports von TypeScript, geht durch das exports-Objekt:

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

Deklariere die Form selbst, wenn die Types die andere Ressource nicht kennen, z.B. mit declare const exports: Record<string, any>. Siehe Exports in Lua für das Verhalten, wenn die Zielressource neu startet.

Baue vor dem Start

Führe den Watcher aus, während du arbeitest, und starte die Ressource in der Konsole nach jedem Build neu:

text
npm run watch
restart my_resource

Wenn ein Neustart nichts Neues tut, überprüfe, dass sich die Ausgabedatei geändert hat, dass das Manifest auf sie zeigt und dass die Ressource nicht von einer veralteten Kopie in cache/ bedient wird.

Checkliste

Symptom Behebung
FiveM kann dist/client.js nicht finden Führe den Build aus; das Manifest laden die Ausgabe, nicht src/
Natives zeigen sich im Editor als unbekannt Installiere @citizenfx/client und @citizenfx/server
import schlägt zur Laufzeit fehl Bundle mit esbuild, damit Importe inline sind
Handler verwendet den falschen Spieler Lese source vor jedem await in eine Konstante
Hohe Resmon von einem Tick Füge await Delay(ms) zum setTick-Handler hinzu
Lua kann deine Funktion nicht sehen Registriere sie mit exports('name', fn) und rufe sie nach Ressourcennamen auf

Kurze Antworten

Kann ich TypeScript direkt in einer FiveM-Ressource verwenden?

Nein. FiveM führt JavaScript aus. Du schreibst TypeScript, bundest es mit einem Werkzeug wie esbuild zu normalem JavaScript und zeigst client_script und server_script auf die Ausgabedateien.

Kann eine TypeScript-Ressource Lua-Exports aufrufen?

Ja. Exports sind sprachübergreifend: Rufe exports['my_resource'].functionName() von JavaScript auf, und Lua kann Exports aufrufen, die du mit exports('name', fn) in JavaScript registrierst.

Was ersetzt Wait() in JavaScript?

Verwende ein Promise um setTimeout innerhalb einer async-Funktion oder einem setTick-Handler. Die Ticking-Schleife selbst ist setTick, und jeder Tick sollte eine Verzögerung erwartet, damit er nicht auf jedem Frame läuft.

Scripts ohne dieses Problem

Quest CreatorEin visueller Editor für Quests und NPC-Dialoge, Node für Node im Spiel gebaut.Script ansehen →Item Creator V2Erstelle nutzbare Items mit Animationen, Props, Effekten und mehr — ganz ohne Code.Script ansehen →Shop CreatorBau einen Shop in unter einer Minute — Besitzer, Angestellte, Tresore und Überfälle inklusive.Script ansehen →

Weiterlesen