Искусство войны. Осваиваем скрипты

Материал из Бронетанковой Энциклопедии — armor.kiev.ua/wiki
Перейти к: навигация, поиск

Одним из главных недостатков любой военной игры из серии «Искусство войны» вполне справедливо называется малое число игровых миссий. Однако сама игра снабжена редакторами карт и миссий, которые позволяют создавать антураж и сценарии, не уступающие включённым в базовый комплект программного продукта. Как справедливо отмечено критикой, их использование требует определённого умения, но при этом абсолютно не нужно быть асом программирования как Дейкстра или создатели RT-11. Немножко логики и понимания основ заложенной в программный продукт сценарной машины — и работающие скрипты, позволяющие получить от игры удовольствие, обеспечены. Чтобы показать, что «не боги горшки обжигают», давайте вместе с автором пройдёмся по этапам создания скриптов несложной, но вполне интересной по прохождению миссии «Forza, Carristi!» для дополнения «Итальянский вариант» первой инкарнации «Искусство войны. Африка 1943».

Постановка задачи

Именно с этого всегда надо начинать. После некоторых раздумий сценарий на одной из стандартных карт для мультиплеера выкристаллизовался в следующее: танковому и берсальерскому взводам итальянской дивизии «Чентауро» надо захватить занятую британцами деревню, которые они используют как снабженческий и коммуникационный пункт. К деревне от стартовых позиций ведут два непересекающихся пути, на каждом из которых итальянцев ждут нелёгкие испытания. Один проход защищается мощной группировкой из двухфунтовых противотанковых орудий и станковых пулемётов, а другой проходит мимо малюсенького населённого пункта, где расположилось британское пехотное отделение с наличием противотанкового ружья системы Бойса и два «Крузейдера». Один вражеский танк вооружён двухфунтовой пушкой, второй — трёхдюймовой гаубицей. Сама деревня-цель атаки защищена тремя отделениями британской пехоты с наличием противотанковых ружей системы Бойса. Для некоторого усложнения задачи игроку его непосредственный начальник, майор Луиджи Ферретти, не разрешил ввести в бой все имеющиеся силы (5 танков М14/41 и 4 берсальерских отделения сразу) и оставил в резерве два танка и два отделения. Таким образом, в составе контролируемых игроком сил 3 М14/41 и 2 отделения. Распылять их, пытаясь атаковать по обоим путям сразу — верный путь к поражению, поэтому игроку предстоит либо осторожно, шаг за шагом, в тесном взаимодействии танков и пехоты продавливать оборону вражеской группировки с орудиями и пулемётами, либо решиться на танковый бой с «Крузейдерами». В этом случае эффективным против М14/41 является один 40-мм ствол, гаубица второго «Крузейдера» «близкой поддержки» и ПТР системы Бойса могут при удаче лишь обездвижить наши танки, что весьма неприятно (они нужны для штурма деревни), но в целом ещё не фатально. Но радио у британцев есть и в случае невозможности огня из этой 40-мм пушки (уничтожения «Крузейдера», повреждения его орудия или полной гибели экипажа), а следовательно и защитить проход от наших танков они вызывают подкрепление — два бронеавтомобиля «Даймлер», которые появляются с тыла и постараются пройти угрожаемым проходом к деревне, чтобы усилить её защиту, по пути истребляя всех попадающихся им на глаза итальянцев. Вне зависимости от выбранного игроком пути в случае больших потерь майор Ферретти передаёт нам под командование резерв для выполнения задачи. Пусть также на основании данных разведки наш командир предупредит нас при подходе к тому или иному проходу, с чем нам предстоит иметь дело.

Таков общий план миссии. Осталось только уточнить частности — деревню считаем захваченной и задачу выполненной, если в её области находится минимум десять наших солдат, причём это число включает членов экипажей танков, как в своих машинах, так и вне их; а из вражеских сил осталось в живых не более двух Томми. Если эти условия выполнены, посчитаем выживших британских солдат крайне деморализованными, бросившими оружие, сдавшимися в плен. Очевидно, что если у нас осталось менее десяти берсальеров и танкистов (вне зависимости от числа и состояния танков), то поставленная задача не может быть выполнена и это будет условием поражения. Определим также взгляды майора Ферретти на то, когда стоит вводить в бой резерв. Пусть он сочтёт это нужным, если у нас из первоначальных сил не осталось больше ни одного исправного танка (танк считается исправным, если у него не повреждены ходовая часть, основное вооружение и в нём присутствует хотя бы один член экипажа) или общее число берсальеров и танкистов стало меньше 15 человек.

Программируем скрипты

