Dokumentation

Dokumentation Interact system

3D-Interaktionssystem + NPC-Dialog-Engine, ganz ohne Dependencies. Setz eine Interaktion an jede beliebige Stelle der Map: auf eine Entity, auf eine freie Koordinate oder auf einen NPC, den du im selben Aufruf spawnst. Kein ox_lib, kein Framework, nichts, was du serverseitig einrichten musst.

  • ESX
  • QBCore
  • ox
  • vRP
  • Standalone
01

Installation

  1. Resource ablegen

    Hol dir das Script aus deinem Tebex-Konto und kopier den Ordner avenida_interact/ auf deinem Server nach resources/[avenida]/.

  2. In der server.cfg aktivieren

    Füg ensure avenida_interact hinzu. Vorher muss nichts anderes starten: Seit 1.1.0 hat die Resource keine Dependencies, die Ladereihenfolge ist also egal.

  3. Deine Defaults setzen

    Öffne config/config.lua und stell ein, was für alle Punkte gilt: Label, Taste, die Distanzen showIcon und canInteract, Icon, Farbe, benannte Farben, die Spawn-Distanz der Peds und Config.Hud für das HUD, das während eines Dialogs ausgeblendet wird.

  4. Demos entfernen

    Die Beispiele in config/entities/, config/peds/ und config/positions/ sind aktiv, solange ihre Dateien da sind. Lösch die, die du nicht willst. Das Manifest globt diese Ordner, mehr musst du nicht anfassen.

  5. Hot Reload testen

    Starte den Server und tipp im Spiel /restart avenida_interact. Die Icons verschwinden und kommen sauber zurück, und die Punkte deiner eigenen Resources bleiben erhalten, ohne dass du deine Session neu startest.

02

Konfiguration

Dein erster Interaktionspunkt in 30 Sekunden

Leg eine myscript/client.lua in deiner eigenen Resource an und füg das hier ein. Ein cyanfarbener Marker erscheint ab 30m, der Prompt „E - Hello“ rollt ab 2m aus. Laufen muss dafür nur avenida_interact selbst.

lua
-- myscript/client.lua
exports.avenida_interact:positionRegister({
    coords      = vec3(-265.0, -963.6, 31.2),
    showIcon    = 30.0,           -- marker fades in at 30m
    canInteract = 2.0,            -- label unrolls at 2m
    hintIcon    = 'interact',     -- see the icon list below
    hintColor   = '#22d3ee',      -- hex, or an alias from Config.Colors
    message     = 'Hello',
    bind        = 'E',
    onInteract  = function()
        TriggerEvent('chat:addMessage', { args = { 'Hello, world!' } })
    end,
})
03

Exports

