Zurück zum Blog
Guide9 Min. Lesezeit

FiveM-KI-NPC-Verhalten: Klügere NPCs skripten

Erwecke deine FiveM-NPCs zum Leben. Lerne Aufgabenfolgen, Wegfindung, reaktive Dialoge und KI-Tricks für immersivere Rollenspielwelten kennen.

Agency Scripts

Gründer und leitender Entwickler bei Agency Scripts

Warum benutzerdefinierte NPC-KI wichtig ist

Die standardmäßige Umgebungsbevölkerung in GTA V ist für ein Einzelspieler-Erlebnis ausgelegt, nicht für immersives Roleplay. NPCs laufen ziellos umher, reagieren unvorhersehbar auf Aktionen von Spielern und berücksichtigen den Roleplay-Kontext um sie herum nicht. Ein Ladenangestellter steht regungslos da, ein Barkeeper tut nichts hinter dem Tresen und Fußgänger ignorieren laufende Tatorte. Benutzerdefinierte NPC-KI macht aus diesen Pappfiguren glaubwürdige Charaktere, die die Immersion verbessern. Ein gut skriptierter Ladenbesitzer steht tatsächlich hinter der Kasse und begrüßt Kunden. Sicherheitskräfte patrouillieren auf festgelegten Routen und reagieren auf Schüsse. Gangmitglieder verteidigen ihr Gebiet und fliehen, wenn sie in der Unterzahl sind. In dieser Anleitung behandeln wir die wichtigsten nativen Funktionen von GTA V zur Steuerung des NPC-Verhaltens, erstellen ein Task-Sequenzsystem, implementieren Patrouillenrouten, konfigurieren Beziehungsgruppen für realistische Kampfdynamiken und erstellen eine szenariobasierte Umgebungsbevölkerung, die die Welt deines Servers zum Leben erweckt.

Task-Sequenzen: die Grundlage des NPC-Verhaltens

Task-Sequenzen sind geordnete Aktionslisten, die ein Ped nacheinander ausführt. Anders als einzelne Task-Aufrufe, die einander unterbrechen, stellt eine Task-Sequenz sicher, dass jede Aktion abgeschlossen wird, bevor die nächste beginnt. Das ist für komplexe Verhaltensweisen unerlässlich, etwa wenn ein Sicherheitsmitarbeiter zu einer Tür geht, sie öffnet, einen Raum betritt und anschließend Wache steht. Ohne Task-Sequenzen müsstest du den Status manuell mit Timern verfolgen, was fehleranfällig und schwer zu warten ist. Die native OpenSequenceTask und CloseSequenceTask Funktionen erstellen einen Sequenz-Handle, der jedem Ped über TaskPerformSequence.

-- Create a task sequence for a shopkeeper NPC
local function CreateShopkeeperRoutine(ped, counterCoords, counterHeading)
    local seq = 0
    -- Open a new sequence (pass 0 as reference, it returns a handle)
    OpenSequenceTask(seq)

    -- Walk to the counter position
    TaskGoStraightToCoord(0, counterCoords.x, counterCoords.y, counterCoords.z, 1.0, -1, counterHeading, 0.5)

    -- Turn to face customers
    TaskAchieveHeading(0, counterHeading, 2000)

    -- Play idle animation at counter
    TaskStartScenarioInPlace(0, 'PROP_HUMAN_SHOP_INTERACT', 0, true)

    CloseSequenceTask(seq)

    -- Assign sequence to the ped
    TaskPerformSequence(ped, seq)
    ClearSequenceTask(seq)
end

-- Usage
local shopkeeper = CreateNPC('s_m_m_shopkeep_01', vector3(25.7, -1347.3, 29.5))
CreateShopkeeperRoutine(shopkeeper, vector3(25.7, -1347.3, 29.5), 270.0)

Das Szenariosystem

