Создание AI для карт типа Hero Arena

Добавлен , опубликован
Раздел:
Триггеры и объекты
Эта статья является переводом статьи Blade.dk с сайта WC3Jass.com. Перевод сделан мной (Van Damm)
Этот обучающий материал позволит вам добавить простой, но прикольный AI в карты типа арена. AI, с созданием которого вы познакомитесь, не будет идеальным. Мы создадим AI, который будет атаковать других героев, подбирать вещи, учить и использовать заклинания, но не будет настолько эффективен, как человек.
Тем не менее, когда вы изучите основы, то сможете самостоятельно его улучшить.

Требуется

Знание JASS - это руководство на JASS, поэтому вам понадобиться знать JASS чтобы написать AI. В принципе, это может быть сделано с помощью GUI, но я не рекомендую этого делать из-за утечек памяти, тонн ненужного кода и невозможности использовать RB и систем, основанных на игровом кэше, через GUI. Если вы не знаете JASS, вы можете почитать Руководство по JASS в библиотеке XGM. Также вам будет необходимо знание того, что такое код (rawcode), узнать об этом можно где-то в другом месте.
Система, основанная на игровом кэше и RB - это может быть SCV от Sergey, Local Handle Variables от KaTTaNa или модуль CSCache от Vexorian (часть Caster System). В этом руководстве я буду использовать CSCache.
Присоединённая карта - маленькая карта, созданная мной (Blade.dk), показывает простой AI, который мы здесь сделаем. Важно чтобы у вас была эта карта, так как руководство часто ссылается на неё

Заметки

  • AI, который мы сделаем, не будет играть настолько хорошо, как человек, но это лучше, чем ничего. А когда вы изучите основы, то сможете самостоятельно его улучшить.
  • много номеров (например номера игроков) начинаются с 1 в GUI, хотя в JASS они начинаются с 0. Это это руководство на JASS, поэтому здесь они начинаются с 0.
  • Вы не обязаны делать всё в точности так, как это делаю я; я делаю это по-своему, но если по-вашему лучше, или вам просто так удобнее, - делайте по-своему. Я не идеален, и это руководство не идеально, но надеюсь, что оно кому-то поможет.
  • вы можете использовать AI из моей карты, не создавая своего собственного (если вы сделаете так, укажите Blade.dk в кредитах), но я предлагаю сделать ваш собственный, так как карты могут очень отличаться и потому что вы чему-то научитесь от этого.
  • демо карта, возможно, не лишена багов, и также не самая весёлая. Помните, что эту карту я создал только для того, чтобы показать AI.
  • хочу сказать особое спасибо Vexorian за то, что он дал мне силы написать первый AI для моей карты, за советы по его созданию и за то, что он показал мне свой AI, что помогло улучшить мой.

Инициализация

В первую очередь создайте триггер под названием "AI" с событием "Игрок - Игрок 1 (Красный) покидает игру". Преобразуйте его в JASS. Этот триггер потребуется нам для определения выхода игрока из игры, чтобы запустить AI для него. Сейчас зарегистрировано только событие, когда игрок 0 покидает игру, поэтому мы используем цикл, чтобы зарегистрировать это событие для всех игроков от 0-11.
Мы хотим, чтобы AI использовал способности. Это может звучать сложно, но это не так. Нам только нужно дать команду героям изучить способности, и они будут использовать их сами.
Примечание: ситуация, когда управляемый компьютером герой будет использовать заклинаниевсегда такая же, как и для заклинания, на котором оно основано. Например, если у вас есть своё заклинание, основаное на заклинании "Молчание", оно будет использоваться в ситуациях, когда оно кастуется в melee-играх. Никогда не используйте способность "Канал" как основу для ваших заклинаний, потому что AI никогда их не использует.
Чтобы знать, какие спеллы у героев, мы создаём кэш, в котором будем их хранить.
В карте-образце я создал кэш при инициализации карты и сохранил его в глобальную переменную udg_GameCache. Заметьте, что кэш обязан быть инициализирован до того, как мы начнём его использовать, поэтому я сделаю это в первой функции InitTrig моей карты.
В карте я создаю функцию "SetupSkills". В функции InitTrig триггера AI я использую ExecuteFunc, чтобы вызвать эту функцию из другого потока. Это делается для того, чтобы предотвратить поток инициализации карты от крушения.
Функция SetupSkills
function SetupSkills takes nothing returns nothing
 local string h // создать локальную строковую переменную