Когда задача стала ясной, не стоит пугаться её кажущегося объёма — и слона можно съесть по кусочкам. Сначала приступаем к «разрезке» этого «слона» на эти самые кусочки, но перед этим следует немного сказать пару слов о принципе работы скриптовой машины «Искусства войны». Поведение и своих, и вражеских войск управляется последовательностями управляющих инструкций, которые сгруппированы в блоки — так называемые триггера. Проводя аналогию с заводским цехом, можно сказать, что триггера - это рабочие. Сколько их работает в данный момент, определяется текущей обстановкой, если нужно, то один рабочий может позвать другого на помощь или отпустить помощника отдыхать в курилку, завершить работу, передать её результаты другому рабочему и пойти в курилку сам. Точно также и триггера — в зависимости от ситуации могут запускать и останавливать другие триггера, завершать свою выполненную работу, чтобы не занимать более ресурсы процессора компьютера. Работающий в данный момент триггер называется активным, их может быть несколько и они выполняются одновременно (в рамках процессорной «многозадачности» — когда процессор быстро-быстро переключается с одного дела на другое, в итоге за большое время в успевая сделать всю порученную ему работу). Также и в цеху могут одновременно работать сразу несколько рабочих над одним или разными делами. Те триггера, которым ещё предстоит вступить в дело или уже выполнившие свою работу, называются неактивными — как отдыхающие рабочие в курилке.

Как в производстве есть промежуточные результаты — полуфабрикаты и готовые детали, так и в скриптовой машине есть их полный аналог — это ячейки для хранения чисел и текстов, называемые переменными. Там, к примеру, может храниться количество танков противника или название группы боевых единиц, нужные для определения выигрышного положения дел. Причём следует иметь в виду, что часть таких ячеек доступна абсолютно всем триггерам (их названия начинаются с двух символов @@), а часть — только одному триггеру, где в них есть необходимость (их названия начинаются с одиночного символа @). Это схоже с ящиком с запчастями, доступным для всего цеха и с бардачком с необходимым инвентарём и деталями у конкретного рабочего, нужным только ему.

В соответствии с этим производим разбиение (по-научному декомпозицию) задуманного сценария на составные части. В нашем случае они достаточно просты — необходимо контролировать следующие ситуации:

  1. Наличие более 9 наших и менее 3 вражеских комбатантов в области деревни. Если это случилось — мы победили;
  2. Наличие в нашем распоряжении менее 10 комбатантов. Если это случилось — мы проиграли;
  3. Наличие в нашем распоряжении менее 15 комбатантов и ни одного исправного танка с экипажем. Если это случилось — мы получаем подкрепления;
  4. Невозможность стрельбы из основного оружия вражеского танка «Крузейдер» с 40-мм пушкой. Если это случилось — противник получает подкрепление.
  5. Наличие хотя бы одного нашего комбатанта к началу прохода с обороняющими его противотанковыми пушками и пулемётами. Если это случилось — мы получаем соответствующее сообщение;
  6. Наличие хотя бы одного нашего комбатанта к началу прохода с обороняющими его «Крузейдерами». Если это случилось — мы получаем соответствующее сообщение;

Каждый пункт — это триггер, в миссии «Forza, Carristi!» они имеют следующие названия:

  1. WinCheck — проверка выигрыша (от английских слов Win - выигрыш и Check — проверка);
  2. FailCheck — проверка поражения (Fail - провал и Check — проверка);
  3. Reinforcement — подкрепление нашим силам (Reinforcement - подкрепление);
  4. Brit_Daimler — подкрепление в виде «Даймлеров» вражеским силам (British Daimlers - британские «Даймлеры»);
  5. Warning_1 — 1-е предупреждение от майора Ферретти (Warning — предупреждение);
  6. Warning_2 — 2-е предупреждение от майора Ферретти.

И в единое целое их связывает триггер init, который автоматически выполняется скриптовой машиной игры сразу после загрузки сценария. Рассмотрим их теперь по отдельности. Однако за подробным описанием синтаксиса и возможностей того или иного оператора всё равно следует обратиться к соответствующему руководству, прилагаемому в электронном виде к игре.

init

// 1. Армия ИГРОКА
Set @@army = 1
// 2. Армия КОМПЬЮТЕРА
Set @@enemy_army = 2
// 3. НЕЙТРАЛЬНАЯ армия
Set @@neutral_army = 3
// 4. Армия СОЮЗНИКА
Set @@allied_army = 4

Set @disposition = "Initial_Area"
ActivateDispositionMode ( @disposition )

