Feb 102017
 

Nextbot – это искусственный интеллект, который используется в Team Fortress 2 и Left 4 Dead. В этом пошаговом руководстве описан простейший ИИ, который будет искать противника (Вас) и преследовать его, пока тот не умрет или не исчезнет с поля зрения. Также он будет выполнять разные произвольные действия, если поблизости нет врагов.

Начнем

Сначала создайте файл с расширением .lua для вашей энтити. Тот, который я сделал для этого руководства расположен в “addons/Nextbot_tut/lua/entities” и называется “simple_nextbot.lua”. Теперь откройте этот файл, чтобы начать добавлять код.

Код

Основные вещи, которые нам требуются для энтити

Начните с определения базовой энтити для последующего использования и установки свойства spawnable. Как и для некоторых других энтити, устанавливаем модель и определяем кое-какие переменные, которыми воспользуемся позже.

AddCSLuaFile()

ENT.Base            = "base_nextbot"
ENT.Spawnable       = true

function ENT:Initialize()

    self:SetModel( "models/hunter.mdl" )

    self.LoseTargetDist = 2000  -- Насколько далеко может уйти противник, чтобы исчезнуть с поля зрения
    self.SearchRadius   = 1000  -- Радиус поиска противника

end

Вещи, связанные с противником

Здесь мы добавим некоторые полезные функции, связанные с противником. НПС/Бот неполноценен, если он не может определить цель, так ведь? Поэтому нам нужны функция для проверки наличия текущего противника и функция поиска противника. Я добавил всякие комментарии, так что вы точно поймете, что делают эти функции.

----------------------------------------------------
-- ENT:Get/SetEnemy()
-- Простые функции, используемые для записи нашего противника
----------------------------------------------------
function ENT:SetEnemy( ent )
    self.Enemy = ent
end
function ENT:GetEnemy()
    return self.Enemy
end

----------------------------------------------------
-- ENT:HaveEnemy()
-- Возвращает true если имеется противник 
----------------------------------------------------
function ENT:HaveEnemy()
    -- Если наш текущий противник валидный
    if ( self:GetEnemy() and IsValid( self:GetEnemy() ) ) then
        -- Если противник слишком далеко
        if ( self:GetRangeTo( self:GetEnemy():GetPos() ) > self.LoseTargetDist ) then
            -- Если противник потерян, то вызывается FindEnemy() для поиска нового
            -- FindEnemy() вернет true если противник найден, что соответственно заставит и эту функцию вернуть положительное значение
            return self:FindEnemy()
        -- Если противник мертв ( мы должны проверить является ли противник игроком до того, как использовать функцию Alive() )
        elseif ( self:GetEnemy():IsPlayer() and !self:GetEnemy():Alive() ) then
            return self:FindEnemy()     -- Вернет false, если ничего не найдено
        end
        -- Противник недалеко и еще живой, поэтому мы можем вернуть true
        return true
    else
        -- Противник невалидный - ищем следующего
        return self:FindEnemy()
    end
end

----------------------------------------------------
-- ENT:FindEnemy()
-- Вернет true и установит нашего противника, если мы его нашли
----------------------------------------------------
function ENT:FindEnemy()
    -- Поиск энтити вокруг нас
    -- Это можно сделать любым удобным вам способом, например, ents.FindInCone() для имитации зрения
    local _ents = ents.FindInSphere( self:GetPos(), self.SearchRadius )
    -- Здесь мы перебираем каждую энтити и ищем желаемую
    for k, v in pairs( _ents ) do
        if ( v:IsPlayer() ) then
            -- Мы нашли нужную, давайте запишем её и вернем true
            self:SetEnemy( v )
            return true
        end
    end
    -- Мы ничего не нашли, поэтому установим нашего противника как nil ( ничего ) и вернем false
    self:SetEnemy( nil )
    return false
end

Корутины

Теперь, когда наш бот умеет искать противника, нам потребуется обучить его некоторым вещам, кроме как просто владеть этим противником. Это будет следующей частью, в которой будет настроена основа нашего ИИ. Корутины (Сопрограммы) – это большая зацикленная часть кода в виде функции, поддерживающая остановки и продолжения выполнения кода с помощью coroutine.wait( время ). Они позволяют делать что-либо в определённом временном порядке. Таким образом, мы можем повернуть бота в сторону противника или воспроизвести анимацию. Это будет работать в цикле до тех пор, пока бот существует. После того, как выполняться все инструкции, бот вернется в начало и снова повторит свои действия. Ниже приведён пример для простейшего бота.