// Паладин 
// Здесь мы инициализируем скилы паладина, повторите это и для остальных героев
 set h = UnitId2String('Hpal') // сохранить значение, возвращённое UnitId2String('Hpal') в локальную переменную
 call StoreInteger(udg_GameCache, h, "BaseSkill1", 'AHhb') // Сохранить базовый скил - Благодать как "BaseSkill1"
 call StoreInteger(udg_GameCache, h, "BaseSkill2", 'AHds') // Сохранить Божественный Щит как"BaseSkill2"
 call StoreInteger(udg_GameCache, h, "BaseSkill3", 'AHad') // Сохранить Ауру Защиты как "BaseSkill3"
 call StoreInteger(udg_GameCache, h, "UltimateSkill", 'AHre') // Сохранить Воскрешение как "UltimateSkill"
...
endfunction
Функция InitTrig_AI:
function InitTrig_AI takes nothing returns nothing
 local integer i = 0
 set gg_trg_AI = CreateTrigger()
 loop
   exitwhen i > 11
   call TriggerRegisterPlayerEventLeave( gg_trg_AI, Player(i) )
   set i = i + 1
 endloop
 call TriggerAddAction( gg_trg_AI, function PlayerLeaves )
 call ExecuteFunc("SetupSkills")
endfunction

Запускаем AI для героя

Чтобы контролировать AI, я создам таймер. Я создам функцию под названием "StartAI", которая берёт один аргумент - герой. Эта функция просто создаёт таймер, присоединяет к нему героя и запускает (создайте сейчас функцию "AILoop", которая будет выполняться при окончании отсчёта таймером, но не пишите в неё никакого кода - мы добавим его позднее, но она нужна сейчас чтобы не получить ошибок при компиляции)
Функця AILoop:
function AILoop takes nothing returns nothing
endfunction
Функция StartAI из карты-примера:
function StartAI takes unit hero returns nothing
 local timer m = CreateTimer()
 call AttachObject(m, "hero", hero)
 call TimerStart(m, 0, false, function AILoop)
 set m = null
endfunction
Заметьте, что я стартую таймер как одноразовый (мы вернёмся к этому позднее).
Теперь просто сделайте, чтобы ваша система выбора героев вызывала эту функцию, когда компьютерный игрок выбирает героя и переходите к функции, которая вызывается когда игрок выходит из игры. Проверьте, есть ли у игрока герой, и если у него/неё есть герой, вызовите функцию, которая запускает AI для этого героя.
Например:
function PlayerLeaves takes nothing returns nothing
 local player p = GetTriggerPlayer()
 call DisplayTextToForce(bj_FORCE_ALL_PLAYERS, GetPlayerName(p)+" has left the game.")
 if udg_Hero[GetPlayerId(p)] != null then
   call StartAI(udg_Hero[GetPlayerId(p)])
 endif
 set p = null
endfunction

Заставляем AI что-то делать