ChangeFogOfWar ( ENABLE ) 
SetWorkArmy ( ARMY , @@army )
// Сообщаем игроку о задании
QuestActivate ( "MainTask" , "Luigi" , 15000 )
PingShow ( "Ping_01" , 1 , POINT , "Ultimate_target" )
RunTrigger ( "FailCheck" )
Delay ( 5000 )
RunTrigger ( "WinCheck" )
RunTrigger ( "Reinforcement" )
RunTrigger ( "Warning_1" )
RunTrigger ( "Warning_2" )
RunTrigger ( "Brit_Daimler" )
Halt

Поскольку в сценарии есть управляемые игроком силы, а также управляемые искусственным интеллектом союзные, вражеские и нейтральные (взятые в плен британские солдаты), то в редакторе миссий организованы четыре армии с соответствующими характеристиками враждебности друг к другу, каждой из них при создании автоматически присваивается свой номер. Конечно, его можно указывать каждый раз в нужном месте, но гораздо лучше создать для каждой армии свою переменную, в которой он хранится и используется при надобности. Да, вместо числа 1 при наборе кода надо будет печатать @@army, но, представим, что из-за каких-нибудь правок в редакторе номер армии сменится, а задействован он в доброй полсотне мест в самых разных триггерах. И придётся их выискивать, править, с высокой вероятностью при этом можно забыть что-то исправить или изменить число 1, которое к номеру армии вообще отношения не имеет. Чтобы избежать этого, везде, где требуется номер армии, используем соответствующие имена переменных, при необходимости его изменения достаточно исправить его только в определении оператором Set и всё снова заработает. Отметим, что эти переменные должны быть доступны абсолютно всем триггерам и потому их имена начинаются с @@. Первые четыре оператора Set здесь задают соответствующие переменные и устанавливают для них числовые значения номеров армий, выданные редактором миссий.

Организация переменной @disposition (обратите внимание на один @ в имени, поскольку эта переменная нужна только в триггере init и нигде больше) нужна для указания на заранее определённую в редакторе миссий прямоугольную область Initial_Area, где игрок может расположить нужным образом его стартовые силы перед выполнением задания. Оператор ActivateDispositionMode переводит игру в режим расстановки сил и выполнение скрипта init. После подтверждения игроком готовности его выполнение будет продолжено.

Оператор ChangeFogOfWar с параметром ENABLE включает «туман войны» (без него нереалистично, но очень удобно проводить отладку сценария), оператор SetWorkArmy выбирает армию, с которой будут проводиться последующие операции. В нашем случае — армию игрока, о чём свидетельствует аргумент в виде переменной @@army. Все задания командования с их описаниями, реакцией на успешное выполнение или провал определяются заранее в редакторе кампаний под своими кодовыми обозначениями, в миссии их можно соответственно выдать, отменить, пометить выполненными или проваленными. В самом начале, естественно, мы должны получить задание и это выполняется оператором QuestActivate. В нашей миссии задание (оно единственное и проходит под кодовым обозначением MainTask) выдаёт майор Луиджи Ферретти (соответственно его позывной Luigi ) и оно будет отображаться на экране 15 секунд (отсюда число 15000 - временные интервалы в скриптовой машине почти всегда задаются в миллисекундах). В дополнение к оповещению о новом приказе оператор PingShow покажет на карте и мини-карте маркер местонахождения деревни-объекта наступления (белая цифра в красном кружке). Этот маркер будет иметь кодовое обозначение Ping_01, цифра 1 — то, что будет отображаться белым цветом в красном кружке, ключевое слово POINT - указатель на заранее определённую в редакторе миссий точку с названием Ultimate_target. На обоих картах на месте этой точки и появится белая единица в красном круге с тонкой чёрной окаёмкой.

Далее нам надо запустить все ранее упомянутые триггера, ответственные за решение своих частных задач. Это делается последовательностью операторов RunTrigger с указанием имени нужного триггера, запускаемого в параллель с уже работающими триггерами (вызов рабочих-помощников из курилки, возвращаясь к нашей аналогии). Единственная тонкость — пятисекундная задержка (оператор Delay) между запуском триггера FailCheck и WinCheck. Это некоторая мера предосторожности, оставшаяся от процесса отладки. Здесь она не нужна, когда все силы заранее расставлены по местам. Однако иногда вражеские боевые единицы находятся в скрытом состоянии и с началом сценария «вбрасываются» в него. Этот процесс занимает какое-то время и если поставить вызов проверки на победу сразу же после вброса, то может получиться так, что первый опрос численности сил противника опередит их появление на карте (не забываем об одновременности работы всех активных триггеров) и игрок получит мгновенный выигрыш. Задержка является одним из методов борьбы с этим нежелательным явлением. Однако, повторимся, в рассматриваемом сценарии такого нет, а потому задержка не нужна, хотя ничему и не вредит.

