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.