function ENT:RunBehaviour()

    while ( true ) do                           -- Всегда выполняющийся цикл

        self:StartActivity( ACT_WALK )          -- Анимация ходьбы
        self.loco:SetDesiredSpeed( 200 )        -- Скорость ходьбы
        self:MoveToPos( self:GetPos() + Vector( math.Rand( -1, 1 ), math.Rand( -1, 1 ), 0 ) * 400 ) -- Подойти к случайной точке в пределах 400 единиц
        self:StartActivity( ACT_IDLE )          -- Idle анимация
        coroutine.wait( 2 )                     -- Остановиться на 2 секунды

        coroutine.yield()
        -- Функция доходит до сюда, после чего возвращается к началу, где бот прогуливается к следующей точке.
else
    end

end

Проще простого, да? Дойти до случайной точки и ждать две секунды, после чего переходить к следующей. К счастью, это не то, чего мы хотим достигнуть. Следующий шагом мы улучшим наш ИИ.

“Мозг” нашего бота

Несмотря на то, что на первый взгляд этот код выглядит пугающе сложно, на самом деле он довольно прост:

  • Проверить наличие противника и если его нет, то использовать описанную выше функцию HaveEnemy().
  • Если противник есть, то воспроизвести некоторые анимации и бежать к игроку.
  • Если противника все же нет, то двигаться к случайной точке.
  • Остановиться на 2 секунды.

Неплохо, да? Взглянем на код, который я напичкал комментариями так, что Вам будет все предельно понятно.

----------------------------------------------------
-- ENT:RunBehaviour()
-- Тут располагается наш ИИ
----------------------------------------------------
function ENT:RunBehaviour()
    -- Эта функция вызывается во время первого спавна энтити и действует как огромный цикл до тех пор, пока существует наш НПС.
    while ( true ) do
        -- Используем вышеупомянутые функции, чтобы узнать, есть ли у нас противник или мы можем искать нового.
        if ( self:HaveEnemy() ) then
            -- При обнаружении противника, выполняется этот блок кода
            self.loco:FaceTowards( self:GetEnemy():GetPos() )   -- Повернуться к нашему противнику
            self:PlaySequenceAndWait( "plant" )     -- Примем позу, подтверждающую, что противник обнаружен
            self:PlaySequenceAndWait( "hunter_angry" )-- Запускаем анимацию, показывая наше озлобленное настроение
            self:PlaySequenceAndWait( "unplant" )   -- Выйдем из позы "plant"
            self:StartActivity( ACT_RUN )           -- Установим анимацию бега
            self.loco:SetDesiredSpeed( 450 )        -- Установим скорость передвижения. Не волнуйтесь, анимация подстроится под эту скорость
            self.loco:SetAcceleration( 900 )            -- Мы собираемся подбежать к врагу достаточно быстро, поэтому нам нужно быстро ускориться 
            self:ChaseEnemy()                       -- Новая функция по типу MoveToPos, которую Вы скоро увидите.
            self.loco:SetAcceleration( 400 )            -- Установим ускорение к стандартному значению после погони за противником
            self:PlaySequenceAndWait( "charge_miss_slide" ) -- Воиспроизведем анимацию остановки движения
            self:StartActivity( ACT_IDLE )          -- Мы закончили. Возвращаемся в состояние покоя idle
            -- Теперь, после того, как данная функция завершила делать то, что ей нужно, она возвращается к началу
            -- Если что-то дописать после этого оператора условия, то это будет выполнено до нового шага цикла
        else
            -- Противник не найден и нам остается только гулять в округе
            -- Такой же код, как мы использовали в тестовом боте
            self:StartActivity( ACT_WALK )          -- Анимация ходьбы
            self.loco:SetDesiredSpeed( 200 )        -- Скорость ходьбы
            self:MoveToPos( self:GetPos() + Vector( math.Rand( -1, 1 ), math.Rand( -1, 1 ), 0 ) * 400 ) Подойти к случайной точке в пределах 400 единиц
            self:StartActivity( ACT_IDLE )
        end
        -- На этом месте кода бот перестаёт преследовать игрока или идти к случайной точке
        -- Используя функцию ниже, мы ждём 2 секунды до того, как снова повторить все описанные действия
        coroutine.wait( 2 )

    end

end

Что же делает функция ChaseEnemy() ?

Вы, наверное, заметили, что эта функция пока еще нигде не определена и её нет на вики. Она очень похожа на гарисовскую Nextbot функцию MoveToPos, за исключением некоторых полезных изменений:

  • Она строит пусть непосредственно к противнику.
  • Продолжает обновлять путь при изменении позиций бота с противником.
  • Перестает преследовать противника, если он исчез. Используется функция HaveEnemy().

Это выглядит вот так:

