Перевод статьи Wrye, посвященной терминологии моддинг-сцены Обливиона и той путаннице, которая возникает в речи об объектах, референсах и записях.
Модули, Записи, FormID
Моддинг с помощью CS означает создание и редактирование модульных файлов (т.е. esp\esm файлов). Есть и другие пути заниматься моддингом (создавать\редактировать ресурсы – меши, текстуры, звуки, речь; редактировать INI и файлы XML, и т.д.), но сейчас речь идет именно о модулях.
Модульные файлы (Module files) — это коллекции записей (records). Разные типы записей относятся к различным типам вещей: звукам, расам, помещенным объектам («референсам»), уровневым спискам (leveled lists) и т.п. Хотя различные типы записей соответствуют различным аспектам игры (к примеру, весу стрелы, позиции Х у помещенного на локацию объекта, ID у звука открытия двери), на базовом уровне они все равно относятся к одной и той же категории – запись.
Все записи имеют уникальный FormID. Многие (но не все) записи также имеют и EditorID. FormID – это 8 цифр в 16-ричной системе счета (к примеру, 00030FDC). Editor ID – это короткие текстовые строки (состоят только из букв и цифр, без пробелов или особых знаков). Записи помещаются в Окно Объектов в CS, их EditorID находится в первом столбце, а formID во втором столбце (по умолчанию этот столбец очень узок, так что его и не увидишь, просто нужно расширить его, зажав левой кнопкой шапку и перетащив вправо). Точно также они расположены и в окне Просмотра Ячейки (Cell View): левое окно содержит список ячеек, ранжированный по их EditorID, а FormID – чуть правее. А в правом окне располагаются помещенные в выбранную ячейку объекты. Именно здесь есть один нюанс: если у референса не было EditorID, то вместо него будет автоматически помещен EditorID не референса, а базового объекта этого референса.
Объект Vs. Объект
Эта секция может сбивать с толку. Возможно, её стоило бы пропустить при чтении. Посыл её в том, что, когда мы говорим о моддинге, мы говорим о «записях», а не об «объектах». По крайней мере в большинстве случаев. Ладно, поехали…
Есть большая проблема с тем, как пользуется слово «объект» в дискурсе моддинга. Проблема в том, что есть целый спектр одинаково очевидных применений этого слова, при этом взаимоисключающих. Я рассмотрю все из них и попытаюсь разрешить эту проблему, отбросив часть стандартной терминологии.
Играя, мы воспринимаем слово «объекты», как относящееся к тому, что мы встречаем в игровом мире: будь то камни, стены, существа, ножи или предметы в сундуке. Работая в окне рендера в CS, мы воспринимаем «объекты» в почти том же самом духе, просто к ним добавляются зоны коллизии, помещенный туман, и тому подобное, но для нас все они тоже «объекты».
Но в CS эти вещи называются «референсами», а «объектами» он называет предметы, размещенные в Окне Обьектов (Objects Window). А вот для программиста объекты – дискретные куски информации, то есть лишь умственные концептуализации (internal representations) любых записей (классы, фракции, базовые объекты и референсы), которые и будут восприниматься как «объекты».
Поэтому я собираюсь применять слово «объект» как можно реже. Когда я использую его, я имею ввиду то, что игроки видят в игре, то есть вещи в игровом мире. Для моддинга, когда я хочу сказать о той или иной сгруппированной информации (data chunks) в целом, я буду использовать слово «записи». И еще, когда я говорю о том или ином типе записи (будь то «ячейка», «фракция», «оружие», «референс» и т.п.), будем считать, что я использую сокращение от «ячейковая запись» (cell record), «фракционная запись» (faction record), «оружейная запись» (weapon record), «референтная запись» (reference record), и т.п.
Держа всё это в голове, я попытаюсь разложить всю терминологию CS несколькими способами:
- вместо «базового объекта» я скажу «базовая запись».
- вместо «переменной референса» я скажу «переменная записи» (record variable).
Референсы
Референсы основаны на базовых записях (они же «Базовые объекты» в CS). Базовые записи – прототип референса. Базовые записи могут иметь тысячи референсов (например, стандартные контейнеры), некоторые – только один референс (большая часть мирных и уникальных НИПов).
Большая часть записей в игре – это референсы. (просто представьте: сотни ячеек, со стенами, камнями, деревьями, существами – всё это считается). Хоть теоретически каждому референсу можно придумать EditorID, никто этого не делал. Поэтому в Oblivion.esm у большей части записей нет своего EditorID.
К тому же, кроме референсов, есть и другая группа записей без EditorID – это базовые записи, создающиеся в процессе игры. EditorID нужны только моддерам, а не CS или игровому движку, оба из них работают с FormID. Так что все алхимические зелья, заклинания, зачарованные броня и оружие, созданные в игре игроком, не нуждаются в том, чтобы их вручную правил человек и потому не имеют EditorID.
Референсы еще и могут иметь референса «родителя». Родительские референсы используются для двух целей: 1) создание цепочки включенных\выключенных состояний референсов (так, появление Врат Обливиона контролируется именно таким способом), 2) связывание референсов, чтобы взаимодействие между ними регулировали скрипты (ловушки триггерятся через натянутые веревки, чей скрипт вызывает ловушку). Замечу, что для ситуаций с включением\выключением родительский референс действительно действует как «родитель», а вот в случае с ситуациями, подобной устройству ловушек, всё имеет обратный смысл: «дочерний» референс контролирует референс «родительский».
Базовые Записи
Базовые Записи выступают и «основой» (“base”) для предметов в контейнерах. Есть некоторая сложность в описании таких предметов – у них то есть, то нет референсов, они либо сами ими являются, то не являются, но большую часть времени они НЕ референсы. Чуть ниже будет об этом.
К тому же, замечу, что не все записи, отображающиеся в Окне Объектов, базовые. Например, текстуры, звуки, вода – их записи используются другими записями, но сами они никогда не выступают в качестве базовых. Между ними и другими типами записей (ячейками, регионами, расами и т.п.), редактируемых только в меню, нет особого различия. Почему же они тогда расположились в Окне Объектов? Не знаю. Возможно, это наследие предыдущих игр серии или просто своевольное решение программистов Бесезды.
Vs. Программистская Терминология
Интерлюдия для сбитых с толку программистов…
Программисты воспримут терминологию CS как какую-то мешанину. CS использует термины «объект», «родитель» и «референс» в своем собственном ключе, отделенном от программистского понимания этих слов.
- в программировании «референс» - по сути указатель (pointer) на другую структуру данных (data structure), но в CS «референс» это комплексная структура данных (complex data structure), больше «Объект», чем «референс». Ближайший аналог для программистского значения референса в CS это FormID.
- в программировании объект –это обычно экземпляр класса (instantiated class). Ближайший аналог для него в CS – это… «референс»!
- в программировании класс – определение типа структуры данных (definition of a type of data structure), экземпляр которого можно создать. Ближайший аналог для него в CS – это базовый «объект»!
- в программировании «родитель» экземпляра ("parent" of an instance) — это класс для экземпляра. А вот в CS родитель референса (parent of a reference) – это другой референс, скорее «предыдущее» звено в цепочке взаимосвязей.
Так что, программисты, забудьте всему, чему вас учили, когда дело идет о CS! А теперь возвращаемся к нашей дискуссии..
Динамический Контент
Динамические записи и предметы – это записи и предметы, сгенерированные в ходе игры. В оригинальном Обливионе большая часть этой категории – динамические референсы (спавнящиеся существа) и динамические предметы (объекты из контейнеров и инвентарей акторов). Вдобавок, игрок может создавать такие предметы, например зелья, заклинания, зачарованные предметы. Плюс к этому, есть парочка особых записей, таких как статуя игрока в Кватче. Кроме этого, OBSE может генерировать широкий спектр динамических базовых записей.
Пример: когда актор спавниться на точке спавна, каждый из таких акторов – динамический референс. Их броня, оружие, инвентарь и предметы в инвентаре после их смерти – динамические объекты.
Пример: когда игрок создает зачарованную брони на алтаре зачарования, две динамических записи создаются – одна для зачарования, другая базовая запись – для брони. Вдобавок, экземпляр такой базовой записи (и зачарования, и брони) добавляются в инвентарь игрока.
Динамические записи (референсы, зачарования, заклинания,т.п.) «принадлежат» к файлу сохранения, а не к моду (esp\esm), поэтому их formID всегда начинаются с FF. Другими словами, если видите formID записи в игре, начинающейся с FF, то перед вами динамическая запись.
Динамические Предметы
Как было сказано, динамические предметы (dynamic items) не всегда ассоциируются с референсами, они не имеют референса, пока находятся в инвентаре, и всегда динамически присваивают новую запись каждый раз, когда вынимаются из инвентаря и выбрасываются в мире игры. В пику не-динамическим предметам, то есть предметам, которые были прямо помещены в игровой мир. Не-динамические предметы всегда отсылают к базовому, созданному модом, референсу, независимо от их перемещений по инвентарям, контейнерам и ячейкам.
Так что же происходит со временным референсов динамического предмета после того, как его подобрали? В большинстве случаев он незамедлительно удаляется из файла сохранения. В действительности (после последнего патча игры) formID для этого динамического референса используются снова (recycled). То есть, будь вы в изолированной ячейке, выбросьте вы клинок, а затем подберите его и бросьте теперь щит, вы обнаружите, что динамический референс щита будет иметь тот же formID, что и у выброшенного, но подобранного клинка.
Однако иногда референс не удаляется из файла сохранения. (неудаляющееся, кажется, связано с переменной в скрипте, который либо указывает на референс, либо привязан к референсу). В таких случаях, референс продолжает существовать в файле сохранения, но не виден в игровом мире, и больше не связан с предметом в инвентаре. Если же предмет снова выброшен из инвентаря, этот старый референс не будет связан с новым референсом, но вместо этого выброшенный предмет автоматически получит новый динамический референс, страдающий той же болезнью, то есть, остающийся в сохранении. То есть, эти остающиеся не у дел референсы не очищаются из файла сохранения, как это обычно бывает в течении цикла в три дня, когда весь мусор убирается, а противники респавнятся. В итоге, если в игре много (тысячи) таких референсов, они кратно увеличивают размеры сохранения. Такой феномен получил название референсы разбухания (bloat references).
Понимание того, как динамические предметы путешествуют между ячейками и контейнерами и просто между различными контейнерами, заключается в том, что они и не движутся вовсе – они копируются. То есть, оригинальный обьект копируется в новый контейнер или ячейку со всей своей информацией (здоровьем предмета, зачарованием, локальными переменными скриптов), а оригинальный предмет удаляется (референсы разбухания же просто маркируются как уничтоженные (marked as destroyed)).
Копирование наиболее очевидно, когда выбрасывается множество копий идентичных предметов (стрелы, клинки со 100% здоровьем). Если сбросить множество идентичных предметов (10 железных стрел), они не упадут как десять отдельных стрел, но будут собраны в единую стрелу с отображением количества стрел в одном референсе. В этом случае игра действительно будет считать 10 стрел как один предмет, а не как 10 штук отдельных референсов. (Но это происходит только для простых динамических предметов).
Так что в каком-то смысле длительное существование предметов – просто враки. То, что игрок видит как один и тот же клинок, вытянутый из сундука, помещенный в другой и т.п., это всего лишь последовательность копий объекта. Однако, коль скоро этот процесс копирования\удаления точно копирует состояние объекта, мы можем говорить о предметах как о длительно существующих сущностях, даже если их реальное существование приходит и уходит.
Скриптовая Терминология
Для скриптинга это имеет два важных значения:
- понимание того, какие типы записей требуют функции скриптов.
- понимание переменных записей и того, что в них можно хранить.
Переменные записи (Record variables) хранят записи или точнее, они хранят formID записей. Если formID идентифицируют записи, то краткое цифровое formID может выступать от лица собственно записи в вызовах функции. Это легко проверяется скриптом, если в нем вызвать «message "targetRef value: %X" targetRef». Эта строчка напечатает formID хранящееся в targetRef. Однако в функциях мы часто опускаем эту деталь и не говорим «getSelf возвращает formID актуального референса». Вместо этого мы говорим «getSelf возвращает актуальный референс». Этот вариант сказать проще, и он в общем-то отражает и суть первого варианта.
Часто можно увидеть, как переменные записи называют «переменными референса». Так сложилось, поскольку без OBSE, оригинальные функции, возвращающие записи, возвращают только записи референсов. Хотя переменные записи прекрасно могут хранить не-референсы, в них ничего кроме референсов хранить нельзя. (поэтому и переменные записей объявляются с ключевым словом ref вместо rec). Поскольку же OBSE позволяет переменным хранить целый спектр типов записей, с этого момента лучше называть их «переменными записи». (К сожалению, мы до сих пор привязаны к синтаксису с ключевым словом ref при объявлении переменных).
При присвоении записей в скриптах, нужно использовать либо переменную записи либо EditorID. Когда скрипт скомпилирован, EditorID сконвертируется в соответствующую formID, которую уже будет использовать игровой движок при работе со скриптом. Замечу, хоть и можно использовать в скриптах formID напрямую, избегая EditorID, это крайне нежелательно, поскольку: а) это труднее понять при чтении б) если к моду добавят еще один мастер-файл, конкретное formID, на которое мы ссылаемся, поменяется, а в скрипте останется старый formID. (А вот скомпилированный formID будет автоматически подтянут к своему новому значению, если будет добавлен новый мастер-файл).
Теперь снова о терминологии. В UESP Wiki в разных статьях одни и те же вещи называются по-разному. По мере понимания и наращивания знаний в области OBSE меняется и представление о том, что такое записи и как они могут быть использованы в скриптах.
В соответствии с этим лучше всего было бы применять такую терминологию:
- в скриптах, переменные записей должны именоваться так, чтобы отображать их использование в скрипте и не использовать ID. Референс и\или базовая запись должны использоваться по необходимости для прояснения сущности переменной. Однако описания функций должны всегда специфицировать, чего именно требует аргумент, референса или базового объекта.
- если функция принимает либо базовый объект, либо референс, используем bor (Я, правда, не уверен есть ли вообще такие функции, в кратких пометках документации OBSE сказано, что если указан базовый объект, то его следует указывать в другом месте, чем аргумент референса).
- Контейнер (Container) как имя переменной референса (всегда\обычно?) понимается, как референс контейнера, непися или существа. (Я не знаю ни одной функции, связанной с инвентарями, чтобы она работала только на референсах контейнеров, но не работала на существах или неписях).
Примеры скриптов:
ref companion
ref companionCell
set companionCell to companion.getParentCell
ref dagger
set dagger to player.placeAtMe wrTestDagger 1 100 0
message "Dagger formid: %X" dagger
ref tempRecord
set tempRecord to getSelf
set tempRecord to tempRecord.getContainer
set tempRecord to tempRecord.getParentCell
ref spell
set spell to GetPlayerSpell
player.Cast spell
ref self
ref myBase
set self to getSelf
set myBase to self.getBaseObject
ref opponent
set opponent to getCombatTarget
для функций «реф» («референс») и «базовый» объект должны использоваться чаще, чтобы прояснить природу аргумента.
(opponent) actorRef.getCombatTarget
(value) npcRef.getClothingValue
(bool) npcRef.getIsSex male|female
(actionRef) reference.getActionRef
(bool) [reference.]isFurniture [base]
(bool) [reference.]isLight [base]
(health) [itemRef.]GetObjectHealth [itemBase]
Другой подход – специфицировать возвращающееся значение и тип всех аргументов:
(opponent:rec) actorRef:rec.getCombatTarget
(value:short) npcRef:rec.getClothingValue
(bool:short) npcRef:rec.getIsSex male|female
(actionRef:rec) reference:rec.getActionRef
(bool:short) isFurniture base:rec
(bool:short) reference:rec.isFurniture
(bool:short) isLight base:rec
(bool:short) reference:rec.isLight
(health:long) [itemRef:rec.]GetObjectHealth [itemBase:rec]
Заметки о Скриптинге
Кое-какие пояснения по этой теме.
- getSelf примененная к отмодифицированному базовому предмету всегда возвращает оригинальной formID предмета.
- getSelf примененная к динамическому предмету всегда возвращает 0, даже если предмет находится в ячейке и имеет референс. Хоть вы и ожидаете, что она вернет formID своей динамической записи, находящейся в ячейке, функция явно создана так, чтобы возвращать 0 в таких ситуациях в целях безопасности.
- placeAtMe создает динамический референс и возвращает корректное formID. (прямо противоположное поведение команде getSelf).
Итоги
Запись: Основополагающая структура данных модуля файлов.
Поле: Параметры Записи (вес стрелы, дальность лука).
FormID: Основополагающий идентификатор для вех записей в виде 8 цифр 16-ричной системы счета.
EditorID: Строковый идентификатора, используемый многими (но не всеми) записями.
Референс: Любой объект в игровом мире – все объекты, помещённые в мир, и (иногда) объекты в контейнерах. (Большая часть референсов лишена EditorID, но все имеют FormID).
Базовый Объект: Запись, на основании которой создан тот или иной референс (к примеру, BarrelFoodLow).
Предметы (Items): это переносимые объекты, которые игрок воспринимает в игровом мире. Они имеют воспринимаемое время существования, которое обычно больше, чем время существования их базовых представлений данных (data representations).
Устаревшие Термины
Объект: сбивающий с толку термин. Лучше его не использовать. К сожалению, оно настолько в ходу, что избежать его практически невозможно.
Настоящий Референс (True Reference): лишний. Не используйте его.
HexID: Вместо этого FormID.
Комментарии