ПрограммированиеФорумИгровая логика и ИИ

Логика объектов в стратегии

#0
20:13, 10 фев 2012

Итак, есть стратегия, есть объекты (рассмотрим пока только юниты). Им можно назначать базовые задания - идти в определенную точку, идти строить определенное здание, идти атаковать определенного юнита. Как реализовать поведение объекта? Ведь любая задача состоит из нескольких подзадач, некоторые иногда зацикливаются. Например, атака. Надо подойти к юниту пару шагов, затем проверить переместился ли он, просчитать новый путь, наконец если подобрались к нему, то можно бить. Если объект убит или недосягаем, то нужно остановить задачу.

Я это представил себе примерно следующим образом. У каждого объекта есть член типа Task. На каждом обновлении объекта он дергается, у каждого Task внутри есть указатель на следующий Task, который будет вызван после успешного выполнения. Но может все совсем иначе делается?

#1
20:16, 10 фев 2012

машины состояний

#2
20:27, 10 фев 2012

Машина состояний это хорошо, но когда одно состояние знает, в которое оно должно "перетечь", или когда машина сама переключает состояние. Но если взять состояние "Идти Туда", то оно может в зависимости от глобальной задачи перейти в "Атаковать", или "Строить", или "Защищаться"...

#3
20:41, 10 фев 2012

> "Идти Туда"
Это не состояние, это действие. (неудобно такое общее понятие считать состоянием)

Состояние - это "Идем к цели", "Идем к стройке",  "Идем к следующей точке маршрута патрулирования".

upd
Можно, правда, "Идти Туда" считать вложенным состоянием. Так более конкретные состояния (типа "Атакуем", "Строим"), в которые оно вложено, тоже смогут параллельно принимать события и делать переходы (в том числе, прекращая действие состояния "Идти Туда").

#4
21:08, 10 фев 2012

Не совсем понял. Состояние состоит из действий? То есть создается состояние Идти За Тем Юнитом И Стрелять В Него, а оно уже на ходу создает действия "Идти в точку 21, 12", когда оно достигнуто, то создает действие "Атаковать"?

#5
0:53, 11 фев 2012

Состояние - это некоторый набор действий, повторяющихся до достижения цели (одной из целей). При достижении цели (одной из целей) состояние меняется.

К примеру состояние "атаковать" длится до тех пор, пока (грубо говоря) не наступают события "противник уничтожен", "противник скрылся" и т.д.

При достижении цели "враг уничтожен" или "враг скрылся" объект в зависимости от других условий может перейти в состояние "патрулировать область", "двигаться к базе противника", "двигаться к своей базе" и т.д.

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

Состояния  "двигаться к базе противника" и "двигаться к своей базе" переходят в другое состояние при достижении соответствующей цели, например в состояние "патрулировать область".

Как-то так.

Учтите, что во избежание зацикливания для каждого состояния должен быть предусмотрен переход в другое состояние. Исключением может является состояние "уничтожен".

#6
1:10, 11 фев 2012

AntonV
> Идти За Тем Юнитом И Стрелять В Него, а оно уже на ходу создает действия "Идти в точку 21, 12", когда оно достигнуто, то создает действие "Атаковать"?
Фактически это будет выглядеть как проверка условий и выполнение соответствующих действий, например:

состояние Атаковать
{
если(противник в пределах атакуемой области и не уничтожен)
    движемся в направлении противника и стреляем
иначе если (противник не в пределах атакуемой области и не уничтожен)
    движемся в направлении противника
иначе если (противник скрылся или противник уничтожен)
    переходим в состояние "патрулировать область"
иначе если (противник нанес критический урон)
    переходим в состояние "уничтожен"

// и т.д.

}
#7
2:17, 11 фев 2012

Barabus
> Фактически это будет выглядеть как проверка условий и выполнение
> соответствующих действий, например
Да, с человеческой точки зрения проблем нету. А в плане алгоритма? Ведь некий поток дергает все время обновление некоторого состояния, и на каждом шаге проверять if/else как-то не оптимально.