Szenarien sind vordefinierte Animationsschleifen, durch die NPCs aussehen, als würden sie echte Tätigkeiten ausführen. GTA V enthält Hunderte integrierter Szenarien, vom Rauchen und Trinken bis zur Benutzung eines Geldautomaten, zur Arbeit am Schreibtisch oder zu Liegestützen im Park. Mit TaskStartScenarioInPlace für Tätigkeiten an einem festen Ort oder TaskStartScenarioAtPosition für Szenarien an bestimmten Positionen kannst du NPCs passende Aktionen ausführen lassen. Für eine glaubwürdige Umgebungs-KI ist es entscheidend, Szenarien an die jeweiligen Orte anzupassen. Ein Barkeeper sollte WORLD_HUMAN_BARTENDERverwenden, ein Mechaniker in einer Garage sollte WORLD_HUMAN_WELDINGverwenden und NPCs vor einem Nachtclub sollten WORLD_HUMAN_SMOKING oder WORLD_HUMAN_STAND_MOBILE.

-- Scenario-based ambient NPCs with location awareness
local AmbientNPCs = {
    {
        model = 'a_m_y_barman_01',
        coords = vector4(-561.2, 286.3, 82.2, 270.0),
        scenario = 'WORLD_HUMAN_BARTENDER',
        invincible = true,
    },
    {
        model = 's_m_y_doorman_01',
        coords = vector4(-560.8, 293.5, 82.2, 180.0),
        scenario = 'WORLD_HUMAN_GUARD_STAND',
        weapon = 'WEAPON_PISTOL',
    },
    {
        model = 'a_f_y_business_01',
        coords = vector4(-555.0, 290.1, 82.2, 90.0),
        scenario = 'WORLD_HUMAN_STAND_MOBILE',
    },
    {
        model = 's_m_y_ammucity_01',
        coords = vector4(22.0, -1105.0, 29.8, 160.0),
        scenario = 'PROP_HUMAN_SHOP_INTERACT',
        invincible = true,
    },
}

local spawnedPeds = {}

local function SpawnAmbientNPCs()
    for _, data in ipairs(AmbientNPCs) do
        local hash = GetHashKey(data.model)
        RequestModel(hash)
        while not HasModelLoaded(hash) do Wait(10) end

        local ped = CreatePed(4, hash, data.coords.x, data.coords.y, data.coords.z, data.coords.w, false, true)
        SetEntityInvincible(ped, data.invincible or false)
        SetBlockingOfNonTemporaryEvents(ped, true)
        FreezeEntityPosition(ped, false)

        if data.weapon then
            GiveWeaponToPed(ped, GetHashKey(data.weapon), 250, false, true)
        end

        TaskStartScenarioInPlace(ped, data.scenario, 0, true)
        SetModelAsNoLongerNeeded(hash)
        table.insert(spawnedPeds, ped)
    end
end

Patrouillenrouten und Wegpunktnavigation

Patrouillenrouten lassen NPCs in einer Schleife zwischen vordefinierten Wegpunkten wechseln. Das ist für Sicherheitskräfte, Polizisten und Gangwachen unerlässlich. Am einfachsten geht das mit einer sich wiederholenden Task-Sequenz, die das Ped zu jedem Wegpunkt gehen lässt, kurz warten lässt und dann zum nächsten führt. Eine fortschrittlichere Variante verwendet eine Zustandsmaschine, die den aktuellen Wegpunktindex verfolgt und Unterbrechungen sauber behandelt. Wenn eine patrouillierende Wache eine Bedrohung erkennt, sollte sie die Patrouille unterbrechen, sich um die Bedrohung kümmern und anschließend von ihrem zuletzt bekannten Wegpunkt aus weiterpatrouillieren, statt von vorne zu beginnen. Das Patrouillensystem sollte außerdem unterschiedliche Bewegungsgeschwindigkeiten, optionale Szenarioaktionen an jedem Wegpunkt und konfigurierbare Wartezeiten unterstützen.

