Создание ИИ

Содержание:

Описывать данный раздел я буду на собственных примерах.

Основы

У нас есть карта по вселенной чужих против хищников, где геймплей подобен АОС картам, через определенное время постоянно появляются союзные юниты, и идут атаковать базу противника. При этом у вас под контролем один герой.
У юнитов хищников есть один набор способностей, у чужих - свой, а у людей вообще уникальная механика разных вооружений и боеприпасов. При этом на карте так же существует механика, при которой некоторые люди и хищники падают на землю раненые, и их могут подобрать чужие.

Как же оживить столь разнообразные фракции?
Для начала определим возможные цели для атаки, куда они пойдут при появлении на карте.
Это будет относиться к стратегии
enum { CHILLOUT, ATTACKING_HIVE, ATTACKING_BASE, ATTACKING_CANYON, A_POINT_1, A_POINT_2, A_POINT_3, A_POINT_4 }
POINT_1 и подобные, это точки которые могут захватить игроки, для получения дополнительно прироста ресурса, с помощью которого делаются апгрейды всех ваших и союзных войск.

Так же, будет 2 типа поведения юнитов. Те, которые появляются и идут в бой на вражескую базу, и те, кто стоит на карте изначально, охраняя территорию
enum { ASSAULT, GUARD } 
так же нужно перечислить все возможные текущие приказы на которые мы будем опираться
enum { NONE, RETREAT, HOLD, RESET, HATCH }
и цели отступления
enum { BASE, HIVE, CANYON }    

Хранение данных

В данном случае, рационально хранить одну структуру с данными на юните. Она хранит все необходимое, и поэтому мы добавим в нее соответствующие переменные для использования в ИИ
// AI data
    int AI_CurrentOrder
    int AI_Destination
    int AI_RetreatPoint
    int AI_Type
    real AI_guard_x, AI_guard_y, AI_point_of_return
AI_CurrentOrder будет принимать у нас значения NONE, RETREAT, HOLD, RESET, HATCH
AI_Destination - CHILLOUT, ATTACKING_HIVE, ATTACKING_BASE и т.д.
AI_RetreatPoint - BASE, HIVE, CANYON
AI_Type - ASSAULT, GUARD

весь код структуры
struct UnitData

    // unit owner
    unit Owner
    int o = 0
    int flag = 0
    
    // for fast strikes
    real MortalStrikeSuccesChance = 0.
    real MortalStrikeFailChance = 0.
    
    // removing data
    bool RemoveAfterDeath = true
    real TimeBeforeRemove = 5.
    timer DeathTimer = CT
    
    // shooting
    timer Cooldown = null
    item LastItem
    int PistolBulletsRemain = 12, ShotgunBulletsRemain = 0, ImpulseRifleBulletsRemain = 0, SmartgunBulletsRemain = 0
    real Evade = 15.
    
    // z values
    real HeightOfHead = 65.
    real UnitHeight = 75.
    real DeathTime = 1.799
    
    // timers
    timer LifeCycleTimer = CT
    timer SlowingTimer = CT
    timer BarrierTimer = CT
    
    timer CastTextTimer
    
    effect Helmet = null, Lamp = null, Misc = null, Firing = null, Glow = null
    
    // bounty
    int min_gold_bounty = 0, max_gold_bounty = 0
    int min_score_bounty = 0, max_score_bounty = 0
    
    // AI data
    int AI_CurrentOrder
    int AI_Destination
    int AI_RetreatPoint
    int AI_Type
    real AI_guard_x, AI_guard_y, AI_point_of_return

    // salvaging
    void remove(){
        GroupRemoveUnit(AI_group, this.Owner)
        Erase(this.LifeCycleTimer)
        Erase(this.Cooldown)
        Erase(this.SlowingTimer)
        Erase(this.BarrierTimer)
        Erase(this.Owner)
        DT(this.DeathTimer)
        DT(this.LifeCycleTimer)
        DT(this.Cooldown)
        DT(this.SlowingTimer)
        DT(this.BarrierTimer)
        this.CastTextTimer = null
            if this.LastItem != null { RemoveItem(this.LastItem); this.LastItem = null }
            if this.Helmet != null { DestroyEffect(this.Helmet) }
            if this.Lamp != null { DestroyEffect(this.Lamp) }
            if this.Misc != null { DestroyEffect(this.Misc) }
            if this.Firing != null { DestroyEffect(this.Firing) }
            if this.Glow != null { DestroyEffect(this.Glow) }
        RemoveItem(UnitItemInSlot(this.Owner, 0))
        RemoveItem(UnitItemInSlot(this.Owner, 1))
        RemoveItem(UnitItemInSlot(this.Owner, 2))
        RemoveItem(UnitItemInSlot(this.Owner, 3))
        RemoveItem(UnitItemInSlot(this.Owner, 4))
        RemoveItem(UnitItemInSlot(this.Owner, 5))
        this.LifeCycleTimer = null
        this.BarrierTimer = null
        this.SlowingTimer = null
        this.DeathTimer = null
        this.Owner = null
        this.destroy(this)
    }
