Wróć do bloga
Guide9 min czytania

Zachowanie NPC AI w FiveM: skryptowanie mądrzejszych NPC

Spraw, by NPC w FiveM ożyli. Sekwencje zadań, pathfinding, reaktywne dialogi i sztuczki AI, aby budować bardziej immersyjne światy roleplay.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Dlaczego własne AI dla NPC ma znaczenie

Domyślna populacja otoczenia w GTA V została zaprojektowana pod rozgrywkę jednoosobową, a nie wciągający roleplay. Postacie NPC chodzą bez celu, reagują nieprzewidywalnie na działania graczy i zupełnie nie rozumieją otaczającego ich kontekstu RP. Sprzedawca w sklepie stoi bez ruchu, barman nic nie robi za ladą, a przechodnie ignorują trwające miejsca zbrodni. Niestandardowe AI postaci NPC zmienia te tekturowe makiety w wiarygodne postacie zwiększające immersję. Dobrze oskryptowany sprzedawca naprawdę stoi za kasą i wita klientów. Ochroniarze patrolują wyznaczone trasy i reagują na strzały. Członkowie gangów bronią swojego terytorium i uciekają przy przewadze wroga. W tym poradniku omówimy kluczowe funkcje natywne GTA V do kontrolowania zachowań NPC, zbudujemy system sekwencji zadań, wdrożymy trasy patroli, skonfigurujemy grupy relacji dla realistycznej dynamiki walki oraz stworzymy populację otoczenia opartą na scenariuszach, która ożywi świat twojego serwera.

Sekwencje zadań: Podstawa zachowania NPC

Sekwencje zadań to uporządkowane listy akcji, które ped wykonuje po kolei. W przeciwieństwie do pojedynczych wywołań zadań, które przerywają się nawzajem, sekwencja zadań gwarantuje ukończenie każdej akcji przed rozpoczęciem kolejnej. Jest to kluczowe w przypadku złożonych zachowań, na przykład ochroniarza, który podchodzi do drzwi, otwiera je, wchodzi do pokoju i zajmuje pozycję na warcie. Bez sekwencji zadań konieczne byłoby ręczne śledzenie stanu za pomocą timerów, co jest zawodne i trudne w utrzymaniu. Funkcja natywna OpenSequenceTask i CloseSequenceTask funkcje tworzą sequence handle, który można przypisać do dowolnego peda za pomocą 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)

System scenariuszy

Scenariusze to predefiniowane pętle animacji, dzięki którym NPC wyglądają, jakby wykonywali prawdziwe czynności. GTA V zawiera setki wbudowanych scenariuszy, od palenia i picia po korzystanie z bankomatu, pracę przy biurku lub robienie pompek w parku. Używanie TaskStartScenarioInPlace do aktywności stacjonarnych lub TaskStartScenarioAtPosition w przypadku tych zależnych od pozycji możesz sprawić, by NPC wykonywali działania odpowiednie do kontekstu. Kluczem do wiarygodnego ambientowego AI jest dopasowanie scenariuszy do lokalizacji. Barman powinien używać WORLD_HUMAN_BARTENDER, mechanik w warsztacie powinien używać WORLD_HUMAN_WELDING, a NPC przed klubem nocnym powinni używać WORLD_HUMAN_SMOKING lub 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

Trasy patrolowe i nawigacja po waypointach

Trasy patrolowe sprawiają, że NPC poruszają się w pętli między zdefiniowanymi waypointami, co jest kluczowe dla ochroniarzy, policjantów i wartowników gangów. Najprostsze podejście wykorzystuje powtarzalną sekwencję zadań, która prowadzi peda do każdego waypointu, czeka chwilę, a następnie przechodzi do kolejnego. Bardziej zaawansowane rozwiązanie opiera się na maszynie stanów, która śledzi aktualny indeks waypointu i płynnie obsługuje przerwania. Gdy patrolujący strażnik wykryje zagrożenie, powinien przerwać patrol, zneutralizować zagrożenie i wznowić patrolowanie od ostatniego znanego punktu, zamiast zaczynać od początku. System patrolowy powinien również obsługiwać różne prędkości poruszania się, opcjonalne scenariusze działań przy każdym waypoincie oraz konfigurowalny czas oczekiwania.

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

Grupy relacji i zachowanie w walce