-- Patrol route system with state management
local PatrolRoutes = {
    police_station_exterior = {
        speed = 1.0, -- walking speed
        waypoints = {
            { coords = vector3(441.0, -982.0, 30.7), heading = 90.0,  wait = 5000, scenario = 'WORLD_HUMAN_GUARD_STAND' },
            { coords = vector3(451.0, -982.0, 30.7), heading = 0.0,   wait = 3000 },
            { coords = vector3(451.0, -993.0, 30.7), heading = 270.0, wait = 5000, scenario = 'WORLD_HUMAN_COP_IDLES' },
            { coords = vector3(441.0, -993.0, 30.7), heading = 180.0, wait = 3000 },
        }
    },
}

local function StartPatrol(ped, routeName)
    local route = PatrolRoutes[routeName]
    if not route then return end

    CreateThread(function()
        local waypointIndex = 1
        while DoesEntityExist(ped) and not IsEntityDead(ped) do
            local wp = route.waypoints[waypointIndex]

            -- Walk to waypoint
            TaskGoStraightToCoord(ped, wp.coords.x, wp.coords.y, wp.coords.z, route.speed, -1, wp.heading, 0.5)

            -- Wait until ped reaches destination
            while DoesEntityExist(ped) and not IsEntityDead(ped) do
                local dist = #(GetEntityCoords(ped) - wp.coords)
                if dist < 1.5 then break end
                Wait(500)
            end

            -- Face the correct direction
            TaskAchieveHeading(ped, wp.heading, 1500)
            Wait(1500)

            -- Play scenario at waypoint if defined
            if wp.scenario then
                TaskStartScenarioInPlace(ped, wp.scenario, 0, false)
                Wait(wp.wait or 5000)
                ClearPedTasks(ped)
            else
                Wait(wp.wait or 3000)
            end

            -- Move to next waypoint (loop back to 1)
            waypointIndex = waypointIndex % #route.waypoints + 1
        end
    end)
end

Beziehungsgruppen und Kampfverhalten

Beziehungsgruppen bestimmen, wie NPCs aufeinander und auf Spieler reagieren. Standardmäßig befinden sich alle NPCs in allgemeinen Beziehungsgruppen mit neutraler Einstellung. Deshalb bekämpfen sich Gangmitglieder nicht gegenseitig und Cops verfolgen Kriminelle nicht automatisch. Mit benutzerdefinierten Beziehungsgruppen kannst du Dynamiken zwischen Fraktionen erstellen, bei denen die Polizei bewaffneten Kriminellen feindlich gesinnt ist, rivalisierende Gangs sich sofort angreifen und Zivilisten vor Kämpfen fliehen. Der SetRelationshipBetweenGroups Native akzeptiert Beziehungsstufen von 0 (Verbündeter) über 3 (neutral) bis 5 (hasserfüllt). Damit wird festgelegt, ob Peds einander bekämpfen, vor einander fliehen oder einander ignorieren. Dieses System bildet die Grundlage für realistische Kämpfe zwischen Fraktionen, Gebietsstreitigkeiten und Reaktionen der Strafverfolgungsbehörden.

-- Relationship group setup for faction-based AI
local RelGroups = {}