endstruct

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

Заполнять данные для ИИ я буду при вхождении в игровой рект, в частности, идет проверка кто это, чей юнит, и на основе этого идет заполнение данных
	if GetOwningPlayer(u) == Player(6) {
                GroupAddUnit(PredatorsCount, u)
                GroupAddUnit(AI_group, u)
                ud.AI_CurrentOrder = NONE
                  	   if Chance(50.) { ud.AI_Destination = ATTACKING_BASE }
                 	   else  { ud.AI_Destination = ATTACKING_HIVE }
                ud.AI_Type = ASSAULT
                ud.AI_RetreatPoint = CANYON
для тех кто стоял изначально на карте (главные командиры каждой фракции), были такие значения
		ud.AI_Type = GUARD
        ud.AI_guard_x = GetUnitX(u)
		ud.AI_guard_y = GetUnitY(u)
        ud.AI_point_of_return = 1000.

Казалось бы, ну заполнили мы все, что дальше то?

Пара по Некромантии

Вы наверное заметили, что каждого юнита с ии мы добавляли в группу. Так вот, нам необходимо сделать триггер, который будет срабатывать циклически, делая пик по группе. Я остановился на пике юнитов через группу, вместо цикла, так как считаю, что таким образом это распределит нагрузку на несколько подпотоков, потенциально избежав узкого места которое могло вызывать лаги при каждом срабатывании перебора при больших количествах юнитов.
	private void DecisionMaking(){
	...... основная функция

	private void Begin_DM(){
	    ForGroup(AI_group, function DecisionMaking)
	}
	
	...
	TimerStart(AI_Timer, 3., true, function Begin_DM)
	...

Функция DecisionMaking

Это будет та самая функция, которая будет принимать тактические решения.
Вначале, нам нужно сделать проверку целей.
Если у юнита отсутствует текущий приказ, и он просто стоит, нам необходимо отдавать ему приказ снова следовать к своей "глобальной" цели. Так мы убеждаемся, что юнит всегда будет что то делать вместо того что бы стоять и размышлять, но это не нужно если он сражается, для этого мы проверяем присутствие вражеских юнитов рядом.
	FilteringUnit = ud.Owner
    GroupEnumUnitsInRange(g, x, y, 650., Filter(function EnemiesFilterEx))
	
	if ud.AI_CurrentOrder == RESET {
        IssueImmediateOrderById(ud.Owner, order_stop)
        ud.AI_CurrentOrder = NONE
    }
    elseif (ud.AI_CurrentOrder == NONE and GetUnitCurrentOrder(ud.Owner) <= 0) {
        enemies_count = CountGroup(g)
        if (ud.AI_Destination == ATTACKING_HIVE and enemies_count == 0) { 
            IssuePointOrderById(ud.Owner, order_attack,GetRectCenterX(gg_rct_AlienHeroRespawn), GetRectCenterY(gg_rct_AlienHeroRespawn))
        }
        elseif (ud.AI_Destination == ATTACKING_BASE and enemies_count == 0) { 
            IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_MarineHeroRespawn), GetRectCenterY(gg_rct_MarineHeroRespawn))
        }
        elseif (ud.AI_Destination == ATTACKING_CANYON and enemies_count == 0) { 
            IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_PredatorHeroRespawn), GetRectCenterY(gg_rct_PredatorHeroRespawn))
        }
        elseif ud.AI_Destination == A_POINT_1 { 
            IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_Point1), GetRectCenterY(gg_rct_Point1))
        }
        elseif ud.AI_Destination == A_POINT_2 { 
            IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_Point2), GetRectCenterY(gg_rct_Point2))
        }
        elseif ud.AI_Destination == A_POINT_3 { 
            IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_Point3), GetRectCenterY(gg_rct_Point3))
        }
        elseif ud.AI_Destination == A_POINT_4 { 
            IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_Point4), GetRectCenterY(gg_rct_Point4))
        }
        elseif (ud.AI_Destination == CHILLOUT and Chance(35.)) { 
            a = GetRandomReal(0.,360.)
            IssuePointOrderById(ud.Owner, order_attack, x + Rx(GetRandomReal(0.,250.), a), y + Ry(GetRandomReal(0.,250.), a))
        }

