Назад к блогу
Guide8 мин чтения

Поведение NPC с ИИ в FiveM: скриптинг умных NPC

Заставьте ваших NPC в FiveM выглядеть живыми. Последовательности задач, поиск пути, реактивный диалог и AI-трюки для создания более погружающих ролевых миров.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Почему кастомный ИИ NPC важен

Стандартное население GTA V рассчитано на однопользовательский опыт, а не на погружение в ролевую игру. NPC ходят без цели, непредсказуемо реагируют на действия игрока и не понимают контекст ролевой игры вокруг них. Продавец стоит неподвижно, бармен ничего не делает за стойкой, а пешеходы игнорируют активные криминальные сцены. Кастомный AI NPC превращает этих картонных персонажей в правдоподобных героев, усиливающих погружение. Хорошо скриптованный продавец действительно стоит за кассой и приветствует покупателей. Охранники патрулируют назначенные маршруты и реагируют на выстрелы. Члены банд защищают свою территорию и убегают, если их превосходят в силе. В этом руководстве мы рассмотрим основные нативные функции GTA V для управления поведением NPC, построим систему последовательностей заданий, реализуем маршруты патрулирования, настроим группы отношений для реалистичной боевой динамики и создадим сценарное население, оживляющее мир вашего сервера.

Последовательности задач: основа поведения NPC

Последовательности задач , это упорядоченные списки действий, которые пед выполняет последовательно. В отличие от отдельных вызовов задач, которые прерывают друг друга, последовательность задач гарантирует завершение каждого действия перед началом следующего. Это важно для сложного поведения, например, охранника, который подходит к двери, открывает её, входит в комнату и занимает позицию охраны. Без последовательностей задач потребовалось бы ручное отслеживание состояния с таймерами, что ненадёжно и сложно в поддержке. Нативный OpenSequenceTask и CloseSequenceTask функции создают дескриптор последовательности, который можно назначить любому педу через 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)

Система сценариев

Сценарии , это предопределённые циклы анимаций, которые заставляют NPC выглядеть так, будто они выполняют реальные действия. В GTA V есть сотни встроенных сценариев, от курения и питья до использования банкомата, работы за столом или отжиманий в парке. Использование TaskStartScenarioInPlace для стационарных действий или TaskStartScenarioAtPosition для позиционно-специфичных, вы можете заставить NPC выполнять контекстно подходящие действия. Ключ к правдоподобному фоновому ИИ , соответствие сценариев локациям. Бармен должен использовать WORLD_HUMAN_BARTENDER , механик в гараже должен использовать WORLD_HUMAN_WELDING и NPC вне ночного клуба должны использовать WORLD_HUMAN_SMOKING или 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

Маршруты патрулирования и навигация по путевым точкам

Маршруты патрулирования заставляют NPC перемещаться между заранее определёнными путевыми точками по циклу, что необходимо для охранников, полицейских и стражей банд. Самый простой подход использует повторяющуюся последовательность задач, которая ведёт персонажа к каждой путевой точке, делает короткую паузу, затем переходит к следующей. Более продвинутый подход использует конечный автомат, который отслеживает текущий индекс путевой точки и корректно обрабатывает прерывания. Когда патрулирующий охранник обнаруживает угрозу, он должен прервать патруль, устранить угрозу, а затем возобновить патрулирование с последней известной путевой точки, а не начинать сначала. Система патрулирования также должна поддерживать разные скорости движения, опциональные сценарные действия на каждой путевой точке и настраиваемое время ожидания.

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

Группы отношений и поведение в бою

Группы отношений определяют, как NPC реагируют друг на друга и на игроков. По умолчанию все NPC находятся в общих группах отношений с нейтральным статусом, поэтому члены банд не сражаются друг с другом, а полиция не преследует преступников автоматически. Пользовательские группы отношений позволяют создавать динамику фракций, где полиция враждебна к вооружённым преступникам, враждующие банды нападают друг на друга при встрече, а мирные жители убегают с поля боя. SetRelationshipBetweenGroups native принимает уровни отношений от 0 (компаньон) до 3 (нейтральный) и до 5 (ненависть), контролируя, будут ли педы сражаться, убегать или игнорировать друг друга. Эта система является основой для создания реалистичных фракционных войн, территориальных споров и реакций правоохранительных органов.

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

Поведение бегства и боя

Управление реакцией NPC на угрозы критично для погружения в боевые сценарии. Поведение по умолчанию в GTA V хаотично: одни NPC бегут, другие прячутся, вооружённые NPC могут как вступить в бой, так и нет. Пользовательское боевое поведение использует боевые атрибуты и конфигурационные флаги для создания предсказуемых, соответствующих роли реакций. Член банды должен стоять и сражаться с плохой точностью, обученный охранник должен укрываться и вести бой с умеренной точностью, гражданское лицо должно немедленно бежать, а VIP-персонаж должен пригнуться и вызвать помощь. The SetPedCombatAttributes native контролирует индивидуальное поведение, например, может ли пед использовать укрытие, стрелять вслепую, расследовать звуки или убегать при превосходящих силах.

-- 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 и производительностью

Спавн NPC напрямую влияет на производительность сервера и клиента. Каждый активный пэд потребляет ресурсы CPU для обработки ИИ, симуляции физики и сетевой синхронизации. Наивная реализация, которая спавнит всех NPC при запуске сервера и держит их постоянно живыми, сильно снижает производительность с ростом населения. Правильный подход использует спавн на основе близости, когда NPC создаются только при нахождении игрока в настраиваемом радиусе и удаляются, когда все игроки покидают зону. Используйте систему пространственного разбиения, которая делит карту на зоны и обрабатывает логику NPC только для зон с близкими игроками. Кэшируйте дескрипторы пэдов и повторно используйте их, когда возможно, вместо удаления и создания NPC при каждом входе и выходе игрока из зоны. Установите SetEntityAsMissionEntity установить в false и использовать флаги создания с учётом сети, чтобы игровой движок мог естественно управлять жизненным циклом пэда в своей системе стриминга.

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

Объединяя всё вместе

Система AI NPC для продакшена объединяет все эти элементы в единую структуру, управляемую конфигурационными данными. Каждое определение NPC в конфиге указывает модель, место спавна, тип поведения (статичный, патрулирующий, охраняющий), боевой пресет, группу отношений и любые сценарии или последовательности задач для выполнения. Фреймворк загружает эти определения, управляет спавном и деспавном в зависимости от близости игроков, обрабатывает переходы состояний при прерывании NPC боем или взаимодействием с игроком и очищает ресурсы, когда NPC больше не нужны. Главный принцип дизайна , разделение данных и логики: все поведение NPC должно настраиваться без изменений кода, позволяя владельцам серверов добавлять новых NPC, изменять маршруты патруля и корректировать боевые параметры только через конфиги. Такой подход на основе данных позволяет масштабировать систему AI от нескольких квестовых NPC до сотен атмосферных персонажей, населяющих целый город, без архитектурных изменений в коде.

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.