Есть несколько вещей, которые нужно сделать, когда таймер заканчивает отсчёт:
· Если герой мёртв, подождать пока он/она воскресится.
· Если герой близок к сметри, приказать ему/ей двигаться к фонтану в центре.
· Если у героя всё нормально с жизнями, проверить, есть ли поблизости враг. Если есть, приказать герою его атаковать, в противном случае проверить, есть ли возле героя вещи, если есть, дать ему/ей приказ "smart" чтобы он/она подобрал её, если нет, просто приказать герою патрулировать случайное место на карте.
· Если герой жив и у него есть неиспользованые очки умений, выучить заклинание.
Начнём с объявления всех переменных. Обратите внимание на переменную 'e' в моей функции, он определяетсколько времени пройдёт до того, как таймер закончит отсчёт снова, так что мы можем ждать меньше, если герой мёртв, или дольше, если он/она атакует. Этапеременная инициализируется со значением 5.
Объявляем локальные переменные:
function AILoop takes nothing returns nothing
 local string a = GetAttachmentTable(GetExpiredTimer())
 local unit h = GetTableUnit(a, "hero")
 local rect i
 local location r
 local real x = GetUnitX(h)
 local real y = GetUnitY(h)
 local group g
 local boolexpr b
 local boolexpr be
 local unit f
 local string o = OrderId2String(GetUnitCurrentOrder(h))
 local real l = GetUnitState(h, UNIT_STATE_LIFE)
 local real e = 5
...
Мы начинаем с проверки, мёртв ли герой. Если это так, присваиваем 'е' зачение 1.5 (так как ждать 5 секунд после воскрешения - это слишком много, мы этого не хотим).
Если жизнь героя (переменная 'l') меньше или равна 0, присваиваем 'е' зачение 1.5 чтобы сделать проверку по таймеру более частой до тех пор, пока герой не воскресится.
...
 if l <= 0 then
   set e = 1.5
 endif
...
Далее, если жизни у героя менее 20% приказываю ему идти к фонтану в центре и присваиваю 'е' зачение 3
...
 if l < GetUnitState(h, UNIT_STATE_MAX_LIFE)/5 then
   call IssuePointOrder(h, "move", GetUnitX(gg_unit_nfoh_0001), GetUnitY(gg_unit_nfoh_0001))
   set e = 3
...
Если у героя всё в порядке со здоровьем, проверяем, стандартный ли у него/неё приказ (чтобы не прерывать чтение заклинаний). Если приказ стандартный, проверяем, есть ли враги в радиусе 500 от героя. Если есть, и это другой герой, просто дайм приказ атаковать (переменную 'e' не изменяем)
function AIFilterEnemyConditions takes nothing returns boolean
 return (GetUnitState(GetFilterUnit(), UNIT_STATE_LIFE) > 0) and IsPlayerEnemy(GetOwningPlayer(GetFilterUnit()), GetOwningPlayer(GetAttachedUnit(GetExpiredTimer(), "hero")))
endfunction  
 
...
 else
   if ((o == "smart") or (o == "attack") or (o == "patrol") or (o == "move") or (o == "stop")  or (o == "hold") or (o == null)) then
 set g = CreateGroup()
 set b = Condition(function AIFilterEnemyConditions)
 call GroupEnumUnitsInRange(g, x, y, 500, b)
 set f = FirstOfGroup(g)
 if f == null then
...
 else
   call IssueTargetOrder(h, "attack", f)
 endif
 call DestroyGroup(g)
 call DestroyBoolExpr(b)
   endif
...
Если вргов не найдено, проверяем, есть ли поблизости вещи. Если вещь найдена и это не руна, проверяем, есть ли у героя свободные слоты инвентаря, и приказываем её подобрать.
function AISetItem takes nothing returns nothing
 set bj_lastRemovedItem=GetEnumItem()
endfunction
 
function AIItemFilter takes nothing returns boolean
 return IsItemVisible(GetFilterItem()) and GetWidgetLife(GetFilterItem()) > 0
endfunction
 
function AIHasEmptyInventorySlot takes unit u returns boolean
 return UnitItemInSlot(u, 0) == null or UnitItemInSlot(u, 1) == null or UnitItemInSlot(u, 2) == null or UnitItemInSlot(u, 3) == null or UnitItemInSlot(u, 4) == null or UnitItemInSlot(u, 5) == null
endfunction
 
...
 if f == null then
   set i = Rect(x-800, y-800, x+800, y+800)
   set be = Condition(function AIItemFilter)
   set bj_lastRemovedItem=null
   call EnumItemsInRect(i, be, function AISetItem)
   if (bj_lastRemovedItem != null) and (GetItemType(bj_lastRemovedItem) == ITEM_TYPE_POWERUP or AIHasEmptyInventorySlot(h)) then
     call IssueTargetOrder(h, "smart", bj_lastRemovedItem)
   else