local function InitRelationshipGroups()
    -- Create custom groups
    AddRelationshipGroup('GANG_BALLAS', RelGroups)
    AddRelationshipGroup('GANG_FAMILIES', RelGroups)
    AddRelationshipGroup('GANG_VAGOS', RelGroups)
    AddRelationshipGroup('POLICE_CUSTOM', RelGroups)
    AddRelationshipGroup('CIVILIAN', RelGroups)

    -- Gangs hate rival gangs
    SetRelationshipBetweenGroups(5, GetHashKey('GANG_BALLAS'), GetHashKey('GANG_FAMILIES'))
    SetRelationshipBetweenGroups(5, GetHashKey('GANG_FAMILIES'), GetHashKey('GANG_BALLAS'))
    SetRelationshipBetweenGroups(5, GetHashKey('GANG_BALLAS'), GetHashKey('GANG_VAGOS'))
    SetRelationshipBetweenGroups(5, GetHashKey('GANG_VAGOS'), GetHashKey('GANG_BALLAS'))

    -- Police dislike all gangs
    SetRelationshipBetweenGroups(4, GetHashKey('POLICE_CUSTOM'), GetHashKey('GANG_BALLAS'))
    SetRelationshipBetweenGroups(4, GetHashKey('POLICE_CUSTOM'), GetHashKey('GANG_FAMILIES'))
    SetRelationshipBetweenGroups(4, GetHashKey('POLICE_CUSTOM'), GetHashKey('GANG_VAGOS'))

    -- Civilians flee from gangs
    SetRelationshipBetweenGroups(1, GetHashKey('CIVILIAN'), GetHashKey('GANG_BALLAS'))
    SetRelationshipBetweenGroups(1, GetHashKey('CIVILIAN'), GetHashKey('GANG_FAMILIES'))
end

-- Assign a ped to a relationship group
local function SetPedFaction(ped, faction)
    local groupHash = GetHashKey(faction)
    SetPedRelationshipGroupHash(ped, groupHash)
end

Flucht- und Kampfverhalten

Für immersive Kampfszenarien ist es entscheidend, zu steuern, wie NPCs auf Bedrohungen reagieren. Das Standardverhalten in GTA V ist chaotisch: Einige NPCs laufen weg, andere ducken sich und bewaffnete NPCs greifen möglicherweise an oder auch nicht. Benutzerdefiniertes Kampfverhalten nutzt Kampfattribute und Konfigurationsflags, um vorhersehbare, der jeweiligen Rolle entsprechende Reaktionen zu erzeugen. Ein Gangmitglied sollte stehen bleiben und mit geringer Genauigkeit kämpfen, ein ausgebildeter Sicherheitsmitarbeiter sollte Deckung suchen und mit mittlerer Genauigkeit angreifen, ein Zivilist sollte sofort fliehen und eine VIP-Figur sollte sich ducken und um Hilfe rufen. Der SetPedCombatAttributes Native steuert individuelles Verhalten, zum Beispiel ob der Ped Deckung nutzen, blind feuern, Geräusche untersuchen oder fliehen kann, wenn er in der Unterzahl ist.

-- Combat behavior presets
local CombatPresets = {
    gang_member = function(ped)
        SetPedCombatAttributes(ped, 46, true)   -- Can fight armed peds on foot
        SetPedCombatAttributes(ped, 5, true)    -- Can use cover
        SetPedCombatAttributes(ped, 2, true)    -- Can do drivebys
        SetPedCombatAbility(ped, 1)              -- Average combat ability
        SetPedCombatRange(ped, 1)                -- Medium range
        SetPedAccuracy(ped, 30)                  -- Poor accuracy
        SetPedFleeAttributes(ped, 0, false)      -- Don't flee
        SetPedCombatMovement(ped, 2)             -- Offensive movement
    end,

    security_guard = function(ped)
        SetPedCombatAttributes(ped, 46, true)
        SetPedCombatAttributes(ped, 5, true)    -- Use cover
        SetPedCombatAttributes(ped, 21, true)   -- Investigate dead peds
        SetPedCombatAbility(ped, 2)              -- Professional
        SetPedCombatRange(ped, 2)                -- Long range
        SetPedAccuracy(ped, 60)                  -- Good accuracy
        SetPedCombatMovement(ped, 1)             -- Defensive movement
    end,

    civilian = function(ped)
        SetPedFleeAttributes(ped, 2, true)       -- Flee immediately
        SetPedCombatAttributes(ped, 17, true)    -- Can be scared
        SetPedCombatAttributes(ped, 46, false)   -- Cannot fight
        SetPedCombatAbility(ped, 0)              -- Poor fighter
    end,
}

-- Apply a combat preset
local function ApplyCombatPreset(ped, presetName)
    local preset = CombatPresets[presetName]
    if preset then preset(ped) end
