Существует немало весьма полезных тем и туториалов, которые помогают на начальном этапе изучения модульной системы и учат, как можно с ее помощью изменять любые детали Mount&Blade Warband, но мне так и не удалось найти ни одной, которая бы досконально объясняла все самые основы и азы операций и их синтаксиса, используемого в модульной системе. Вместо этого существует множество небольших одиночных тем-вопросов в разделе "Вопросов и ответов", которые помогают начинающим кодерам и модульщикам разобраться в принципах работы модульной системы. Итак, я, как, без сомнения, наиболее опытный и умелый член этого сообщества, который лучше всех сумееет точно, понятно и толково разъяснить основы синтаксиса модульной системы, решил, что не будет хуже, если я попытаюсь.
ОСНОВЫ
Список всех существующих операций и команд, которые могут быть использованы, содержится в файле header_operations.py. Каждая команда сопровождается комментарием (отмеченным символом #), в котором прописаны все аргументы данной операции, и возможно — краткое описание и подсказки по использованию. Каждая операция модульной системы имеет следующий вид:
1. Операция заключена в круглые скобки: (operation)
2. Сразу после операции должна стоять запятая: (operation),
3. Аргументы пишутся внутри скобок сразу после названия самой операции в строго заданном порядке и разделяются запятыми: (operation, argument, argument),
Помимо этого, в header_operations каждой операции присвоен ее собственный номер. Например, call_script = 1. Когда игра выдает сообщения об ошибке, они имеют вид SCRIPT ERROR ON OPCODE ##: … (Здесь и далее # - целое число. Прим. пер.) Номер OPCODE соответствует номеру операции в header_operations. Таким образом, если у ошибки OPCODE 1, то можно понять, что ошибка произошла при попытке вызвать операцию call_script.
Вообще все элементы, содержащиеся в любом файле МС, строго пронумерованы по порядку. Нумерация начинается с 0, и каждый следующий элемент имеет номер на 1 больше, чем предыдущий. Таким образом классы, предметы, триггеры, сцены и т. д., после компиляции МС в игровые текстовые файлы, сортируются по их этим самым номерам. Итак, если вы видите, что ошибка в “ trigger 0” - значит, имеется в виду первый триггер в соответствующем файле, и если ошибка в 4-ой строке, искать нужно 5-ю операцию в этом триггере.
ПеременныеЛокальные переменные (local variables) - “:variable” - переменные, названия которых начинаются с двоеточия ( : ) - это локальные переменные. Они существуют только в данном фрагменте кода, ограниченном скобками ( [ ] ) и они не могут быть вызваны непосредственно из других фрагментов кода иначе, кроме как быть переданными в качестве аргументов операции. Перед тем, как использовать переменную в коде, необходимо ее сперва инициализировать — присвоить ей какое-либо значение (0 или любое другое, подходящее по типу). Названия локальных переменных заключаются в кавычки ( “” ), и начинаются с двоеточия ( : ). Пример: “:agent_id”
Глобальные переменные (global variables) - “$variable” - переменные, названия которых начинаются со знака доллара ( $ ) - это глобальные переменные. Они могут быть доступны из любой части кода, из любого module_*.py файла после того, как были однажды инициализированы. По умолчанию значение созданной глобальной переменной равно 0. Названия глобальных переменных заключаются в кавычки ( “” ), и начинаются со знака доллара ( $ ). Пример: “$g_talk_troop”
Регистры (registers) – reg(#) или reg# - это переменные, используемые движком игры для временного хранения информации. Регистры глобальны, но могут быть доступны из других частей кода только пока существуют. В header_common содержатся объявления всех существующих регистров от reg0 до reg65.
Строки (strings) – s# - это специальная разновидность регистров, предназначенная для хранения не чисел, а строк текста. Строки, не прописанные заранее в module_strings (быстрые строки(quick strings)) начинаются с символа @. В header_common содержатся объявления всех существующих строковых регистров от s0 до s67.
Позиции (positions) – pos# - В отличие от предыдущих двух видов регистров, позиции хранят больше одного элемента информации; каждая позиция содержит координаты по осям X, Y и Z, а также углы поворота объекта относительно каждой из этих осей. В header_common содержатся объявления всех существующих регистров - позиций от pos0 до pos64, а также начинающийся с pos64 неустановленный массив позиций, используемых осадными башнями.
Постоянные, константы (constants) – constant – это постоянные значения, прописанные в module_constants, содержащие численное значение, присвоенное им при инициализации. Их значения не могут быть изменены никак иначе, кроме как напрямую в module_constants. Они могут быть доступны из любой части кода, из любого module_*.py файла. Константы не заключаются в кавычки.
Локальные и глобальные переменные, как и регистры, инициализируются с помощью команды (assign, <variable_name>, <variable_value>), Присваиваемое значение (<variable_value>) может быть другой переменной. Строки и позиции имеют свои собственные уникальные операции, прописанные в header_operations.
ПриставкиВ модульной системе существует возможность обращаться из одного файла к объектам из другого с помощью специальных приставок, добавляемых перед названием нужного объекта. Таким образом, если нужно, например, в module_mission_templates использовать какую-либо анимацию, то название этой анимации пишется следующим образом: anim_<название анимации из module_animations>.
module_animations: "anim_" - Анимации
module_factions: "fac_" - Фракции
module_info_pages: "ip_" - Описания
module_items: "itm_" - Предметы
module_map_icons: "icon_" - Иконки глобальной карты
module_game_menus: "menu_" - Меню
module_meshes: "mesh_" - Модели
module_mission_templates: "mst_" - Миссии
module_particle_systems: "psys_" - Визуальные эффекты
module_parties: "p_" - Отряды, партии
module_party_templates: "pt_" - Шаблоны отрядов, партий
module_pstfx_params: "pfx_" - Параметры окружающей среды
module_presentations: "prsnt_" - Игровые ситуации
module_quests: "qst_" - Квесты
module_scene_props: "spr_" - Сценовые объекты
module_scenes: "scn_" - Сцены
module_scripts: "script_" - Скрипты
module_skills: "skl_" - Умения, навыки
module_sounds: "snd_" - Звуки
module_strings: "str_" - Строки
module_tableau_materials: "tableau_" - Скрипты, обрабатывающие текстуры
module_troops: "trp_" - Классы персонажей
Module_scripts, скрипты, параметры, аргументы и т. д.Большая часть module_*.py файлов содержат в основном самостоятельный, изолированный код. Такие файлы как _troops, _skills, _items и т. д. представляют собой просто список различных игровых объектов с описаниями их свойств. module_game_menus содержит всевозможные меню в игре, их пункты, параметры и др. Presentations хранит данные, связанные с рисованием различных игровых ситуаций, и т. д.
Но module_scripts связан с каждым из этих файлов — он содержит «общие» функции, которые могут быть вызваны из любой части игрового кода. Поэтому предполагается, что скрипты из module_scripts будут вызываться, в основном, из других module_*.py файлов с помощью операции (call_script, <script_name>, <arguments>),
Чтобы упростить процесс использования скриптов, операция вызова позволяет передавать информацию из текущего фрагмента кода в сам скрипт без использования глобальных переменных с помощью так называемых аргументов или параметров. Эти аргументы или параметры могут быть локальными переменными, объявленными в данном фрагменте кода. Сам скрипт в module_scripts при вызове начнет свое выполнение с приема и сохранения переданных аргументов как локальных переменных в своем теле, чтобы иметь возможность использовать их при выполнении. Правда, скрипты не могут передавать информацию в код, откуда они были вызваны тем же способом... Но для этих целей обычно используются регистры. В коде, из которого вызывается скрипт, сразу после завершения выполнения скрипта значения этих регистров для удобства сохраняются в локальные переменные. В скрипт за одну операцию вызова можно передавать до 15 аргументов.
Пример:
Фрагмент кода из module_mission_templates:
//...объявления, инициализация...//
(assign, ":local_variable_1", 10), #объявление первой локальной переменной
(assign, ":local_variable_2", 5), #объявление второй локальной переменной
#вызов скрипта с передачей локальных переменных как аргументов
(call_script, "script_example_script", ":local_variable_1", ":local_variable_2"),
#получение результата выполнения скрипта из регистра
#и сохранение его в новую локальную переменную
(assign, ":local_variable_3", reg0), #local_variable_3 = 15
//...продолжение кода, работающего с local_variable_3...//
Скрипт из module_scripts:
# script_example_script
# Input: variable_1, variable_2
# Output: Sum in reg0
("example_script", [
#получение и сохранение переданных аргументов
(store_script_param, ":num1", 1), #теперь num1 равен 10
(store_script_param, ":num2", 2), #теперь num2 равен 5
#тело скрипта, выполнение действий над аргументами
(store_add, ":sum", ":num1", ":num2"),
#сохранение результата выполнения скрипта в регистр,
#чтобы он был доступен из исходного фрагмента кода в mission_templates
(assign, reg0, ":sum"),
]),
ТЕРМИНОЛОГИЯ
«Класс» и «Агент» (Troop vs Agent)Классы обозначают объекты, описанные в module_troops — классы персонажей игры, каждый из которых имеет какие-то свои уникальные свойства и иособенности. “trp_player” - это игрок, “trp_npc##” - это класс компаньонов, “trp_knight_#_##” - класс лордов, “trp_swadian_footman” - класс свадийских пехотинцев и т. д. Классы используются как шаблоны для создания персонажей с определенным набором свойств, причем каждый класс может содержать большое количество объектов. Например, солдаты в отряде, которые группируются по типу (классу), и которых в каждом типе(классе) может быть несколько.
Агенты же являются отдельными действующими лицами, например, в какой-либо сцене (будь то сцена сражения, таверна, улицы города или любая другая). Агенты создаются из классов-шаблонов из module_troops... Для игрока, компаньонов, лордов, но при этом каждый агент уникален, для любого персонажа создается его отдельный собственный агент. Так что даже для группы солдат одинакового класса все равно создаются несколько агентов одинакового класса, по одному для каждого солдата. Каждый агент на сцене уникален, но как только сцена закрывается, агенты перестают существовать вместе с ней.
Партии и Шаблоны Партий (Parties and Party Templates)«Партия» - это любой объект на глобальной карте, будь то отряд игрока, разбойники, их убежища, армии лордов или города, замки, деревни. Типы партий поделены на категории и прописаны в первом слоте партии “slot_party_type”, в котором должно содержаться число, соответствующее нужной категории. Эти категории прописаны в module_constants и начинаются с приставки spt_ (slot_party_type). Примерами spt_* являются spt_kingdom_hero_party (для армий лордов) или spt_castle (для замков).
Каждая партия имеет свой уникальный ID номер, который может использоваться в коде. Например, партия игрока - это “p_main_party”, у которой значение ID — 0. Точно определенные партии имеют постоянный, статический ID, как партия игрока и все города, замки и деревни. Все такие партии со статическими ID прописаны в module_parties. Их ID можно просмотреть в ID_parties.py после того, как МС будет скомпилирована.
Для всех партий, которые НЕ прописаны в module_parties, ID создаются динамически, по мере создания самих партий. Партии же создаются с помощью шаблонов партий по тому принципу, что агенты и классы (см. выше). Шаблоны партий прописаны в module_party_templates.py и их названия начинаются с приставки “pt_*”. Шаблон содержит в первую очередь список классов персонажей, находящихся в этой партии и численные интервалы, означающие количество представителей каждого класса в этой партии (точное количество выбирается случайным образом в пределах этого интервала). Шаблоны партий используются для динамического создания отдельных партий с помощью команды (spawn_party). Созданной партии присваивается ID номер, и после этого партия может быть классифицирована по значению slot_party_type и т. д. Также шаблоны партий могут быть использованы для уже существующих партий, а именно можно добавить шаблон партии со всеми входящими в нее персонажами к уже существующей партии в качестве «подкрепления».
СлотыСлоты, в сущности, - это способ сгруппировывать переменные по объектам (к которым относятся партии, классы, агенты, команды, фракции, сцены, сценовые объекты и шаблоны партий). Каждая партия, класс, агент, предмет и т. д. имеет определенный набор «слотов» - переменных, привязанных именно к этому объекту. Каждая такая переменная имеет название типа ”slot_item_SLOTNAME”, и они привязываются к каждому экземпляру объекта (предмета (item'a) в данном случае), что очень удобно, поскольку позволяет хранить свою собственную уникальную информацию для каждого отдельного объекта. Выглядят слоты как константы из-за того, что движок игры воспринимает их именно так, и имя каждого слота заменяет на число, прописанное в module_constants (slot_item_base_price = 53). Это значит, что чтобы узнать базовую цену данного объекта-предмета, движок должен взять значение 53-го слота, привязанного к этому объекту.
Итак, посмотрев на какую-нибудь операцию, работающую со слотами, как например (item_get_slot, “:base_price”, “:item_id”, slot_item_base_price), можно увидеть, что необходимо точно указывать, базовую цену какого именно предмета вам нужно узнать (“:item_id”). Но вы можете использовать цикл try_for_*, подставляя в каждой итерации цикла новое значение в “:item_id”, и перебрать таким образом любое количество предметов, чтобы произвести какие-то действия с ценой каждого из них, например.
Вы можете узнать цену какого-либо предмета, получив значение соответствующего слота с помощью операции item_get_slot, изменить ее, присвоив новое значение с помощью операции item_set_slot, проверять, не равна ли она какому-либо заданному значению с помощью item_slot_eq и многое другое.
Если вы знакомы с C-подобными языками программирования, то слоты работают примерно так же, как это:
slot_troop_spouse
spouse[troop1] = spousename
spouse[troop2] = spousename
spouse[trp_player] = spousenameДвижок игры этот код воспринимает так: troop_variable_30[troop1], troop_variable_30[troop2], troop_variable30[trp_player], поскольку slot_troop_spouse в module_constants имеет номер 30.
slot_item_base_price
baseprice[item1] = priceA
baseprice[item2] = priceB
baseprice[item3] = priceCДвижок игры этот код воспринимает так: item_variable_53[item1], item_variable_53[item2], item_variable_53[item3],... поскольку slot_ item_base_price в module_constants прописан под номером 53.
Пока слотам не присвоено никакое значение операцией *_set_slot, значения всех слотов по умолчанию равны 0.
УСЛОВИЯ И ЦИКЛЫ
Необходимо упомянуть, что игровой движок в любой конструкции “Если-Тогда” воспринимает любую операцию проверки условия как “ЕСЛИ”, и весь код, который следует за ней — как “ТОГДА”. Так как код выполняется построчно, то если его выполнение прервется при первом же неверном условии, то вообще весь последующий код не будет выполнен.
Поэтому для изоляции конструкций “Если-Тогда”, чтобы код мог нормально выполняться и прерывание его выполнения на одном неверном условии не значило прекращение выполнения вообще, используются так называемые “try-блоки”, использующие операции (try_begin), (else_try), и (try_end), Подробнее о использовании try-блоков ниже.
Операции - условия
(eq,<value>,<value>), Значение 1 = Значение 2?
(gt,<value>,<value>), Значение 1 > Значение 2?
(ge,<value>,<value>), Значение 1 >= Значение 2?
(lt,<value>,<value>), Значение 1 < Значение 2?
(le,<value>,<value>), Значение 1 <= Значение 2?
(neq,<value>,<value>), Значение 1 != Значение 2?
(is_between,<value>,<lower_bound>,<upper_bound>), Нижняя граница <= Значение < Верхняя граница?
Кроме этого любая операция, содержащая “_is_”, также является условием и может быть проверена на выполнение-невыполнение (true-false). Например, операция (agent_is_alive, <agent_id>), - это проверка, жив ли в данный момент агент <agent_id>. Если да, то есть если данный агент жив, то выполнение кода продолжается. Если же агент мертв, то выполнение данного try-блока прерывается.
Для того, чтобы проверить операцию-условие, содержащую “_is_” на НЕвыполнение (выполнение кода продолжается, если условие неверно), нужно сделать следующее:
(neq|<operation>),
В нашем предыдущем примере, (neq|agent_is_alive, <agent_id>), выполнение кода продолжится в том случае, если данный агент мертв (НЕ жив), и прервется, если он жив.
Управляющие операции
(try_begin),
(try_end),
(else_try),
(try_for_range,<destination>,<lower_bound>,<upper_bound>),
(try_for_range_backwards,<destination>,<lower_bound>,<upper_bound>),
(try_for_parties,<destination>),
(try_for_agents,<destination>),
<destination> - это переменная, значение которой будет изменяться в каждой итерации цикла в соответствии с текущей итерацией. Подробнее см.ниже.
Логические операторы И и ИЛИПоскольку, как уже упоминалось выше, движок воспринимает каждую операцию-условие в конструкции “Если-Тогда” как “ЕСЛИ”, а последующий код — как “ТОГДА”, логическое И может быть реализовано просто путем выписывания нужных условий одно за другим. Например, если нужно определить, является ли агент живой вражеской лошадью, то есть если агент жив И является лошадью И является врагом, то код будет выглядеть просто следующим образом:
(agent_is_alive,<agent_id>),
(neg|agent_is_human,<agent_id>),
(neg|agent_is_ally,<agent_id>),
Чтобы проверить выполнение одного ИЛИ другого условия, используется такая конструкция:
(this_or_next|<operation>),
По тем же самым причинам эти условия так же должны располагаться одно за другим. Например, так будет выглядеть проверка, равно ли значение переменной 1 ИЛИ 5:
(this_or_next|eq, “:test_variable”, 1),
(eq, “:test_variable”, 5),
Конструкция Если-Тогда-ИначеПоскольку по умолчанию операции-условия относятся ко всему коду, следующему за ними, возникает необходимость разделять код на фрагменты, чтобы каждое условие относилось только к коду своей секции и не затрагивало остальное.
Это можно сделать, используя “try блок”: код, который должен быть изолирован в отдельную секцию, заключается между операциями (try_begin), и (try_end), Такой способ может быть использован при создании простейшей конструкции “Если-Тогда”: первые строки после (try_begin), должны быть условием “Если”, и за ними должен следовать код, выполняемый, если это условие верно (“Тогда”). После чего должна стоять операция (try_end), означающая конец “try блока” - секции.
И в любой уважающей себя конструкции Если-Тогда должна быть операция Если-Иначе. В МС такой операцией является (else_try), Если внутри try блока какое-либо условие окажется неверно, то выполнение кода прервется на нем и продолжится с ближайшей операции (else_try),
Пример:
#Какой-то предыдущий код
(try_begin),
(eq, 1, 1), #Верно, переходим к следующей строке
(gt, 2, 5), #Неверно, переходим к ближайшему следующему else_try
#consequence operations here
(else_try),
(eq, 0, 1), #Неверно, переходим к ближайшему следующему else_try
#consequence operations here
(else_try),
(eq, ":favorite_lance", ":jousting"), # Возможно?
(assign, ":prisoners", 100),
(try_end),
#Последующий код, не имеющий отношения к этому try блоку
ЦиклыВ МС существует множество вариантов цикла For-Next. Простейший из них выглядит следующим образом: начинается с операции (try_for_range, ":iterator", <lower bound>, <upper bound>), (описание см. выше), затем идет тело цикла, состоящее из собственно тех операций, которые должны повторяться, и в конце стоит (try_end), означающий конец цикла.
Во время каждой итерации цикла изменяется значение переменной ":iterator". Значения этой переменной начинаются со значения, указанного в качестве нижней границы (lower_bound) и, пошагово изменяясь после каждой пройденной итерации, приближаются к значению, соответствующего значению верхней границы (upper bound), уменьшенного на 1. Эта переменная ":iterator" может быть использована в теле цикла try_for_range, но не может быть изменена, потому что это нарушит работу цикла — некоторые итерации могут быть пропущены. Если переменная ":iterator" не используется в теле цикла, то вместо нее в операции цикла НЕОБХОДИМО использовать переменную ":unused".
Пример:
(try_for_range, ":i", 0, 10),
(store_add, ":count", ":i", 1),
(assign, reg0, ":count"),
(display_message, "@{reg0}"),
(try_end),
#Этот код будет выводить на экран сообщения, считающие от 1 до 10
Также существует цикл (try_for_range_backwards, ":iterator", <lower bound>, <upper bound>), в котором стартовое значение итератора будет равно значению верхней границы — 1, а конечное — нижней границе. Не обращайте внимания на комментарии в header_operations, гласящие, что нужно поменять местами нижнюю и верхнюю границы. Это не так. Этот цикл очень удобен, например, для удаления элементов из списка (членов отряда, например), чтобы не возникало проблем с индексацией.
Заметьте, что ни верхняя, ни нижняя граница не обязаны быть просто числом: это с тем же успехом могут быть переменные с численным значением, присвоенным им в коде ранее. Примеры см. в следующем разделе.
Существуют еще два специальных варианта цикла try_for_*: (try_for_agents, <agent_id>), и (try_for_parties, <party_id>). Эти циклы предназначены для перебора всех агентов на сцене или партий в игре соответственно. Итератор в процессе прохода по циклу последовательно принимает значения каждого агента или партии, и для использования в теле цикла должен быть сохранен как локальная переменная.
Пример:
(get_player_agent_no, ":player"),
(agent_get_team, ":playerteam", ":player"),
(try_for_agents, ":agent"), #Цикл перебора всех агентов на сцене
(agent_is_alive, ":agent"), #Проверка, жив ли данный агент
(agent_is_human, ":agent"), #Если жив, проверка, является ли человеком (не лошадью)
(agent_is_non_player, ":agent"), #Если жив и человек, проверка, не является ли игроком
(agent_get_team, ":team", ":agent"), # Если жив, человек и не игрок, узнаем его команду
(eq, ":team", ":playerteam"), #Проверка, в одной ли команде этот агент с игроком
#Последующие операции, производимые с этим агентом, если он в одной команде с игроком
(try_end), #Переход к следующему агенту или если больше агентов нет — выход из цикла
Выходы из циклаВы, скорее всего, неоднократно столкнетесь с необходимостью перебирать в цикле всех агентов, чтобы найти одного конкретного агента, или же перебирать массивы любых других объектов, чтобы найти какой-то один конкретный. И после того, как искомый найден — нет больше необходимости проходить циклом по оставшимся — вы ведь уже нашли то, что искали. Нужно выходить из цикла. В МС нет никаких специальных операций для того, чтобы прекратить выполнение цикла, но существует несколько простых и эффективных способов сделать это вручную. Какой из них выбрать в данном случае, зависит от типа использующегося цикла try_for_*.
Способ 1: Изменение условия конца цикла.
- Для цикла try_for_range
Вы легко можете прервать цикл try_for_range, изменив в процессе выполнения верхнюю границу так, что она совпадет с нижней. Таким образом, когда цикл попытается выполнить следующую итерацию, он обнаружит, что итератор уже достиг верхней границы (итератор будет больше или равен верхней границе) и сразу же прекратит выполнение. Чтобы иметь возможность воспользоваться этим способом, нужно, чтобы верхняя граница была переменной, объявленной вне цикла, и необходимо убедиться, что при изменении этой переменной не произойдет потери данных, хранящихся в ней — создайте резервную копию этой переменной, чтобы не потерять ее исходное значение. Когда код был выполнен достаточное количество раз, когда нужный объект был найден и т. д., присвойте переменной верхней границы значение, равное значению нижней границы.
Пример:
#Проходим циклом (try_for_agents), по всем агентам
(assign, ":end", "itm_glaive"), #Установка верхней границы
(try_for_range, ":item", "itm_jousting_lance",":end"), #Перебираем все копья в игре
(agent_has_item_equipped, ":agent", ":item"), #Проверяем, есть ли у данного агента данное копье в инвертаре
(agent_set_wielded_item, ":agent", ":item"), #Если есть — агент берет его в руки
(assign, ":end", "itm_jousting_lance"), #Выход из цикла — приравниваем верхнюю границу к нижней — перестаем перебирать копья, мы ведь уже нашли, какое есть у агента
(try_end),
#Продолжаем работать с агентами в цикле (try_for_agents),
- Для цикла try_for_range_backwards
Тот же самый способ используется и для цикла try_for_range_backwards. Но здесь условием конца цикла является нижняя граница, поэтому для выхода нужно изменять не верхнюю, а нижнюю границу — она должна быть приравнена к значению верхней границы.
Пример:
#Код, в котором присваивается какое-то значение переменной ":troop"
(assign, ":array_begin", 0), # Установка нижней границы
(try_for_range_backwards, ":i", ":array_begin", 10), #Цикл от 0 до 10
(party_slot_eq, "p_main_party_backup", ":i", 0), #Проверка, равняется ли i-тый слот данной партии 0
(party_set_slot, "p_main_party_backup", ":i", ":troop"), #Если да, то присваиваем этому слоту значение переменной ":troop"
(assign, ":array_begin", 10), # Выход из цикла — приравниваем нижнюю границу к верхней — перестаем перебирать слоты, ведь нужный слот, равный 0, уже найден
(try_end),
Способ 2: С помощью проверки условия
- Для циклов try_for_agents и try_for_parents
В циклах try_for_agents и try_for_parents нет возможности менять верхние и нижние границы. Вместо этого для выхода из цикла используется проверка какого-либо условия (как правило, проверка на равенство двух значений), которая располагается в начале тела цикла, чтобы проверять данное условие перед каждой итерацию. Пока условие выполняется, цикл продолжает работать. Когда условие становится неверным, цикл продолжает работать, однако в его теле не происходит уже никаких действий, поскольку условие, стоящее перед всем основным телом, неверно и выполнение кода тела цикла прерывается в самом начале. Таким образом, цикл очень скоро закончит свою работу. Чтобы воспользоваться этим способом, инициализируйте новую переменную перед циклом. Затем, в самом начале цикла поставьте проверку условия, связанного с этой переменной. После того, как нужный код в теле цикла выполнился, значение этой переменной должно измениться так, чтобы условие, стоящее в начале, стало неверным и произошел выход из цикла.
Пример:
#Код, в котором присваивается какое-то значение позиции pos1
(assign, ":break_loop", 0), #Инициализация переменной для выхода из цикла
(try_for_agents, ":agent"), #Перебор всех агентов
(eq, ":break_loop", 0), #Проверка условия: равна ли переменная 0?
(agent_is_alive, ":agent"), #Если условие верно, продолжаем; жив ли данный агент?
(agent_is_non_player, ":agent"), #Если жив, проверка, не игрок ли он?
(agent_is_human, ":agent"), #Если жив и не игрок, проверка, человек ли он?
(agent_get_position, pos0, ":agent"), #Если жив, человек и не игрок, сохраняем его местоположение в pos0
(get_distance_between_positions_in_meters, ":distance_to_target_pos", pos0, pos1), #Сохраняем расстояние между pos0 и pos1 в локальную переменную
(lt, ":distance_to_target_pos", 10), #Проверка, находится ли агент менее, чем в 10 метрах от pos1
(assign, ":agent_at_target", ":agent"), #Если да, то сохранить ID этого агента в локальную переменную ":agent_at_target"
(assign, ":break_loop", 1), #Выход из цикла — изменяем значение переменной, чтобы при следующем прохождении цикла условие не было выполнено
(try_end),
#Выход из цикла произойдет, как только будет найден первый живой агент-человек, не являющийся игроком и находящийся менее, чем в 10 метрах от целевой позиции pos1
#Дальнейший код, работающий с найденным агентом
ДРУГИЕ ТЕМЫ
ТриггерыТриггеры из module_mission_templates.py (да и из module_triggers.py, если уж на то пошло) всегда записываются в следующем виде:
Первая часть: частота проверки условия выполнения триггера, "интервал вызова", либо в секундах, либо по определенному событию, ка например "ti_on_agent_hit"
Вторая часть: задержка в секундах между проверкой условий из блока условий и выполнением тела триггера. Не работает, если в качестве частоты проверки условия указано событие, как например "ti_on_agent_hit".
Третья часть: задержка «перезарядки» в секундах, то есть время между моментом завершения выполнения триггера и моментом, когда он снова сможет быть активирован. Специальная задержка перезарядки "ti_once" предназначена для одноразовых триггеров, которые могут выполниться лишь однажды, и после одного выполнения не могут больше выполняться вообще.
Четвертая часть: Блок условий, которые всегда проверяются при вызове триггера
Пятая часть: Тело триггера, основной код, который выполняется только в том случае, если выполнены все условия из блока условий и после задержки, указанной во вторй части триггера.
Важно: В отличие от всех остальных чисел в МС, интервалы задержек в триггерах НЕ обязаны быть целыми числами.
На практике это выглядит как-то так:
(10, 2, 60,
[
(eq, 1, 1),
],
[
(val_add, "$global_variable", 10),
]),
В этом примере интервал вызова составляет 10 секунд, поэтому триггер будет вызываться через каждые 10 секунд. Задержка между проверкой блока условий и выполнением тела равна 2 секундам, поэтому если все условия из блока условий выполнены, то спустя 2 секунды начнется выполнение тела триггера. Интервал перезарядки составляет 60 секунд, значит после каждого выполнения триггера пройдет минута, прежде чем он снова сможет быть активирован.
Здесь блок условий — простая проверка 1==1, которая всегда будет выполняться, поэтому тело триггера будет всегда срабатывать через 2 секунды после проверки условий выполнения триггера. В теле производится простейшее действие: к глобальной переменной прибавляется число 10.
Временные триггеры
Чтобы понять, что интервалы проверки/задержки/перезарядки во временных триггерах могут быть весьма непростыми штуками, рассмотрим следующий пример:
(2, 1, 3, [<условие, которое иногда выполняется, иногда нет>],
[
#некоторый код
]),Этот триггер, как и любой триггер с интервалом проверки > 0, начнет проверяться с 1 секунды миссии:
Секунда Событие
1 Триггер вызван; условие неверно — ожидание следующего вызова ( 2 секунды)
3 Триггер вызван; условие неверно — ожидание следующего вызова ( 2 секунды)
5 Триггер вызван; условие верно — включение задержки ( 1 секунда)
6 Тело выполнено - ожидание следующего вызова ( 2); включение перезарядки ( 3)
11 Триггер вызван; условие неверно — ожидание следующего вызова ( 2 секунды)
13 Триггер вызван; условие верно — включение задержки ( 1 секунда)
14 Тело выполнено - ожидание следующего вызова ( 2); включение перезарядки ( 3)
19 Триггер вызван....Итак, хотя интервал вызова равен 2 секундам, можно увидеть, что на самом деле вызов триггера не происходит каждые две секунды; вместо этого он происходит в 1, 3, 5, 11, 13, 19 секунды и т. д.
Если триггер должен вызываться точно «по расписанию», нельзя использовать ни задержку, ни перезарядку.
Два триггера с одинаковыми типами или интервалами
Если два триггера имеют одинаковые интервалы вызова (будь то в секундах или по условию ti_*), то порядок, в котором они будут выполнены, определяется текущим шаблоном миссии. Триггер, прописанный в шаблоне первым, первым и сработает, за ним уже выполнится второй с тем же интервалом и т. д. Это означает, что если у вас есть два триггера, срабатывающих по условию ti_on_agent_spawn, первым выполнится тот, который прописан в файле перед другим.
Триггеры в начале миссии
Логично предположить, что триггер ti_before_mission_start срабатывает перед тем, как сформирована сцена и появились агенты. Затем выполняются триггеры ti_on_agent_spawn – перед всеми остальными триггерами. Потом срабатывают триггеры ti_after_mission_start и все триггеры, интервал вызова у которых равен 0, поскольку на этом этапе запускается таймер миссии. Триггеры, срабатывающие по событию, не будут вызваны, пока это ti_* событие не произойдет. Остальные временные триггеры начинают вызываться где-то на первой секунде миссии, после выполнения всех триггеров ti_after_mission_start и с интервалами вызова 0.
Если у вас есть триггеры, которые не обязательно должны срабатывать сразу при запуске миссии, добавление в блок условий чего-нибудь в этом роде сильно поможет снизить нагрузку на процессор при запуске миссии:
(store_mission_timer_a, reg0),(gt, reg0, <время, прошедшее со старта миссии в секундах>),
Ни ti_before_mission_start, ни ti_after_mission_start не требуют использования "ti_once" в качестве перезарядки, поскольку они все равно и так будут выполнены только один раз.
Подводя итог, запуск миссии происходит в следующем порядке:
- Триггеры ti_before_mission_start
- Триггеры ti_on_agent_spawn
- Запуск таймера миссии
- Триггеры ti_after_mission_start и триггеры с интервалом вызова равным 0
- Временные триггеры (с 1 секунды миссии)
- Триггеры по событию
ДиалогиПервая часть любого диалога, с которой он начинается, выглядит примерно так:
"[ ...какой-то код... ]"
Это код, отвечающий за проверку условий при вызове диалога. Единственное и главное его назначение – проверять, выполнено ли данное условие или набор условий и возвращать результат. Каждый диалог при вызове проверяет это условие до тех, пока оно не будет выполнено.
Далее идут последующие части диалога, которые выполняются, только в том случае, если проверка условий пройдена успешно и активна именно их ветвь диалога. Поэтому, если вы хотите, чтобы какое-то одно действие выполнялось всегда, когда, игрок, например, встречает Главнокомандующего во вторник, но совершенно другое – когда они встречаются в среду, диалог может выглядеть как-то так:
#Начало диалога
[
#См. header_dialogs, чтобы узнать, какие команды могут быть использованы здесь помимо "anyone". Но любой диалог обязательно должен иметь строку "start",
anyone, "start",
#Начало стартового блока проверки условий
[
(eq, "$g_talk_troop", "trp_generalissimo_evilguy"),#Собеседник игрока – главнокомандущий?
(try_begin),
(eq, "$g_day_of_week", 2),#Сейчас вторник?
(assign, "$g_conversation_temp",1),
(str_store_string, s17, "@Так начинается разговор по вторникам"),
(else_try),
(eq, "$g_day_of_week", 3),# Сейчас среда?
(assign, "$g_conversation_temp",2),
(str_store_string, s17, "@ Так начинается разговор по средам "),
(else_try), ,# Если сейчас не вторник и не среда
(assign, "$g_conversation_temp",3)
(str_store_string, s17, "@ Так начинается разговор во все остальные дни "),
(try_end),
],
#Допустим, проверка пройдена и собеседник игрока – главнокомандующий. Теперь надо вывести нужный текст
"s17",
#Теперь нужно добавить ответные реплики игрока, пускай даже какие-нибудь простейшие типа «Уйти» для выхода из диалога
"player_response_or_responses_here",
#Специальный код, если нужен:
[],
#Конец диалога
],Но вообще создавать строки диалога, зависящие от классов, в одном блоке условий, как это было сделано выше — довольно плохой тон. Так делать не стоит, поскольку больше вероятность сделать какую-нибудь логическую ошибку. Лучше заключать создание каждой строки в отдельный блок условий, вот так:
[anyone,"start", [(eq, "$g_talk_troop", "trp_player_castellan"),], "Что вам угодно, {playername}?", "castellan_talk",[]],С помощью такого подхода можно сделать несколько вариантов начала разговора, используя несколько блоков условий:
[code][anyone,"start",
[
(eq, "$g_talk_troop", "trp_player_castellan"),
(eq, "$g_some_game_global", 1),
], "Что вам угодно,{playername}?", "castellan_talk",[]],
[anyone,"start",
[
(eq, "$g_talk_troop", "trp_player_castellan"),
(eq, "$g_some_game_global", 2),
], "А, это снова ты, {playername}! Подлый мерзавец,
Строки состояния
Строка состояния — это условное выражение, использующееся в строке для подстановки одной из двух данных строк в зависимости от значения определенного регистра. Если значение регистра не равно 0, подставляется первый вариант; в противном случае — если значение регистра равно 0 — подставляется второй вариант. Вари