можно было бы сделать намного оптимальней используя банально массивы, но я был молод и глуп, так что сорян =(
Здесь можно увидеть, что если задать цель ИИ CHILLOUT , то он с шансом в 35% раз в 3 секунды будет пытаться идти в случайную точку вокруг себя на максимальном расстоянии 250, это поведение подобно стандартной способности Бродячий(нейтральный)

Для рассмотрения примера применения способностей разберем хищника, так как у него самая тривиальная механика ИИ
У него имеются следующие способности:
  • Выстрел из пушки (снаряд в точку)
  • Метание диска (наносит урон, рикошетит от рельефа)
  • Перезарядка (восстанавливает ману, долгий каст)
  • Прыжок (перемещает на определенное расстояние)
Так же, у них могут быть предметы:
  • Мина
  • Сеть
  • Аптечка

Вообще, для использования любых способностей нам нужно знать, есть ли вообще враги рядом, а сколько союзников вокруг, и прочие данные.
	private void DecisionMaking(){
		UnitData ud = GetData(GetEnumUnit())
		group g = CG
		unit targ
		int UNIT_TYPE = GetUnitTypeId(ud.Owner)
		int enemies_count
		int ally_count
		real mp
		real d
		
		...
		// predator AI
	    if (UNIT_TYPE == 'r000' or UNIT_TYPE == 'r001' or UNIT_TYPE == 'R022') {  <- это айди всех юнитов хищников
			FilteringUnit = ud.Owner
	        GroupEnumUnitsInRange(g, x, y, 950., Filter(function EnemiesFilterEx))  <- фильтр берет всех живых и видимых
	        enemies_count = CountGroup(g)  <- количество врагов вокруг
	        targ = RandomFromGroup(g)  <- цель для способностей
			GC(g)
	        FilteringUnit = ud.Owner
	        GroupEnumUnitsInRange(g, Gx(targ), Gy(targ), 150., Filter(function AllyFilter))
	        ally_count = CountGroup(g)   <-  количество союзников вокруг цели
	        d = DBU(ud.Owner, targ)  <- дистанция до цели
	        mp = GetMp(ud.Owner)  <- нужно будет знать количество маны
		....
В целом, нам нужно построить древо которое будет смотреть подходят ли данные для применения способностей, и соответственно строить их нужно так, что бы первыми на проверку шли наиболее приоритетные действия. Если для вас применение защитных заклинаний в приоритете, стоит их вынести повыше.
Здесь же, у меня получилось такое древо

Сначала, попробуем научить хищников применять плазмо пушку.
Сравнивая данные, мы смотрим, будем ли стрелять или нет
			// plasma cannon
            if (d <= 850. and enemies_count > 0 and ally_count == 0 and Chance(15.) and mp > 50.) { 
дистанция до цели должна быть меньше 850, союзников рядом не должно быть, и есть вообще ли мана для выстрела. Chance(15.) (15% шанс, GetRandomReal(0.01, 100.) <= chance) добавлен для добавления элемента случайности.

После этого, можно сделать еще одну проверку. Если условие выше выполнится, то юнит гарантированно будет стрелять, однако так как эта способность с малым временем перезарядки, можно сделать залп или одиночный выстрел. Я делал выбор, из двух залпов, трех, и одиночного.
			if Chance(50.) {   <- шансы 50 на 50, будет ли это залп из 2 выстрелов
                    temp_timer = CT
                    ad = AIData.create()  <- эта структура хранит количество выстрелов и юнита стреляющего, висит на таймере
                    ad.source = ud.Owner
                    ad.target = targ
                    ad.c = 2  <- количество выстрелов
                    TimerStartEx(temp_timer, 0.5, false, function ShoulderCannonLotOfShots, ad)
                    a = GetRandomReal(0.,360.)
                    IssuePointOrderById(ud.Owner, order_darksummoning, Gx(targ) + Rx(GetRandomReal(0.,125.), a), Gy(targ) + Ry(GetRandomReal(0.,125.), a) )   <- добавление небольшой случайности в конечные координаты цели, что бы добавить более интересные залпы по разным точкам, вместо "машинного" точно в цель
                }
                else {  <- останется либо 3 выстрела, либо один
                    a = (Ga(ud.Owner)-180.)+GetRandomReal(-60.,60.)
                    d2 = GetMaxAvailableDistanceEx(ud.Owner, 400., a)  <- функция проверяет максимальное расстояние до указанного (400) до первого препятствия из разрушаемых декораций.
                        if (Chance(30.) and d <= 250. and d2 > 300.) {   <- с шансом 30% и условием что враг почти в упор, и препятствий на расстоянии 300 нет
                            temp_timer = CT
                            ad = AIData.create()
                            ad.source = ud.Owner
                            ad.target = targ
                            ad.c = 3  <-  три выстрела подряд
                            IssuePointOrderById(ud.Owner, order_darkportal, Gx(targ) + Rx(GetRandomReal(250.,d2), a), Gy(targ) + Ry(GetRandomReal(250.,d2), a) )
                            TimerStartEx(temp_timer, 1.2, false, function ShoulderCannonLotOfShots, ad)
                        }
                        else {   <-  одиночный выстрел
                            a = GetRandomReal(0.,360.)
                            IssuePointOrderById(ud.Owner, order_darksummoning, Gx(targ) + Rx(GetRandomReal(0.,125.), a), Gy(targ) + Ry(GetRandomReal(0.,125.), a) )
                        }
                }

На первый взгляд может показаться непонятным, но все станет намного яснее со временем вникания в структуру. Мы делаем такие ветки условий, которые будут учитывать локальные данные
Простейший пример использования метательного диска хищников
			// disc
            elseif (d <= 950. and enemies_count > 0 and Chance(25.) and mp > 15.) {
                a = GetRandomReal(0.,360.)
                IssuePointOrderById(ud.Owner, order_deathanddecay, Gx(targ) + Rx(GetRandomReal(0.,125.), a), Gy(targ) + Ry(GetRandomReal(0.,125.), a) )
                TimerStartEx(CT, 0.15, false, function TimedInvoke, ud)
            }
расстояние до цели меньше 950, с шансом в 25% и мана для использования есть. Вы наверное так же обратили внимание на запуск таймера в функцию TimedInvoke. Я сделал эту функцию, что бы после использования каких либо способностей или предметов, юнит не ждал обновления в 3 секунды которое мы установили, а после 0.15 сек снова переключался на текущую задачу.
функция Invoke
public void Invoke(UnitData ud){
        real x = GetUnitX(ud.Owner), y = GetUnitY(ud.Owner), a
        if ud.AI_CurrentOrder == NONE {
            if (ud.AI_Destination == ATTACKING_HIVE) { 
                IssuePointOrderById(ud.Owner, order_attack,GetRectCenterX(gg_rct_AlienHeroRespawn), GetRectCenterY(gg_rct_AlienHeroRespawn))
            }
            elseif (ud.AI_Destination == ATTACKING_BASE) { 
                IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_MarineHeroRespawn), GetRectCenterY(gg_rct_MarineHeroRespawn))
            }
            elseif (ud.AI_Destination == ATTACKING_CANYON) { 
                IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_PredatorHeroRespawn), GetRectCenterY(gg_rct_PredatorHeroRespawn))
            }
            elseif (ud.AI_Destination == A_POINT_1) { 
                IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_Point1), GetRectCenterY(gg_rct_Point1))
            }
            elseif (ud.AI_Destination == A_POINT_2) { 
                IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_Point2), GetRectCenterY(gg_rct_Point2))
            }
            elseif (ud.AI_Destination == A_POINT_3) { 
                IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_Point3), GetRectCenterY(gg_rct_Point3))
            }
            elseif (ud.AI_Destination == A_POINT_4) { 
                IssuePointOrderById(ud.Owner, order_attack, GetRectCenterX(gg_rct_Point4), GetRectCenterY(gg_rct_Point4))
            }
            elseif (ud.AI_Destination == CHILLOUT and Chance(35.)) { 
                a = GetRandomReal(0.,360.)
                IssuePointOrderById(ud.Owner, order_attack, x + Rx(GetRandomReal(0.,250.), a), y + Ry(GetRandomReal(0.,250.), a))
            }
        }   
    }
    
    
    private void TimedInvoke(){
        Invoke(GetTimerAttach(GetExpiredTimer()))
    }

