Описывать данный раздел я буду на собственных примерах.
Основы
У нас есть карта по вселенной чужих против хищников, где геймплей подобен АОС картам, через определенное время постоянно появляются союзные юниты, и идут атаковать базу противника. При этом у вас под контролем один герой.
У юнитов хищников есть один набор способностей, у чужих - свой, а у людей вообще уникальная механика разных вооружений и боеприпасов. При этом на карте так же существует механика, при которой некоторые люди и хищники падают на землю раненые, и их могут подобрать чужие.
Как же оживить столь разнообразные фракции?
Для начала определим возможные цели для атаки, куда они пойдут при появлении на карте.
Это будет относиться к стратегии
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
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 тактики
Ред. prog
Плюсы - модульность, повторное использование бихевиоров, большая гибкость в рантайме.
Минусы - вызов функций дороже набора ифов, модульность требует чуть больше усилий приложить к продумыванию модулей.
А по поводу статьи
"Каждые 3 секунды юнит будет проверять по количеству врагов вокруг"
А не кажется ли что такой ИИ будет крайне тупым? конечно можно попасть под тик и ИИ набросится сразу и выдаст прокаст действий с задержкой 0.15, это хорошо да, но можно самому напасть на такого и он просто будет куском мяса эти условные 2.9 секунд, насчет движения ещё да согласен, но не на реакции скилов