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.