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
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
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.jsnicht finden kann. Führe den Build immer vorensureoderrestartaus.
Installiere Types und einen Bundler
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:
{
"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:
{
"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 |
// src/server.ts
onNet('my_resource:requestData', () => {
const src = (global as any).source as number
emitNet('my_resource:data', src, { hello: 'world' })
})// 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:
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:
exports('getGreeting', (name: string) => `Hello ${name}`)Und rufe ihn von einer Lua-Ressource auf:
local text = exports['my_resource']:getGreeting('Mic')Die andere Richtung, das Aufrufen eines Lua-Exports von TypeScript, geht durch das exports-Objekt:
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:
npm run watch
restart my_resourceWenn 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 →