----------------------------------------------------
-- ENT:ChaseEnemy()
-- Работает подобно гарисовской функции MoveToPos,
-- за исключением того, что движение зафиксировано
-- за позицией противника до тех пор, пока он им
-- является.
----------------------------------------------------
function ENT:ChaseEnemy( options )

    local options = options or {}

    local path = Path( "Follow" )
    path:SetMinLookAheadDistance( options.lookahead or 300 )
    path:SetGoalTolerance( options.tolerance or 20 )
    path:Compute( self, self:GetEnemy():GetPos() )      -- Вычисляем путь к позиции противника

    if ( !path:IsValid() ) then return "failed" end

    while ( path:IsValid() and self:HaveEnemy() ) do

        if ( path:GetAge() > 0.1 ) then                 -- Поскольку мы следуем за игроком, нам необходимо постоянно корректировать путь
            path:Compute( self, self:GetEnemy():GetPos() ) -- Снова вычисляем путь к позиции противника
        end
        path:Update( self )                             -- Эта функция перемещает бота по вычисленному пути

        if ( options.draw ) then path:Draw() end
        -- Если мы застряли, то вызывается функция HandleStuck и возвращается соответсвеющий результат
        if ( self.loco:IsStuck() ) then
            self:HandleStuck()
            return "stuck"
        end

        coroutine.yield()

    end

    return "ok"

end

Взгляните на гарисовскую функцию и на эту, если хотите увидеть конкретные изменения.

Почти готово

Теперь бот завершен и Вам необходимо только добавить его во вкладку с NPC. Это достаточно просто:

list.Set( "NPC", "simple_nextbot", {
    Name = "Simple bot",
    Class = "simple_nextbot",
    Category = "NextBot"
} )

Здесь замените оба “simple_nextbot” на название файла Вашего бота. Остальные параметры говорят сами за себя.

Задачи

Теперь у Вас есть базовый бот, который бегает по карте и в общем-то, и все. Вы можете попробовать самостоятельно реализовать некоторые из этих вещей, дабы оживить его:

  • Поиск большего, чем просто игроков.
  • Воспроизводить звуки, когда бот блуждает.
  • Поиск противника впереди себя, а не по всей округе.
  • Спрятаться в укрытие, если у противника в руках дробовик.
  • Прекратить преследовать противника, когда он достаточно близко и выполнить атаку в ближнем бою.

Весь код

AddCSLuaFile()

ENT.Base            = "base_nextbot"
ENT.Spawnable       = true

function ENT:Initialize()

    self:SetModel( "models/hunter.mdl" )

    self.LoseTargetDist = 2000  -- Насколько далеко может уйти противник, чтобы исчезнуть с поля зрения
    self.SearchRadius   = 1000  -- Радиус поиска противника

end

----------------------------------------------------
-- ENT:Get/SetEnemy()
-- Простые функции, используемые для записи нашего противника
----------------------------------------------------
function ENT:SetEnemy( ent )
    self.Enemy = ent
end
function ENT:GetEnemy()
    return self.Enemy
end

----------------------------------------------------
-- ENT:HaveEnemy()
-- Возвращает true если имеется противник 
----------------------------------------------------
function ENT:HaveEnemy()
    -- Если наш текущий противник валидный
    if ( self:GetEnemy() and IsValid( self:GetEnemy() ) ) then
        -- Если противник слишком далеко
        if ( self:GetRangeTo( self:GetEnemy():GetPos() ) > self.LoseTargetDist ) then
            -- Если противник потерян, то вызывается FindEnemy() для поиска нового
            -- FindEnemy() вернет true если противник найден, что соответственно заставит и эту функцию вернуть положительное значение
            return self:FindEnemy()
        -- Если противник мертв ( мы должны проверить является ли противник игроком до того, как использовать функцию Alive() )
        elseif ( self:GetEnemy():IsPlayer() and !self:GetEnemy():Alive() ) then
            return self:FindEnemy()     -- Вернет false, если ничего не найдено
        end
        -- Противник недалеко и еще живой, поэтому мы можем вернуть true
        return true
    else
        -- Противник невалидный - ищем следующего
        return self:FindEnemy()
    end
end

----------------------------------------------------
-- ENT:FindEnemy()
-- Вернет true и установит нашего противника, если мы его нашли
----------------------------------------------------
function ENT:FindEnemy()
    -- Поиск энтити вокруг нас
    -- Это можно сделать любым удобным вам способом, например, ents.FindInCone() для имитации зрения
    local _ents = ents.FindInSphere( self:GetPos(), self.SearchRadius )
    -- Здесь мы перебираем каждую энтити и ищем желаемую
    for k, v in pairs( _ents ) do
        if ( v:IsPlayer() ) then
            -- Мы нашли нужную, давайте запишем её и вернем true
            self:SetEnemy( v )
            return true
        end
    end
    -- Мы ничего не нашли, поэтому установим нашего противника как nil ( ничего ) и вернем false
    self:SetEnemy( nil )
    return false