вернемся к нашим баранам хищникам!
Вот так сделано применение способности перезарядки, которое должно поддерживаться 4 секунды для получения 200 маны
			// recharge
            elseif (enemies_count == 0 and Chance(65.) and mp <= GetMaxMp(ud.Owner) * 0.3) {
                IssueImmediateOrderById(ud.Owner, order_drunkenhaze)
            }
Врагов нет, шанс 65%, и маны меньше чем 30%

Помните я говорил, что есть такая способность как прыжок? Так вот, можно научить ИИ использовать ее для того, что бы быстро перемещаться до цели, а так как сам хищник ближнего боя, это ему будет очень кстати
			// jump
            elseif (d > 200. and d <= 600. and targ != null and Chance(15.) and mp > 20.) {
                a = GetRandomReal(0.,360.)
                IssuePointOrderById(ud.Owner, order_darkportal, Gx(targ) + Rx(GetRandomReal(0.,125.), a), Gy(targ) + Ry(GetRandomReal(0.,125.), a) )
            }
Расстояние до врага больше 200 но меньше 600 (радиус прыжка), цель вообще существует и есть мана на прыжок. И тоже делается случайное смещение в координатах ради более аутентичного поведения.

дальше идут предметы
			elseif (Chance(7.) and UnitHasItemByType(ud.Owner, 'I00E')) {  <- мины, раскидывает в случайную точку вокруг себя в радиусе 500
                a = GetRandomReal(0.,360.)
                UnitUseItemPoint(ud.Owner, GetItemFromUnitByType(ud.Owner, 'I00E'), x + Rx(GetRandomReal(0.,500.), a), y + Ry(GetRandomReal(0.,500.), a))
            }
            elseif (Chance(5.) and GetUnitAbilityLevel(ud.Owner, 'A009') == 0) { <- невидимость, включение
                IssueImmediateOrderById(ud.Owner, order_devour)
                TimerStartEx(ud.DeathTimer, 0.25, false, function TimedInvoke, ud)
            }
            elseif (Chance(5.) and GetUnitAbilityLevel(ud.Owner, 'A008') == 0 and mp <= GetMaxMp(ud.Owner) * 0.25) { <- выключение невидимости
                IssueImmediateOrderById(ud.Owner, order_devourmagic)
                TimerStartEx(ud.DeathTimer, 0.25, false, function TimedInvoke, ud)
            }
			elseif (enemies_count <= 0 and GetHp(ud.Owner) <= GetMaxHp(ud.Owner) * 0.60 and UnitHasItemByType(ud.Owner, 'I00G')) { <- аптечка
                UnitUseItem(ud.Owner, GetItemFromUnitByType(ud.Owner, 'I00G'))
            }