И, наконец, оператор Halt завершает работу триггера init, оставляя в работе шесть других триггеров. Вновь возвращаясь к производственной аналогии, триггер init выполнил работу мастера, предоставил необходимое сырьё для работы и распределил людей по задачам. И, сочтя на этом свою миссию завершённой, удалился.

FailCheck

SetWorkArmy ( ARMY , @@army )
Set @friends = GetNUnits ( ARMY , @@army , HUMAN , CREW )

If ( @friends < 10 ) Then
  DestroyTrigger ( "WinCheck" )
  QuestFail ( "MainTask" , "Luigi" , 15000  )
  Delay ( 15000 )
  MissionFail ( )
  Halt
Endif
Delay ( 1000 )

Этот триггер ответственен за обработку условия поражения. Уже обсуждавшийся выше оператор SetWorkArmy устанавливает для последующей обработки силы, подконтрольные игроку. Следующий оператор Set помещает в переменную @friends число всех комбатантов в армии игрока (солдат и членов экипажей техники, вне зависимости от их местонахождения внутри или вне своих машин или орудий). Последнее получается путём вызова функции GetNUnits с указанными аргументами. А далее оператором If-Then-Endif проверяется условие того, что число наших людей меньше десяти. Если это так, то оператор DestroyTrigger останавливает триггер WinCheck, ответственный за обработку условия победы, всё, победы не видать, оператор QuestFail отмечает задание под кодовым обозначением MainTask проваленным, реплика майора Ферретти будет отображаться на экране 15 секунд (см. выше в описании триггера init обсуждение оператора QuestActivate - его аргументы те же, что и у QuestFail). Оператор задержки Delay даёт нам возможность прочитать сообщение и осознать тяжесть поражения, а оператор MissionFail окончательно завершает игру поражением. Хотя после этого все триггера будут остановлены, останов посредством Halt ничему не вредит и только подчёркивает конец работы триггера.

Если наши силы насчитывают десять или более комбатантов, то ничего не происходит. Триггер даёт секундную задержку для обеспечения работы другим триггерам (во время исполнения любых других инструкций, отличных от Delay он сильно нагружает процессор) и начинает свою работу сначала. Так уж устроена скриптовая машина «Искусства войны» — если встретился конец кода триггера, а оператора останова Halt — нет, то он вновь начинает исполняться со своей первой строки. Таким образом, при явном отсутствии останова триггер выполняется до выхода из боя или завершения битвы тем или иным образом.

WinCheck

SetWorkArmy ( ARMY , @@army )
Set @friends = GetNUnitsInArea ( ARMY , @@army , HUMAN , "Village_Area" , CREW )
SetWorkArmy ( ARMY , @@enemy_army )
Set @foes    = GetNUnitsInArea ( ARMY , @@enemy_army , HUMAN , "Village_Area" , CREW )

If ( @friends > 9 AND @foes < 3 ) Then
  DestroyTrigger ( "FailCheck" )
  QuestWin ( "MainTask" , "Luigi" , 15000  )
  PingHide ( "Ping_01" )
  MissionWinButton ( "Activate" )
  Delay ( 120000 )
  MissionWin ( )
  Halt
Endif
Delay ( 1000 )

Этот триггер ответственен за обработку условия победы. Сначала оператор SetWorkArmy устанавливает для последующей обработки силы, подконтрольные игроку. Следующий оператор Set помещает в переменную @friends число всех комбатантов в армии игрока (солдат и членов экипажей техники, вне зависимости от их местонахождения внутри или вне своих машин или орудий) в прямоугольной области деревни с кодовым обозначением Village_Area, определённой заранее в редакторе миссий. Это число получается путём вызова функции GetNUnitsInArea с указанными аргументами. То же самое делается и по отношению к противнику — оператор SetWorkArmy устанавливает для последующей обработки силы, подконтрольные враждебному нам ИИ, число его комбатантов в области деревни записывается в переменную @foes.

А далее оператором If-Then-Endif проверяется условие того, что число наших людей больше девяти, а вражеских — меньше трёх. Если это так, то оператор DestroyTrigger останавливает уже триггер проверки поражения FailCheck, оператор QuestWin отмечает задание под кодовым обозначением MainTask выполненным, реплика майора Ферретти будет отображаться на экране 15 секунд (аргументы те же, что и у QuestActivate или QuestFail). Также, как подтверждение выполненного задания, оператор PingHide убирает маркер на карте и мини-карте — задача выполнена! Для желающих поскорее отпраздновать победу оператор MissionWinButton отображает надпись Cражение выиграно и кнопку Завершить бой, а для тех, кто хочет жестоко добить оставшиеся силы врага оператор задержки Delay даёт ещё две минуты игрового времени. Оператор MissionWin окончательно завершает игру победой. Остальное полностью идентично триггеру FailCheck.