Interact Points

  • positionRegister
    exports.avenida_interact:positionRegister(cfg) --> id

    Registriert einen Punkt auf festen Koordinaten. Nimmt jedes visuelle Feld (hintIcon, hintColor, showIcon, canInteract, offsetZ, blips) plus choices[] für mehrere Tasten auf demselben Punkt. canInteract akzeptiert eine Zahl oder eine Funktion, du kannst den Prompt also an einen Job, ein Item oder einen Cooldown binden. Gibt eine ID zurück.

  • pedRegister
    exports.avenida_interact:pedRegister(cfg) --> id

    Spawnt einen konfigurierten NPC (Modell, animDict und animName oder ein Scenario, Blip), hängt einen Punkt daran und gibt seinen Index zurück. Der Ped ist unverwundbar, flieht nicht und geht nicht in Ragdoll. Er spawnt und verschwindet wieder im Umkreis von Config.PedSpawnRange, standardmäßig 50m. Setz randomComponents = false bei einem Händler mit festem Namen, sonst sieht jeder Spieler ein anderes Outfit.

  • pedRemove
    exports.avenida_interact:pedRemove(idx)

    Neu in 1.1.0. Despawnt einen mit pedRegister registrierten Ped und löscht seine Registrierung, damit ein temporärer Questgeber wieder gehen kann, wenn die Quest vorbei ist.

  • entityRegister
    exports.avenida_interact:entityRegister(cfg) --> id

    Hängt einen Punkt an eine bestehende Entity, die du als cfg.entity übergibst. Unterstützt cfg.bone, um den Prompt an einen Bone statt an das Zentrum der Entity zu hängen. Für alles, was ein anderes Script gespawnt hat: ein Fahrzeug, ein platziertes Prop, ein fallengelassenes Item.

  • entityRegisterByHash
    exports.avenida_interact:entityRegisterByHash(hash, cfg)

    Passiver Watcher: Die Config gilt für jede Entity dieses Modells, für die schon gestreamten und für die, die später auftauchen. Ein einziger Thread deckt alle registrierten Hashes ab und geht sie alle 5s durch. Genau das willst du für ATMs, Automaten und Türen, statt eine Koordinatenliste zu pflegen.

  • entityRemove
    exports.avenida_interact:entityRemove(idx)

    Hängt den von entityRegister zurückgegebenen Punkt wieder ab. Die Entity selbst bleibt unangetastet.

  • interactCreate
    exports.avenida_interact:interactCreate(cfg) --> id

    Punkt zur Laufzeit, nach Bedarf erzeugt und entfernt. Für alles, was nur eine Weile existiert: ein Missionsziel, eine Tasche am Boden, eine Leiche zum Durchsuchen.

  • interactRemove
    exports.avenida_interact:interactRemove(id)

    Entfernt einen mit interactCreate erzeugten Punkt und seinen Blip, falls er einen deklariert hat.

  • unlockLastInteract
    exports.avenida_interact:unlockLastInteract()

    Ein Punkt sperrt sich nach der Benutzung selbst, damit eine gehaltene Taste ihn nicht zweimal auslöst. Ruf das am Ende deiner Aktion auf, um ihn wieder freizugeben. Ist der Spieler noch in Reichweite, kommt der Prompt direkt zurück, sonst wartet er, bis der Spieler wieder in Reichweite kommt.

Prompt API

  • HandleTextUI
    exports.avenida_interact:HandleTextUI(id, data) --> duiHandler

    Erzeugt oder aktualisiert eine eigenständige 3D-Sprechblase, ganz ohne Interaktionspunkt dahinter. Für den Fall, dass du die Sichtbarkeit aus deinem eigenen Thread steuern willst. Gibt den Handler zurück, den du an Draw3DSprite weitergibst.

  • Draw3DSprite
    exports.avenida_interact:Draw3DSprite({ duiHandler, coords, maxDistance, hintOnly? })

    Zeichnet die Sprechblase in Weltkoordinaten aus einem Render-Thread, entzerrt anhand des Seitenverhältnisses. Mit hintOnly = true wird nur der Marker gezeichnet, das ist der Zustand, den ein normaler Punkt aus der Ferne zeigt.

  • CloseTextUI
    exports.avenida_interact:CloseTextUI(id)

    Blendet eine Sprechblase aus, behält ihr DUI aber im Speicher, das erneute Öffnen geht damit ohne Verzögerung.

  • RemoveTextUI
    exports.avenida_interact:RemoveTextUI(id)

    Entfernt eine Sprechblase endgültig und gibt ihr DUI frei. Nimm das, wenn du weißt, dass sie nicht wiederkommt.

  • RemoveTextUIs
    exports.avenida_interact:RemoveTextUIs()

    Entfernt alle aktiven Sprechblasen. Beim Stoppen der Resource passiert das automatisch.

  • IsDuiVisible
    exports.avenida_interact:IsDuiVisible() --> boolean

    Gibt true zurück, solange ein Prompt auf dem Bildschirm ist. Frag den Export ab, damit sich deine eigenen Prompts nicht über eine laufende Interaktion legen.