однако если никакие условия вообще не прошли, пушку использовать не может из-за своих вокруг цели, враги рядом есть, диск использовать не хотели, а предметов нет, то с шансом в 75% хищник применяет попытку смертельного удара на ближайшую цель в радиусе 250
			else {
                targ = GetNearestUnit(ud.Owner, 250.)
                    if (Chance(75.) and targ != null and mp > 10.) {
                        IssueTargetOrderById(ud.Owner, order_disenchant, targ)
                    }
            }

Полный блок кода с тактическим применением способностей хищника

кат
	// predator AI
    elseif (UNIT_TYPE == 'r000' or UNIT_TYPE == 'r001' or UNIT_TYPE == 'R022') {
        FilteringUnit = ud.Owner
        GroupEnumUnitsInRange(g, x, y, 950., Filter(function EnemiesFilterEx))
        enemies_count = CountGroup(g)
        targ = RandomFromGroup(g)
        GC(g)
        FilteringUnit = ud.Owner
        GroupEnumUnitsInRange(g, Gx(targ), Gy(targ), 150., Filter(function AllyFilter))
        ally_count = CountGroup(g)
        d = DBU(ud.Owner, targ)
        mp = GetMp(ud.Owner)
            // plasma cannon
            if (d <= 850. and enemies_count > 0 and ally_count == 0 and Chance(15.) and mp > 50.) { 
                if Chance(50.) {
                    temp_timer = CT
                    ad = AIData.create()
                    ad.source = ud.Owner
                    ad.target = targ
                    ad.c = 2
                    TimerStartEx(temp_timer, 0.5, false, function ShoulderCannonLotOfShots, ad)
                    a = GetRandomReal(0.,360.)
                    IssuePointOrderById(ud.Owner, order_darksummoning, Gx(targ) + Rx(GetRandomReal(0.,125.), a), Gy(targ) + Ry(GetRandomReal(0.,125.), a) )
                }
                else {
                    a = (Ga(ud.Owner)-180.)+GetRandomReal(-60.,60.)
                    d2 = GetMaxAvailableDistanceEx(ud.Owner, 400., a)
                        if (Chance(30.) and d <= 250. and d2 > 300.) {
                            temp_timer = CT
                            ad = AIData.create()
                            ad.source = ud.Owner
                            ad.target = targ
                            ad.c = 3
                            IssuePointOrderById(ud.Owner, order_darkportal, Gx(targ) + Rx(GetRandomReal(250.,d2), a), Gy(targ) + Ry(GetRandomReal(250.,d2), a) )
                            TimerStartEx(temp_timer, 1.2, false, function ShoulderCannonLotOfShots, ad)
                        }
                        else {
                            a = GetRandomReal(0.,360.)
                            IssuePointOrderById(ud.Owner, order_darksummoning, Gx(targ) + Rx(GetRandomReal(0.,125.), a), Gy(targ) + Ry(GetRandomReal(0.,125.), a) )
                        }
                }
            }
            // disc
            elseif (d <= 950. and enemies_count > 0 and Chance(25.) and mp > 15.) {
                a = GetRandomReal(0.,360.)
                IssuePointOrderById(ud.Owner, order_deathanddecay, Gx(targ) + Rx(GetRandomReal(0.,125.), a), Gy(targ) + Ry(GetRandomReal(0.,125.), a) )
                TimerStartEx(CT, 0.15, false, function TimedInvoke, ud)
            }
            // recharge
            elseif (enemies_count == 0 and Chance(65.) and mp <= GetMaxMp(ud.Owner) * 0.3) {
                IssueImmediateOrderById(ud.Owner, order_drunkenhaze)
            }
            // jump
            elseif (d > 200. and d <= 600. and targ != null and Chance(15.) and mp > 20.) {
                a = GetRandomReal(0.,360.)
                IssuePointOrderById(ud.Owner, order_darkportal, Gx(targ) + Rx(GetRandomReal(0.,125.), a), Gy(targ) + Ry(GetRandomReal(0.,125.), a) )
            }
            elseif (d <= 800. and enemies_count > 0 and Chance(15.) and UnitHasItemByType(ud.Owner, 'I00F')) { 
                a = GetRandomReal(0.,360.)
                UnitUseItemPoint(ud.Owner, GetItemFromUnitByType(ud.Owner, 'I00F'), Gx(targ) + Rx(GetRandomReal(0.,125.), a), Gy(targ) + Ry(GetRandomReal(0.,125.), a))
            }
            elseif (Chance(7.) and UnitHasItemByType(ud.Owner, 'I00E')) { 
                a = GetRandomReal(0.,360.)
                UnitUseItemPoint(ud.Owner, GetItemFromUnitByType(ud.Owner, 'I00E'), x + Rx(GetRandomReal(0.,500.), a), y + Ry(GetRandomReal(0.,500.), a))
            }
            elseif (Chance(5.) and GetUnitAbilityLevel(ud.Owner, 'A009') == 0) {
                IssueImmediateOrderById(ud.Owner, order_devour)
                TimerStartEx(ud.DeathTimer, 0.25, false, function TimedInvoke, ud)
            }
            elseif (Chance(5.) and GetUnitAbilityLevel(ud.Owner, 'A008') == 0 and mp <= GetMaxMp(ud.Owner) * 0.25) {
                IssueImmediateOrderById(ud.Owner, order_devourmagic)
                TimerStartEx(ud.DeathTimer, 0.25, false, function TimedInvoke, ud)
            }
            elseif (enemies_count <= 0 and GetHp(ud.Owner) <= GetMaxHp(ud.Owner) * 0.60 and UnitHasItemByType(ud.Owner, 'I00G')) {
                UnitUseItem(ud.Owner, GetItemFromUnitByType(ud.Owner, 'I00G'))
            }
            else {
                targ = GetNearestUnit(ud.Owner, 250.)
                    if (Chance(75.) and targ != null and mp > 10.) {
                        IssueTargetOrderById(ud.Owner, order_disenchant, targ)
                    }
            }

добавление шансов в условия необходимо, что бы, во первых, добавить элемент случайности, а во вторых, исключить случаи "машинного калькулейта", когда в идеальных условиях юнит будет применять конкретную одну абилку.

Что же получается?

Каждые 3 секунды юнит будет проверять по количеству врагов вокруг и другим данным возможность применять способности. Мало того, что бы звезды сошлись, необходимо будет еще что бы шанс применения совпал вместе с ними. Это гарантирует, что юнит будет применять все свои способности не всегда, но если он их и применит, то это будет в приемлемых условиях.

Карта с данным ИИ в закрепе, SOON TM будет часть с разбором стратегии и частью 2 тактики

Содержание
`
ОЖИДАНИЕ РЕКЛАМЫ...
2
24
5 лет назад
Отредактирован prog
2
Для героев в варе я предпочитаю делать ИИ немного иначе. На героя вешается триггер входа юнитов в ренж и эти входящие юниты обрабатываются "реагирующей" частью ИИ - добавляются в группу целей в радиусе от героя, возможно получают приоритеты угрозы и уничтожения. "Реагирующая" часть позволяет делать именно это - оперативно реагировать на изменение ситуации вокруг героя - при необходимости тут не только расставляются приоритеты, но вы отдаются "срочные" приказы, ну и события передаваемые в "реагирующую" часть ИИ могут не ограничиваться входом новых целей в радиус. А на периодике висит чистка групп целей от находящихся слишком далеко и "думающая" часть ИИ, которая принимает решения не требующие мгновенной реакции на события (эта часть похожа на то что описано в статье).
Ключевые отличия:
  • принятие решений не всегда требует перебора всех целей в радиусе, если есть известные приоритетные цели, то ИИ будет работать по ним, пока выполняются условия, игнорируя второстепенные цели.
  • реакция на изменение обстановки, ИИ не будет ждать следующего цикла чтобы понять что пришел враг от которого нужно убегать или что мимо пробежала цель с очень низким запасом здоровья или что здоровье упало до критичного минимума и надо что-то с этим делать.
  • отсутствие периодического пика группы, только событие на вход и перебор тех кто входил раньше.

А на Lua это вобще красота - можно себе позволить, например, выдать юниту массив бихевиоров реализованных отдельными функциями. Бихевиоры перебираются и по очереди пытаются выполниться, пока какой-то не выполнится. И по необходимости эти бихевиоры тасуются и меняются в рантайме - получили предмет - добавили бихевиор использования предмета, получили новую способность - добавили бихевиор способности. У способности есть несколько стилей применения - запилили выдачу случайного бихевиора из списка описывающих применение этой способности бихевиоров и, соответственно, получили дешевую и сердитую реализацию "характера" для героя. И так далее. На жассе такое делать - упороться можно, а тут - просто массив функций тасовать.
Плюсы - модульность, повторное использование бихевиоров, большая гибкость в рантайме.
Минусы - вызов функций дороже набора ифов, модульность требует чуть больше усилий приложить к продумыванию модулей.
1
26
5 лет назад
1
Могу сказать, что я еще опишу ии который опирается на так сказать "аггро лист", и более резко решающий что ему делать прямо сейчас. Описанный тут вариант - самый простой, который банально использует способности по каким то критериям. Именно поэтому там пометка на разделе - "простой")
То, что ИИ необходимо как то реагировать мгновенно на изменяющуюся обстановку - безусловно верно, по моей логике это как скармливать обработчику какие либо ивенты, а тот пережевывая решает что ему делать
0
24
5 лет назад
0
Hate, мое дело информацию к размышлению выдать)
2
32
5 лет назад
2
На героя вешается триггер входа юнитов в ренж
так регион же это... как бы не ходит вслед за юнитом, или для меня щас будет открытие... я вообще последний ИИ писал по типу каждый тик (0,1 сек) таймера проверяем всё вокруг и куча ифов (если стоит, если идёт, если есть враг, сильный ли враг, побит ли бот, кастуем абилки, если есть в кого и есть зачем и тд.), нагрузки никакой, ибо сначала проверяют булевки а потом идут переборы групп и прочие вычисления... К примеру если нет маны на скилл или скилл в кд, нет смысла перебирать группу и искать врага... но получается, что всё было зря если можно регистрировать событие входа в зону героя... ну нет же, не было же такого. Регион статичен, можно так входить в зону здания не спорю, а тут вот
А по поводу статьи
"Каждые 3 секунды юнит будет проверять по количеству врагов вокруг"
А не кажется ли что такой ИИ будет крайне тупым? конечно можно попасть под тик и ИИ набросится сразу и выдаст прокаст действий с задержкой 0.15, это хорошо да, но можно самому напасть на такого и он просто будет куском мяса эти условные 2.9 секунд, насчет движения ещё да согласен, но не на реакции скилов
5
26
5 лет назад
5
или для меня щас будет открытие...
А не кажется ли что такой ИИ будет крайне тупым?
да, такой период вполне может привести к тому, что в этот промежуток юнит ничего не скастанет, однако просто атаковать автоатаками он будет сам по себе. Еще нужно учитывать специфику геймплея, сколько в среднем происходит сражение между юнитами, какая динамика. В данном случае, 3 секунды вполне нормальны, можно конечно опустить даже до одной.
Загруженные файлы
1
3
4 года назад
1
Спасибо за прикрепленную карту к уроку
0
23
2 месяца назад
0
Жду 2 часть :)
Чтобы оставить комментарий, пожалуйста, войдите на сайт.