#8
2:53, 11 фев 2012

Пофантазировал чуток - процедура обновления юнита..

// к_нолю // отнимает из параметра колво ми-секунд, за которое рисовался пре-кадр.

if( я.режим == режим_мертвяк_остывает )
{
  if( к_нолю( я.время_разложения) ) рециклинг( я);
  // Если время мертво-лежания на поле боя кончилось, и никакой волшебник
  // не успел скастить для юнита "переход из режима мертвяка", то ячейка масива
  // под этот юнит освобождается, линки разрываются. Место для новых юнитов.

  // я.анимация_разложения();
  return;
}

я.продвинуть_свои_детали(); // регенерация, доты-тикалки, откаты пушек или абилок

if( камера_близко( я) ) юнит_шумит( я); // попытка заказа звукового оформления

if( я.режим == режим_в_штаб_за_медалькой )
{ // вместо этого, может быть некое Бегство юнита в тыл, когда ему Мораль сломали.
  if( not к_нолю( я.время_стана) )
  {
    // я.анимация_офигения();
    return; // юнит танцует - игрок видит, что этим юнитом сейчас не порулиш.
  }
  юнит_двигается( я); // исполнение движения по рельсу, который выдан из поиска пути
  return;
}
if( я.подвиг_накоплен >= я.подвиг_очередной_порог )
{ // вместо этого, может накопиться "страх", и юнит побежит с поля боя.
  я.режим == режим_в_штаб_за_медалькой;
  я.время_стана = время_стана_при_получении_медальки;
  выдать_путь( я, штабное_место( я) ); // кто и куда // где находится штаб юнита.
  я.шоры == идите_в_сад; // игнорить всё - главное, получить левел-ап.
    // Свойство важно в других процедурах - не даёт управлять юнитом и подобное.
  return;
}

if( not юнит_приостановлен( я) ) // квэсто-датель гуляет - игрок его кликнул - он стоит.
{
  юнит_двигается( я);
}

юнит_охотится( я); // находит мишень, стреляет, если откаты у пушек к нолю вернулись.
/*
Если его пушки не достают до врага, то важны свойства Приоритета поведения.
Приоритет может быть супер_агресив, тогда юнит будет ехать в позицию врага, пока
все его бортовые пушки не смогут достать до врага, даже если враг будет убегать.
Если приор средний, то юнит не будет сдвигаться, если его самая дально-бойная
пушка достаёт до врага, а когда перестанет доставать, то юнит кинет жребий 50%,
чтобы решить вопрос о кратко-временом преследовании.
Если приор "стоять насмерть", то юнит не будет двигаться за врагом.
Тоесть, должно быть несколько флажков или ради-батонов из которых юнит
будет применять манёвры, которые не считаются "особым режимом", а как-бы
попутные параметры, которые можно пинать-менять случайно, либо от всяких
проклятий, которые прокатывают по нашему юниту.

Важная деталь - у юнита есть, как-миним, два свойства для хранения мишени.
Авто-мишень - её юнит сам обнаружил, когда она попала в радиус дальнего орудия,
либо, когда мишень первая нанесла урон и была вне-тумана войны для нашего штаба.
Штабная мишень - её назначил игрок или бот-мозг - юнит старается убить раньше.
*/

if( я.режим == режим_шляется)
{
  к_нолю( я.время_смены_шляния);

  if( not я.враг_для_стрельбы) // нет номера вражеского юнита.
  {
  if( not я.куда_сейчас_иду) // нет номера клетки, а значит и рельса нет.
  {
  if( not я.время_смены_шляния)
  {
    я.время_смены_шляния = рулетка(3, 5) * 1000; // от 3 до 5 секунд

    if( юнит_далеко( я, я.контрольная_точка) )
    { // надо вернуться к точке привязки
      выдать_путь( я, я.контрольная_точка );
    }
    else if( я.е_список_мест( список_мест_гулять) )
    { // штаб или скрипт вложил в юнит список мест

      if( not я.выбор_мест)
      { // достаём случайный номер клетки, под фильтр "гуляния".
        выдать_путь( я, я.список_мест( 0, список_мест_гулять) );
      }
      else // считаем, что нужен последовательный патруль.
      { // начнут поиск со след-записи после Последнего выданого места.
        я.посл_место = я.список_мест( я.посл_место +1, список_мест_гулять);
        выдать_путь( я, я.посл_место);
      }
    }
    else
    {
      vec3 л = я.позиция + хаос_направ360( земля.ребро_клетки * 5);
      выдать_путь( я, земля.клетка_из_хикс_зэд( л) );
    }
  }
  }
  }
}