Key handler

  • HandleHoldTextUI
    exports.avenida_interact:HandleHoldTextUI(id, data) --> duiHandler

    Die Schicht, auf der die Punkte aufbauen: Sie zeichnet einen Prompt an Coords und überwacht dessen Taste aus einem Manager-Thread, der alle 50ms läuft. data nimmt BindToHold (Control-Index), DistanceHold, Coords, ChoicesUI und ChoicesCtrl für einen Prompt mit mehreren Tasten, onCallback(id) und canInteract(id, dist). Löst beim Tastendruck aus, mit 500ms Debounce, und dein Callback läuft in einem pcall, ein Fehler auf deiner Seite reißt den Prompt also nicht mit runter.

  • CloseHoldTextUI
    exports.avenida_interact:CloseHoldTextUI(id)

    Blendet den Prompt aus, ohne die Instanz zu zerstören, für einen sauberen Abgang, wenn der Spieler die Zone verlässt.

  • RemoveHoldTextUI
    exports.avenida_interact:RemoveHoldTextUI(id)

    Zerstört eine Instanz und gibt ihr DUI frei.

  • RemoveHoldTextUIs
    exports.avenida_interact:RemoveHoldTextUIs()

    Zerstört alle aktiven Instanzen.

04

Rezepte

Feste Position mit Map-Blip

Der einfachste Fall: ein Punkt auf festen Koordinaten, mit seinem Blip in derselben Table. Der Marker erscheint ab 30m, das Label ab 2m. Pack das in eine beliebige Client-Datei deiner Resource.

lua
exports.avenida_interact:positionRegister({
    coords      = vec3(149.5, -1040.4, 29.4),
    showIcon    = 30.0,
    canInteract = 2.0,
    hintIcon    = 'bank',
    hintColor   = 'blue',
    message     = 'Use the ATM',
    bind        = 'E',
    onInteract  = function()
        TriggerEvent('myserver-banking:openATM')
    end,
    blips = {
        sprite  = 277,
        color   = 2,
        scale   = 0.8,
        text    = 'ATM',
        display = 4,
    },
})

Jeder ATM der Map, zwei Tasten pro Stück

Der Watcher registriert deine Config einmal pro Modell und wendet sie auf jedes passende Prop an, egal ob es jetzt schon gestreamt ist oder erst später auftaucht. Jede Auswahl trägt ihr eigenes Label, ihre eigene Farbe und ihre eigene condition, der Raubüberfall taucht also nur auf, wenn dein Police-Script es erlaubt. Achte auf den Feldnamen: Eine Auswahl nutzt condition, nicht canInteract.

lua
local ATM_PROPS = {
    'prop_atm_01', 'prop_atm_02', 'prop_atm_03', 'prop_fleeca_atm',
}

for _, prop in ipairs(ATM_PROPS) do
    exports.avenida_interact:entityRegisterByHash(prop, {
        canInteract = 1.2,
        offsetZ     = 1.0,
        hintIcon    = 'bank',
        hintColor   = 'blue',
        choices = {
            {
                bind       = 'E',
                message    = 'Use the ATM',
                onInteract = function()
                    TriggerEvent('myserver-banking:atmUse')
                end,
            },
            {
                bind      = 'G',
                message   = 'Rob the ATM',
                hintColor = 'red',
                condition = function()
                    return exports['myserver-police']:hasEnoughCops(2)
                end,
                onInteract = function(entity)
                    TriggerServerEvent('myserver-robbery:startATM', NetworkGetNetworkIdFromEntity(entity))
                end,
            },
        },
    })
end

NPC mit verzweigtem Dialog und GPS-Waypoint

Ein vollständiger Questgeber: Der Ped spawnt mit Animation und Blip, und die Dialogantworten führen aus, was du willst, hier den Native SetNewWaypoint. changeDialog ersetzt Text und Antworten, du verschachtelst also so tief, wie das Gespräch es braucht. randomComponents = false sorgt dafür, dass Maria bei jedem Spieler dasselbe Outfit trägt.

lua
exports.avenida_interact:pedRegister({
    coords           = vec4(-1393.81, -1064.09, 3.17, 266.09),
    model            = 'a_f_y_vinewood_04',
    animDict         = 'anim@amb@waving@male',
    animName         = 'ground_wave',
    randomComponents = false,
    message          = 'Talk with Maria',
    showIcon         = 10.0,
    canInteract      = 3.0,
    hintIcon         = 'talk',
    hintColor        = 'gold',
    blips = { sprite = 119, color = 50, text = 'Lumberjack', scale = 0.8 },
    dialogue = {
        name     = 'Maria',
        startMsg = 'Hello, are you ready to start your shift?',
        elements = {
            {
                label  = 'Yes, where do I start?',
                action = function(changeDialog, close)
                    SetNewWaypoint(-584.01, 5491.59)
                    changeDialog('The forest is on your GPS now.', {
                        { label = 'Got it!', action = function(_, close) close() end },
                    })
                end,
            },
            {
                label  = 'I\'ll come back later',
                action = function(_, close) close() end,
            },
        },
    },
})
05