end

----------------------------------------------------
-- ENT:RunBehaviour()
-- Тут располагается наш ИИ
----------------------------------------------------
function ENT:RunBehaviour()
    -- Эта функция вызывается во время первого спавна энтити и действует как огромный цикл до тех пор, пока существует наш НПС.
    while ( true ) do
        -- Используем вышеупомянутые функции, чтобы узнать, есть ли у нас противник или мы можем искать нового.
        if ( self:HaveEnemy() ) then
            -- При обнаружении противника, выполняется этот блок кода
            self.loco:FaceTowards( self:GetEnemy():GetPos() )   -- Повернуться к нашему противнику
            self:PlaySequenceAndWait( "plant" )     -- Примем позу, подтверждающую, что противник обнаружен
            self:PlaySequenceAndWait( "hunter_angry" )-- Запускаем анимацию, показывая наше озлобленное настроение
            self:PlaySequenceAndWait( "unplant" )   -- Выйдем из позы "plant"
            self:StartActivity( ACT_RUN )           -- Установим анимацию бега
            self.loco:SetDesiredSpeed( 450 )        -- Установим скорость передвижения. Не волнуйтесь, анимация подстроится под эту скорость
            self.loco:SetAcceleration( 900 )            -- Мы собираемся подбежать к врагу достаточно быстро, поэтому нам нужно быстро ускориться 
            self:ChaseEnemy()                       -- Новая функция по типу MoveToPos, которую Вы скоро увидите.
            self.loco:SetAcceleration( 400 )            -- Установим ускорение к стандартному значению после погони за противником
            self:PlaySequenceAndWait( "charge_miss_slide" ) -- Воиспроизведем анимацию остановки движения
            self:StartActivity( ACT_IDLE )          -- Мы закончили. Возвращаемся в состояние покоя idle
            -- Теперь, после того, как данная функция завершила делать то, что ей нужно, она возвращается к началу
            -- Если что-то дописать после этого оператора условия, то это будет выполнено до нового шага цикла
        else
            -- Противник не найден и нам остается только гулять в округе
            -- Такой же код, как мы использовали в тестовом боте
            self:StartActivity( ACT_WALK )          -- Анимация ходьбы
            self.loco:SetDesiredSpeed( 200 )        -- Скорость ходьбы
            self:MoveToPos( self:GetPos() + Vector( math.Rand( -1, 1 ), math.Rand( -1, 1 ), 0 ) * 400 ) Подойти к случайной точке в пределах 400 единиц
            self:StartActivity( ACT_IDLE )
        end
        -- На этом месте кода бот перестаёт преследовать игрока или идти к случайной точке
        -- Используя функцию ниже, мы ждём 2 секунды до того, как снова повторить все описанные действия
        coroutine.wait( 2 )

    end

end

----------------------------------------------------
-- ENT:ChaseEnemy()
-- Работает подобно гарисовской функции MoveToPos,
-- за исключением того, что движение зафиксировано
-- за позицией противника до тех пор, пока он им
-- является.
----------------------------------------------------
function ENT:ChaseEnemy( options )

    local options = options or {}

    local path = Path( "Follow" )
    path:SetMinLookAheadDistance( options.lookahead or 300 )
    path:SetGoalTolerance( options.tolerance or 20 )
    path:Compute( self, self:GetEnemy():GetPos() )      -- Вычисляем путь к позиции противника

    if ( !path:IsValid() ) then return "failed" end

    while ( path:IsValid() and self:HaveEnemy() ) do

        if ( path:GetAge() > 0.1 ) then                 -- Поскольку мы следуем за игроком, нам необходимо постоянно корректировать путь
            path:Compute( self, self:GetEnemy():GetPos() ) -- Снова вычисляем путь к позиции противника
        end
        path:Update( self )                             -- Эта функция перемещает бота по вычисленному пути

        if ( options.draw ) then path:Draw() end
        -- Если мы застряли, то вызывается функция HandleStuck и возвращается соответсвеющий результат
        if ( self.loco:IsStuck() ) then
            self:HandleStuck()
            return "stuck"
        end

        coroutine.yield()

    end

    return "ok"

end

list.Set( "NPC", "simple_nextbot", {
    Name = "Simple bot",
    Class = "simple_nextbot",
    Category = "NextBot"
} )

Источник: http://wiki.garrysmod.com/page/NextBot_NPC_Creation

Sorry, the comment form is closed at this time.