if( я.сигнал_с_пульта_управления)
{
  // Если наш юнит является избраным строителем и ...
  // Если игрок, через кнопки на экране, определил "здание" и смог клик-воткнуть
  // три-мерный макет здания так, что он не был красным, то в свойство избраного
  // юнита попадает константа (избран.сигнал_с_пульта_управления = строить_кэш;)
юнит_реагирует_на_сигнал_управа( я);
/*
Внутри процедуры, по нужному кейсу, считываем контекстовую инфу про нужное
здание и нужное место - там делаем проверку на экономическую ситуацию - если
ресурсов не хватает на даный момент, то выдаём игроку диалог, в котором
предлагаем "исполнять по-любасу", "отменить", "создать ярлык в уголке экрана".
Допустим, что игра умная и посчитала ресурсы на момент, когда строитель
доедет до места строительства (это опасно, но я всегда хотел подобное).
  
Другие сигналы управления юнитом, могут менять свойства юнита без сложных
проверок. Например, сменить Приоритет поведения на "следущий по циклу".
Либо приказать "прекратить огонь", либо "бежать на (очередную) базу".
Сигналы подаются бот-мозгом и человеком через единый набор констант.

Вероятно, бот-мозг перебирает юниты по одному, и каждому высылает
некий разумный сигнал.
Если сигнал туповат, то юнит его игнорит, потому-что игроки тоже умеют
тупить и кликать по разным кнопкам и местам, без рабора.
*/

  я.сигнал_с_пульта_управления = 0;
}
#9
4:53, 11 фев 2012

Вот у меня примерно такие машинки (только расчет пути у них не разбит на две стадии, как тут. Поправил немного исходный алгоритм, чтобы это разделение было.).
Все активные состояния лежат в стеке и делают ON_DO каждый тик.
Есть push и pop (мб, еще что-то). pop убирает то состояние, которое его вызвало, и все верхние. При выходе из состояния происходит ON_RESUME у того, которое под ним.
BEGIN, END - это при входах-выходах из состояний.

Угадайте с одного раза, что оно делает. Найдите 3 бага.

void* waypoints;
int next;
int waypoints_cnt;

void ON_BEGIN(state_init_patrol) DO
{
    waypoints = get_path(.path_name="Marker"); // получаем путь
    waypoints_cnt = get_path_len(.path=waypoints); // его длину

    ON_RESUME(state_init_patrol); // просто вызываем функцию ON_RESUME(state_init_patrol), которая добавляет state_patrol
}

void ON_DO(state_init_patrol) DO NOTHING
void ON_RESUME(state_init_patrol) DO
{
    push_state(state_patrol); // добавляем подсостояние state_patrol
}

void ON_END(state_init_patrol) DO NOTHING
//------------------------------------------------------------------------------
void ON_BEGIN(state_patrol) DO
{
    next = get_path_closest_point(.path=waypoints); // берем индекс ближайшей точки пути

    push_state(state_follow_waypoints); // добавляем подсостояние state_follow_waypoints
}

void ON_DO(state_patrol) DO
{
    int enemy = get_closest_unit();
    if (enemy) // если видим кого-то
    {
        set_target(.uid=enemy); // то он будет целью
        pop_state(); // это убирает state_patrol со всеми подсостояниями, которые сейчас работают (state_follow_waypoints и state_move_to_dest)
        push_state(state_attack); // меняем состояние на state_attack
    }
}