...
   endif
   call RemoveRect(i)
   call DestroyBoolExpr(be)
...
Если у героя заняты все слоты инвентаря или рядом вещей нет, приказать ему/ей патрулировать случайную точку на карте в поиске новых целей.
...
 else
   set r = GetRandomLocInRect(bj_mapInitialPlayableArea)
   call IssuePointOrderLoc(h, "patrol", r)
   call RemoveLocation(r)
...
Теперь давайте проверим, есть ли у героя неиспользованые очки навыков (сделаем это отдельно от блока атаки/подбора вещи/патрулирования)
Если есть, вызываем функцию, которая учит заклинание герою. Я использовал функцию, сохраняющую, сколько раз она уже выучила способность герою, чтобы придерживаться определённой последовательности при изучении навыков:
function AILearnSkill takes unit h, string a returns nothing
 local integer i = GetTableInt(a, "LearnSkillOrder")+1
 local string hs = GetStoredInteger(udg_GameCache, UnitId2String(GetUnitTypeId(h))
 if i == 1 or i == 4 or i == 8 then
   call SelectHeroSkill(h, hs, "BaseSkill1"))
 elseif i == 2 or i == 5 or i == 9 then
   call SelectHeroSkill(h, hs, "BaseSkill2"))
 elseif i == 3 or i == 7 or i == 10 then
   call SelectHeroSkill(h, hs, "BaseSkill3"))
 elseif i == 6 then
   call SelectHeroSkill(h, hs, "UltimateSkill"))
 endif
 call SetTableInt(a, "LearnSkillOrder", i)
endfunction
...
 if GetHeroSkillPoints(h) > 0 and l > 0 then
   call AILearnSkill(h, a)
 endif
...
Теперь просто заставляем таймер закончить отсчёт вновь через 'e' секунд:
...
 call TimerStart(GetExpiredTimer(), e, true, function AILoop)
...
В конце нужно обнулить локальные переменные:
...
 set h = null
 set i = null
 set r = null
 set g = null
 set b = null
 set f = null
 set be = null
...

Последние замечания

Это - основы, AI может быть намного лучше, но это должно помочь вам начать. Это не должно быть сложным, но если вы только что в первый раз прочитали это обучение, у вас может сложться такое впечатление. Карта была сделана как наглядный пример, так что рекомендую её посмотреть.
Что вы можете добавить для улучшения AI:
  • Попробуйте заставить его находить самого слабого врага из всех ближайших.
  • Попробуйте заставить несколько разных AI игроков работать вместе для убийстваопределённого юнита.
  • Когда основные битвы концентрируются у фонтана, заставьте героя убегать оттуда если у него мало жизней.
  • Заставьте AI писать сообщения согласно ситуации (представьте себе AI игрока, говорящего вам "Сдохни, #¤%ный #¤%!" перед тем как вас убить)
Я надеюсь это кому-то поможет!

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
2
35
18 лет назад
2
По мне, аи проще в менеджере в ве писать :)
Ошибки граматеи правьте в "Последних примеч"
2
23
18 лет назад
2
Да не - грамотно написано... понять не трудно даже ламмерам (В JassScript) Вроде меня... :)
Самый реальный АI для HA можно увидеть в Vexorian Hero Arena...
2
16
18 лет назад
2
Не то чтобы супер, но довольно интересную тему афтар загнул.
Как-никак а придется делать этот чертов AI куда-нибудь, для чего-нибудь...
2
10
18 лет назад
2
А для доты то же ?
3
1
17 лет назад
3
А для AOS Можно
Этот комментарий удален
3
7
16 лет назад
3
Статья отличная но возникает вопрос подойдут ли ети боты к аосу?
3
1
16 лет назад
3
Если настроить правильно, то боты подойдут и для АоС, конечно-же...
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.