Fragen

Brauche ich wirklich kein ox_lib?

Nein. Seit 1.1.0 baut bridges/compat.lua die DUI-Erstellung, die Proximity-Points und die Wait-Helper mit reinen Natives nach, und die Zeile @ox_lib/init.lua ist im Manifest auskommentiert. Wenn du das lieber über ox_lib laufen lässt, setz Config.UseOxLib = true und kommentier die Zeile ein. Beide Wege werden gepflegt, der native ist der Standard.

Mein Marker taucht nicht auf, wo muss ich suchen?

Erstens die Reichweite: showIcon steht in Config.Defaults auf 4m, was wenig ist, wenn du einen Marker aus der Ferne erwartet hast, also übergib deinen eigenen Wert. Zweitens muss die Entity bei entityRegister zum Aufrufzeitpunkt existieren, während entityRegisterByHash auf sie wartet, was bei Game-Props der richtige Weg ist. Drittens setz Config.Debug = true, dann landen interne Fehler in der Client-Konsole.

Meine Auswahl-Bedingung wird ignoriert

Eine Auswahl liest condition, nicht canInteract. canInteract ist das Feld des Punkts selbst, dort nimmt es eine Zahl oder eine Funktion. Innerhalb von choices[] heißt die Sperre condition = function() ... end, und eine Auswahl, deren condition false zurückgibt, wird schlicht nicht gezeichnet.

Welche Farben kann ich benutzen?

Jeden Hex-Wert wie #22d3ee oder einen Namen aus Config.Colors, die mit red, blue, green, gold, purple und white ausgeliefert wird. Diese Table ist escrow-frei, häng also die Palette deines Servers dran und nutz überall deine eigenen Namen. Ein unbekanntes hintIcon wird als Text gezeichnet, du kannst ein Emoji also als schnelles Icon nehmen.

Der Prompt war auf meinem Ultrawide verzerrt

Das ist in 1.1.0 behoben. Die DUI-Textur liegt jetzt fest auf 1920x1080 und wird beim Zeichnen anhand des echten Seitenverhältnisses entzerrt, und das Icon ändert seine Größe nicht mehr mit der Distanz. Die Antwortboxen der Dialoge haben oberhalb von 2560x1440 ebenfalls mehr Höhe bekommen. Zieh das Update, wenn du noch auf 1.0.x bist.

Meine Punkte bleiben sichtbar, wenn meine Resource stoppt

Die Zuordnung läuft über GetInvokingResource(), ein Punkt gehört also der Resource, die den Export aufgerufen hat. Wenn du aus einem gemeinsamen Handler registrierst, der im Kontext einer anderen Resource läuft, richtet sich das Aufräumen nach dieser anderen Resource. Pack den Aufruf in eine Funktion der Resource, der der Punkt gehört. Seit 1.1.0 werden Punkte auch beim Start einer Resource gelöscht, ein Restart lässt also keine Geister mehr zurück.

Wie hänge ich es an ESX, QBCore, Qbox, ox oder vRP?

Du musst gar nichts anhängen: Die Resource kennt dein Framework nicht und redet nie mit deinem Server. Lies den Job oder den Grade des Spielers in canInteract oder in der condition einer Auswahl und ruf deine eigenen Events aus onInteract heraus auf. Alles, was auf deinem Framework läuft, läuft unverändert hinter diesen beiden Funktionen.

Wie viele Punkte kann ich registrieren?

Gemessen mit 100 Punkten: 0.00ms im Leerlauf. Gezeichnet wird nur innerhalb der showIcon-Reichweite, und die Tastenüberwachung läuft alle 50ms für Instanzen in Reichweite. Tausende Punkte über die Map verteilt kosten also nichts, solange sie nicht alle in derselben Straße stehen.

Warenkorb
Spirit RP
Discord