end

NPC-Lebenszyklus und Performance verwalten

Das Spawnen von NPCs wirkt sich direkt auf die Server- und Client-Performance aus. Jeder aktive Ped beansprucht CPU-Leistung für KI-Verarbeitung, Physiksimulation und Netzwerksynchronisierung. Eine naive Implementierung, bei der alle NPCs beim Serverstart gespawnt und dauerhaft am Leben gehalten werden, beeinträchtigt die Performance stark, sobald die Population wächst. Verwende stattdessen ein entfernungsbasiertes Spawn-System, bei dem NPCs erst erstellt werden, wenn sich ein Spieler innerhalb einer konfigurierbaren Reichweite befindet, und wieder despawnen, wenn alle Spieler das Gebiet verlassen. Nutze ein räumliches Partitionierungssystem, das die Karte in Zonen unterteilt und NPC-Logik nur für Zonen mit Spielern in der Nähe verarbeitet. Speichere Ped-Handles zwischen und verwende sie wieder, wenn möglich, anstatt NPCs jedes Mal zu löschen und neu zu erstellen, wenn ein Spieler eine Zone betritt und verlässt. Setze SetEntityAsMissionEntity auf false und verwende netzwerkbewusste Erstellungsflags, damit die Game-Engine den NPC-Lebenszyklus im eigenen Streaming-System auf natürliche Weise verwalten kann.

-- Proximity-based NPC spawner with zone management
local SPAWN_RANGE = 80.0
local DESPAWN_RANGE = 120.0
local activeZones = {}

CreateThread(function()
    while true do
        local playerCoords = GetEntityCoords(PlayerPedId())

        for zoneName, zone in pairs(NPCZones) do
            local dist = #(playerCoords - zone.center)

            if dist < SPAWN_RANGE and not activeZones[zoneName] then
                -- Spawn zone NPCs
                activeZones[zoneName] = SpawnZoneNPCs(zone)
            elseif dist > DESPAWN_RANGE and activeZones[zoneName] then
                -- Despawn zone NPCs
                for _, ped in ipairs(activeZones[zoneName]) do
                    if DoesEntityExist(ped) then
                        DeleteEntity(ped)
                    end
                end
                activeZones[zoneName] = nil
            end
        end

        Wait(2000) -- Check every 2 seconds
    end
end)

Alles zusammenführen

Ein produktionsreifes NPC-KI-System vereint all diese Elemente in einem zusammenhängenden, durch Konfigurationsdaten gesteuerten Framework. Die Definition jedes NPCs in deiner Config legt das Modell, den Spawn-Ort, den Verhaltenstyp (stationär, Patrouille, Bewachung), das Kampfprofil, die Beziehungsgruppe und alle auszuführenden Szenarien oder Aufgabenfolgen fest. Das Framework lädt diese Definitionen, verwaltet das Spawnen und Despawnen basierend auf der Nähe von Spielern, verarbeitet Statusänderungen, wenn NPCs durch Kämpfe oder Spielerinteraktionen unterbrochen werden, und gibt Ressourcen frei, wenn NPCs nicht mehr benötigt werden. Das wichtigste Designprinzip ist die Trennung von Daten und Logik: Das gesamte NPC-Verhalten sollte sich ohne Codeänderungen konfigurieren lassen. So können Serverbetreiber neue NPCs hinzufügen, Patrouillenrouten ändern und Kampfparameter ausschließlich über Config-Dateien anpassen. Dank dieses datengetriebenen Ansatzes kann dein KI-System von einer Handvoll Quest-NPCs auf Hunderte von Charakteren in der ganzen Stadt anwachsen, ohne dass Änderungen an der Architektur des zugrunde liegenden Codes nötig sind.

Bereit loszulegen?

Hol dir die Scripts in unserem Shop, oder komm auf den Discord für Support, Updates und einen Blick auf das, was kommt.