void ON_RESUME(state_patrol) DO NOTHING
void ON_END(state_patrol) DO NOTHING
//------------------------------------------------------------------------------
void ON_BEGIN(state_follow_waypoints) DO
{
    set_dest(.pos=path_point(.path=waypoints, .point=next), .dist=0.f); // назначаем, что нужно подойти вплотную к одной из точек маршрута
    push_state(state_move_to_dest); // добавляем подсостояние движения
}

void ON_DO(state_follow_waypoints) DO NOTHING

void ON_RESUME(state_follow_waypoints) DO
{
    next = (next + 1) % waypoints_cnt; // к следующей точке маршрута
    ON_BEGIN(state_follow_waypoints);
}

void ON_END(state_follow_waypoints) DO NOTHING
//------------------------------------------------------------------------------
void ON_BEGIN(state_move_to_dest) DO NOTHING

void ON_DO(state_move_to_dest) DO
{
    if (!approach_dest()) // шагаем. Если уже пришли и никуда идти не надо
        pop_state(); // то уберем state_move_to_dest. Это запустит ON_RESUME(state_follow_waypoints) или ON_RESUME(state_attack)
}

void ON_RESUME(state_move_to_dest) DO NOTHING
void ON_END(state_move_to_dest) DO NOTHING
//------------------------------------------------------------------------------
void ON_BEGIN(state_attack) DO NOTHING

void ON_DO(state_attack) DO
{
    int first_target = get_target();
    int attacker = get_offending_closest_unit();
    int target = attacker && get_dist(.pos=get_pos(.uid=attacker)) < get_dist(.pos=get_pos(.uid=first_target)) ? attacker : first_target; // если еще кто-то другой раздражает, то его тоже надо упомянуть
    
    if (target) // если цель не пропала
    {
        int skill = get_attack_skill();
        float skill_dist = get_skill_max_dist(.skid=skill);
    
        if (get_dist(.pos=get_pos(.uid=target)) >= skill_dist) // если до цели слишком далеко
        {
            set_dest(.pos=get_pos(.uid=target), .dist=skill_dist); // назначаем, что нужно подойти к цели на дистанцию skill_dist (расчет пути)
            
            pop_state(); // убираем state_attack. Это вызовет выход из состояния state_move_to_dest, если оно запущено поверх state_attack
            push_state(state_attack); // входим в чистый state_attack
            // выглядит, как хак, но нет желания сочинять много управляющих функций
            
            push_state(state_move_to_dest); // добавляем подсостояние движения
        }
        else
        {
            set_dest(.pos=get_pos(), .dist=0.f); // запишем, что пришли. Это заставит состояние state_move_to_dest завершиться

            set_target(.uid=target);
            int target_exists = attack(.skid=skill); // атакуем
            set_target(.uid=first_target);
            
            if (!target_exists) // eсли цель умерла или пропала или...
                pop_state(); // то выходим из state_attack. Это запустит ON_RESUME(state_init_patrol)
        }
    }
    else
        pop_state();
}

void ON_RESUME(state_attack) DO NOTHING
void ON_END(state_attack) DO NOTHING
#10
6:56, 11 фев 2012

  Ты пытаешься с помощью ООП решить задачу,
которая не решается таким способом. Оно изначально
не для этого создавалось.

#11
17:29, 11 фев 2012

AntonV
> Ведь некий поток дергает все время обновление некоторого состояния, и на каждом
> шаге проверять if/else как-то не оптимально.
Проверять условия все равно придется. А иначе как Вы узнаете, что цель достигнута и состояние нужно сменить? Даже для того, чтобы объект достиг определенной точки и остановился, необходимо регулярно проверять равенство между текущими и целевыми координатами.

Снижения затрат на производительность можно добиться ограничением итераций обновления состояний в единицу времени. Например, обновлять все состояния не чаще, чем 1/20 секунды, т.е. 1/20 секунды будет квантом игрового времени.

Также, возможно, вам будет интересна реализация состояний в языке UnrealScript движка UE3. Правда, при программировании на C++ эта информация мало чем поможет :)

ПрограммированиеФорумИгровая логика и ИИ

Тема в архиве.