ОСНОВЫ
Список всех существующих операций и команд, которые могут быть использованы, содержится в файле 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_spousespouse[troop1] = spousenamespouse[troop2] = spousenamespouse[trp_player] = spousename
Движок игры этот код воспринимает так: troop_variable_30[troop1], troop_variable_30[troop2], troop_variable30[trp_player], поскольку slot_troop_spouse в module_constants имеет номер 30.
slot_item_base_pricebaseprice[item1] = priceAbaseprice[item2] = priceBbaseprice[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",[]],
С помощью такого подхода можно сделать несколько вариантов начала разговора, используя несколько блоков условий:
[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}! Подлый мерзавец, [b]Строки состояния[/b]Строка состояния — это условное выражение, использующееся в строке для подстановки одной из двух данных строк в зависимости от значения определенного регистра. Если значение регистра не равно 0, подставляется первый вариант; в противном случае — если значение регистра равно 0 — подставляется второй вариант. Вариант может быть пустой строкой. Люди, знакомые с программированием, могут понять, что строке состояния соответствует оператор «?:» в других языках программирования.[b]Использование строк состояния[/b]Строка состояния представляет собой выражение, заключенное в фигурные скобки «{ }» и выглядящее следующим образом: сначала идет название регистра, на основании значения которого и будет определяться выбор варианта, за ним следует символ «?», далее — вариант, при регистре, не равном 0, символ «:», вариант при регистре, равном 0.Простейший пример строки состояния:[code]город{reg0?а:}
Если reg0 не равен 0, вместо данного выражения в строку подставится «а», если равен — подставится «» - пустая строка. Поэтому, если reg0 не равен 0, то данная строка будет «города», а если равен - «город».
Сложные строки состояния
Строки состояния могут вкладываться одна в другую, создавая сложные динамически генерируемые строки. Например:
{reg6?Я:{reg7?Ты{s11}}}
Данная строка примет значение «Я», если reg6 != 0 (значение reg7 в этом случае не важно), “Ты”, если reg6= 0 и reg7 != 0, или же строки s11 (в данном случае содержащей имя лорда), если оба этих регистра равны 0. Это компактное и красивое решение, более эффективное, чем использование напрямую отдельного строкового регистра и громоздкое присваивание ему «Я», «Ты» или имени нужного лорда с помощью, например, try блоков или других подобных конструкций.
Однако сложные строки состояния стоит использовать, только если данная строка встречается в коде только однажды. В случае, если такая строка должна использоваться в коде несколько раз, будет как раз удобнее и лучше один раз создать отдельный строковый регистр и присвоить ему нужное значение с помощью try блока так, как это было описано выше, без использования строк состояния.
Строки пола
Строка пола, или как ее реже называют, строка чередования полов — это условное выражение, использующееся в диалогах для подстановки одной из двух данных строк в зависимости от пола персонажа, с которым происходит диалог (обычно игрока). Если этот персонаж мужского пола, то вместо данного выражения подставляется первый вариант, если женского — то второй. Это является одним из способов динамической генерации текста и используется, как правило, для правильного формирования обращений игровых персонажей к игроку.
Использование строк пола
Строка пола имеет чрезвычайно простую конструкцию: она состоит из двух текстовых строк, одна для мужчины, другая для женщины, разделенных слешем “/” и заключенных в фигурные скобки “{ }”.
«Добрый день, {сударь/сударыня}. Чем могу помочь?»
В случае, если персонаж, к которому обращено эта фраза, мужского пола, она примет вид:
«Добрый день, сударь. Чем могу помочь?»
Ну и если собеседник — женщина:
«Добрый день, сударыня. Чем могу помочь?»
Примечания
Используйте строки пола всегда, когда это возможно
Будьте особенно внимательны, когда пишете диалог для игрока или лорда, у которого может быть другой пол, нежели вы предполагаете. Если с помощью каких-либо сторонних модификаций или чисто случайно пол собеседника окажется не таким, каким вы предполагали при написании диалога, диалог будет несогласован. Поэтому если есть возможность, что собеседником может оказаться как мужчина, так и женщина, используйте строки пола для избежания ошибок.
Строки пола могут использоваться только с говорящим и слушающим персонажами
Обратите внимание, что строка пола относится к тому, к кому обращена реплика, а не к тому, кто ее произносит.
Чтобы слушателем был игрок, запись диалога должна начинаться так:
[anyone
Если слушателем должен быть другой игровой персонаж, то эта запись должна быть такой:
[anyone|plyr
Также помните, что строки пола учитывают пол только тех двух персонажей, которые непосредственно участвуют в диалоге (говорящий и слушающий). Если нужно определить пол какого-то стороннего, третьего персонажа, следует определить класс этого персонажа и воспользоваться функцией troop_get_type, чтобы определить его пол, и в зависимости от полученного результата выполнить какие-то действия.
Регистры
Регистры в основном используются для динамического отображения на экране чисел. Это происходит так: регистру присваивается нужное числовое значение, после чего регистр встраивается в отображаемую строку, используемую, например, в диалогах или сообщениях, выводимых на экран. Таким образом применение регистров является одним из четырех вариантов создания динамически генерируемых строк (наряду со строками состояния, строками пола и строковыми регистрами).
Использование регистров
Регистр, встроенный в строку, представляет собой следующую конструкцию: название регистра, заключенное в фигурные скобки «{ }». Когда строка выводится на экран, данное выражение заменяется числовым значением регистра. Имена регистров строго определены и имеют вид «reg*», где * - число от 0 до 65. Например данная строка содержит два регистра с ID 1 и 2 соответственно, значения которых будут выводиться игроку в данной строке:
Денег у вас в наличии: {reg1}^Общая стоимость товаров: {reg2}
Для изменения значения регистра используется операция (assign),
Примечания
Важно отметить, что регистры – не переменные; они предназначены только для отображения чисел на экране или их хранения. Все вычисления этого выводимого числа необходимо проводить с помощью локальных или глобальных переменных, и после чего только уже в регистр записывать окончательный ответ для вывода на экран.
Хотя для регистров и можно использовать арифметические и другие операции типа (store_add), (val_add), и т. д., следует избегать подобных ситуаций. С регистрами не должно проводиться никаких операций! Используйте для вычислений вместо регистров переменные — это сделает код гораздо более логичным, удобным и понятным.
Единственная функция, которая предназначена для работы напрямую с регистрами – (shuffle_range),
Строковые регистры
Строковые регистры — это специальные регистры, которые используются для хранения строк. Строковые регистры могут включаться в какую-либо строку, например, в часть диалога, в качестве одного из четырех способов динамической генерации текста (другие три способа — с помощью регистров, строк пола и строк состояния).
Использование строковых регистров
Строковый регистр должен быть вставлен в строку, будучи заключенным в фигурные скобки «{ }». Когда текст данной строки выводится на экран (в качестве диалога или сообщения и др.), строковый регистр заменяется строкой, в нем содержащейся. Заметьте, что все пробелы и любые другие символы внутри строкового регистра выводятся точно так, как были записаны внутри.
Вид имен строковых регистров строго задан: имя любого строкового регистра начинается с символа «s», за которым следует число от 0 до 67.
Как ты посмел усомниться в могуществе такого величайшего государства как {s43}?! Умри, {s5}!
Эта строка содержит два строковых регистра с номерами 43 и 5 соответственно. Перед тем, как эта строка будет выведена на экран в виде реплики диалога, выражения строковых регистров будут заменены строками, содержащимися в них, образуя таким образом связный текст. Соответственно, перед тем, как выводить эту строку в диалоге, необходимо присвоить этим строковым регистрам подходящие значения. В данном случае, вероятно, наиболее подходящими операциями для этого будут str_store_string_faction_name для s43 и str_store_string_troop_name для s5.
Управление строковыми регистрами
Управление строковыми регистрами происходит с помощью соответствующих операций, предназначенных специально для этого. Первый параметр в этих операциях — это всегда ID строкового регистра, значение которого должно быть изменено. Второй параметр, если он есть, - ID объекта, из которого будет получена строка, присваиваемая строковому регистру. Подобные операции легко узнать — их названия начинаются с приставки str_.
Флаги
Флаг — это «да-или-нет»-состояние, привязанное к своему числовому значению с помощью операции логического сложения (логического ИЛИ «|»). Название произошло от аналогии с семафором: когда флаг поднят, состояние активно, когда опущен — состояние неактивно. Флаг имеет два условных состояния, в одном из которых может находиться: «1» - активен, «0» - неактивен. Поскольку для хранения одного флага используется один бит информации, получается, что его значение можно проверять простейшей математической операцией, и это позволяет флагу занимать минимальное количество памяти, поэтому такая конструкция, несмотря на простоту, является наиболее удобной и надежной.
В МС все объекты, операции и переменные технически представляют собой просто числа, что позволяет просто добавлять флаги к ID объектов и других подобных конструкций, чтобы хранить о этих объектах дополнительную информацию, за которую эти флаги отвечают. Но только движок игры способен определить, содержит ли данный числовой ID какие-либо флаги и в зависимости от этого совершать какие-то действия.
Использование флагов
Синтаксис использования флагов довольно прост: чтобы добавить к объекту флаг, нужно после названия объекта через символ «|» написать название флага, например anyone|plyr. Таким образом можно добавлять любое количество флагов, просто так же ставя очередной символ «|» и приписывая название очередного флага после него. Каждый приписанный флаг добавляет один бит, содержащий его значение, к ID объекта.
Пример
party_tpl|pt_looters|plyr
Этот пример из module_dialogs.py ассоциирует данный диалог с шаблонной партией pt_looters и применяет к нему флаги party_tpl и plyr. Можно увидеть, что свойства получившегося диалога зависят от примененных флагов, но не зависят от порядка, в котором они прописаны. В итоге данный диалог происходит благодаря флагам party_tpl и pt_looters предназначен для разговора не с отдельным персонажем, как это было бы по умолчанию, а с партией, имеющей шаблон pt_looters, то есть с отрядом грабителей. Флаг plyr означает, что диалог будет обращен не к игроку, как это было бы по умолчанию, а к лидеру партии.