Grupy relacji określają, jak NPC reagują na siebie nawzajem i na graczy. Domyślnie wszyscy NPC należą do ogólnych grup relacji o neutralnym nastawieniu, dlatego członkowie gangów nie walczą ze sobą, a policjanci nie ścigają automatycznie przestępców. Własne grupy relacji pozwalają tworzyć dynamikę frakcji, w której policja jest wroga wobec uzbrojonych przestępców, rywalizujące gangi atakują się na widok, a cywile uciekają przed walką. Natyw SetRelationshipBetweenGroups natywna funkcja przyjmuje poziomy relacji od 0 (towarzysz) przez 3 (neutralny) do 5 (nienawiść), kontrolując, czy pedy będą walczyć, uciekać, czy ignorować się nawzajem. System ten stanowi podstawę tworzenia realistycznych wojen frakcji, sporów terytorialnych i reakcji służb porządkowych.

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

Zachowanie ucieczki i walki

Kontrolowanie reakcji NPC na zagrożenia jest kluczowe dla immersyjnych scenariuszy walki. Domyślne zachowanie w GTA V jest chaotyczne: niektórzy NPC uciekają, inni kulą się ze strachu, a uzbrojeni NPC mogą podjąć walkę lub nie. Własne zachowanie bojowe wykorzystuje atrybuty walki i flagi konfiguracyjne do tworzenia przewidywalnych reakcji dopasowanych do roli. Członek gangu powinien stać i walczyć z niską celnością, wyszkolony ochroniarz powinien schować się za osłoną i walczyć z umiarkowaną celnością, cywil powinien natychmiast uciekać, a postać VIP powinna przykucnąć i wezwać pomoc. Ten SetPedCombatAttributes native kontroluje poszczególne zachowania, na przykład to, czy ped może korzystać z osłon, strzelać na oślep, badać źródła dźwięków lub uciekać przy przewadze wroga.

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

Zarządzanie cyklem życia i wydajnością NPC

Spawnowanie NPC ma bezpośredni wpływ na wydajność serwera i klienta. Każdy aktywny ped zużywa cykle procesora na przetwarzanie AI, symulację fizyki i synchronizację sieciową. Naiwna implementacja, która spawnuje wszystkich NPC przy starcie serwera i utrzymuje ich na stałe, drastycznie obniży wydajność wraz ze wzrostem liczby graczy. Prawidłowe podejście wykorzystuje spawnowanie oparte na odległości: NPC są tworzeni tylko wtedy, gdy gracz znajduje się w konfigurowalnym zasięgu, i usuwani, gdy wszyscy gracze opuszczą dany obszar. Używaj systemu partycjonowania przestrzennego, który dzieli mapę na strefy i przetwarza logikę NPC tylko dla stref z graczami w pobliżu. Cachuj uchwyty pedów i używaj ich ponownie, kiedy to możliwe, zamiast usuwać i tworzyć NPC za każdym razem, gdy gracz wchodzi do strefy i z niej wychodzi. Ustaw SetEntityAsMissionEntity na false i użyj flag tworzenia uwzględniających sieć, aby silnik gry mógł w naturalny sposób zarządzać cyklem życia pedów w ramach własnego systemu streamingu.

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

Podsumowanie całości

Produkcyjny system AI dla NPC łączy wszystkie te elementy w spójny framework oparty na danych konfiguracyjnych. Każda definicja NPC w Twoim configu określa model, miejsce spawnu, typ zachowania (stacjonarny, patrolujący, pilnujący), preset walki, grupę relacji oraz wszelkie scenariusze lub sekwencje zadań do wykonania. Framework ładuje te definicje, zarządza spawnem i despawnem w oparciu o bliskość graczy, obsługuje przejścia między stanami, gdy działanie NPC zostanie przerwane przez walkę lub interakcję z graczem, oraz czyści zasoby, gdy NPC nie są już potrzebni. Najważniejszą zasadą projektową jest oddzielenie danych od logiki: całe zachowanie NPC powinno być konfigurowalne bez modyfikacji kodu, co pozwala właścicielom serwerów dodawać nowych NPC, modyfikować trasy patroli i dostosowywać parametry walki wyłącznie za pomocą plików konfiguracyjnych. To podejście oparte na danych oznacza, że Twój system AI może skalować się od kilku NPC z zadaniami do setek postaci otoczenia zaludniających całe miasto bez konieczności wprowadzania zmian architektonicznych w kodzie.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.