Дневник разработчиков Stellaris №240 — Улучшения скриптов в 3.3

Привет и добро пожаловать в очередной дневник разработчиков о моддинге, выход которых за несколько недель до релиза уже стал традицией. Боюсь, скоро у нас закончатся возможности революционных изменений в скриптах, но пока ещё есть пара крутых улучшений, которые появятся в 3.3 и которые мы просто обязаны вам показать (чтобы вы могли опробовать их заранее в открытой бете).

Скрипт-значения

Начнём наш рассказ с полей веса. Я имею в виду нечто вроде этого:

weight = {
base = 1
modifier = {
factor = 2
some_trigger = yes
}
}

Мы обнаружили, что код, стоящий за такой структурой скрипта, имел несостыковки: был ряд отдельных реализаций в коде, которые не очевидным образом отличались для конечного пользователя, который пишет скрипт. Например, в одних можно было писать factor или add, а в других factor или weight. Местами всё вообще было очень плохо: иногда, если вы выставляли базовое значение равным 0, игра считала его за 1, и в таком случае (личности ИИ) factor воспринимался как add!

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

Несмотря на некоторые трудности в начале (однажды у меня на каждой планете в галактике появлились все доступные аномалии) это оказалось возможно, так что больше нам не нужно беспокоиться, что эти поля будут работать по-разному в разных местах. По сути, единственная разница осталась лишь в том, равно базовое значение 1 или 0.

После этого к системе можно было добавить ещё кое-что. Например, почему есть только factor, add и weight? Ведь существует много других математических операций. Так что мы добавили вычитание, деление, деление с остатком, минимум, максимум, модуль и округление (с помощью round, floor, ceiling и round_to). Мы также избавили вас от необходимости заключать их в modifier = {}, если он должен применяться всегда, а не по триггеру.

Но это было только начало. Ещё в 3.1 мы добавили возможность использовать trigger:<триггер> вместо числа в подобных местах, чтобы позволить более сложные вычисления (то есть оно бы брало результат триггера, например num_pops мог вернуть 32 поселения, а не абсолютное число). Хотя стоящий за этим код был не без изъянов. По сути, каждый раз, когда игра хотела посчитать значение trigger:num_pops, она брала строку «trigger:num_pops», проверяла, начинается ли она с «trigger:», если да, то отрезала это, а затем пыталась сделать триггер из остатков (и записывала ошибку в журнал в случае неудачи). К сожалению, всё это происходило не при запуске, а каждый раз, как игра встречала это в скрипте, при вычислении данных для подсказки, например, и делала это каждый кадр. В результате всё это было очень неудобно отлаживать и это влияло на производительность гораздо сильнее, чем должно было.

Это можно было улучшить. Поэтому в 3.3 мы сделали контейнер под названием CVariableValue, который может содержать такие объекты:

  • Целое или с фиксированной запятой (по сути обычное число).
  • Область видимости или цель события, то есть можно ссылаться на owner.trigger:num_pops.
  • Триггер.
  • Определение модификатора.
  • Строка переменной.
  • Скрипт-значение*

*К этому я вернусь позднее.

По сути, когда бы игра ни считывала скрипт-значение, она определяет это при запуске. То есть когда игре понадобится само значение, не придётся обрезать строку и получать необходимое — она сможет просто передать значение или вызвать триггер, которые были указаны. Так получилось, что эти изменения значительно упростили внедрение возможности использовать trigger:<триггер>, так что если есть ещё какие-то места, где желательно использовать такую запись, у нас больше нет отговорок, чтобы этого не делать (ой-ой).

Моддеры среди вас заметят, что попутно мы сделали возможным ещё кое-что. Перво-наперво, появилась отличная возможность вызывать modifier:<модификатор> так же, как вызывался бы trigger:<триггер>. По сути, если к поселению применяется модификатор на +20% к счастью, и вы используете modifier:pop_citizen_happiness, то получите 0,2. А ещё мы добавили скрипт-значения.

Эта идея пришла из более новых игр PDS, контент-дизайнеры которых могли насмехаться над нами с помощью гораздо более широких возможностей для вычислений в скриптах. По сути, широкими их делает возможность заменит значение ключом, который произведёт ряд вычислений по требованию. То есть my_script_value может равняться 57 + ( 24 * num_pops ) / num_colonies или чему-нибудь в этом духе. Упомянутые ранее изменения почти вывели нас на тот же уровень, поэтому мы добавили ещё одну новинку (названную в честь script_values и способную на многое, на что они способны в более новых наших играх, однако общего кода у них очень мало, так что тонкости работы могут отличаться).

Эти «скрипт-значения», по сути, являются полями веса, о которых я упоминал в начале дневника, но которые можно определить ключом в папке script_values, например:

leader_cost = {
base = 2
modifier = {
subtract = 6
num_owned_leaders > 5
}
modifier = {
add = trigger:num_owned_leaders
num_owned_leaders > 5
}
mult = 50
}

Затем мы можем ссылаться на это из любого места игры с помощью value:leader_cost и значение будет вычисляться по требованию. Мы уже нашли это очень полезным в улучшении игровых скриптов — теперь не только легче получать верные значения, но можно значительно сократить количество скопированных скриптов в полях веса (вес должностей, я иду!). Удобно ещё и то, что поскольку скрипт-значения считываются так же, скриптовые значения, мы можем скармливать им параметры, например value:my_value|PARAMETER|50| заставит использовать скрипт-значение my_value там, где любые появления $PARAMETER$ будут заменены на 50.

Даже со всеми этими изменениями в язык скриптов всё ещё можно было добавить пару улучшений. Первым стало добавление complex_trigger_modifiers в script_values и поля веса. По сути, это позволяет вам использовать значение триггеров, которые слишком сложны для использования через trigger:<триггер>. Можно привести такой пример:

complex_trigger_modifier = { #меньше миров => больше угрозы за их уничтожение
trigger = check_galaxy_setup_value
parameters = { setting = habitable_worlds_scale }
mode = divide
}

Это работает с теми же триггерами, которые работают с export_trigger_value_to_variable. Мы также добавили к ним несколько триггеров. Среди важных: все триггеры скрипт-листов count_x (например, count_owned_planet) и триггер distance.

Исчерпывающее руководство по скрипт-значениям прикреплено к этому сообщению (и в common/script_values). Честно говоря, количество возможностей, которые открывает для нас новая система, трудно переоценить. Например, в примере выше мы масштабируем стоимость лидера в зависимости от количества уже нанятых лидеров. Так же мы масштабируем бонусы единства с помощью памятников аборигенам в зависимости от количества взятых бонусов за стремление. Это длинный список и он будет продолжать расти с каждым вышедшим обновлением.

Перезапись модов

Скрипт-значения — не единственная тема на сегодня. Мододелы давно удивляются тому, что различные элементы игры по-разному относятся к перезаписыванию. Увы, пока это положение вещей сохранится, но мы добились определённого прогресса на этом направлении, и мне кажется, что вам будет интересно узнать о причинах.

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

  • Она полностью замещает существующую (используется последняя прочитанная).
  • Она замещает существующую, но с изъянами, например, если вы отдельно перезаписываете должности, то больше не сможете отсылаться к ним в модификаторах.
  • Вторая запись игнорируется (используется первая прочитанная).
  • Используются сразу обе записи (дублирование — не идеальный вариант).
  • Игра дописывает информацию к существующей записи (особый случай с on_action).

Так откуда разница в поведении? По сути всё сводится к тому, как база данных читает код на C++.

Как правило, когда игра сталкивается с файлом скрипта, она читает объет (например, miner = { }) и сохраняет его в соответствующей базе данных (например, должностей), к которой будет обращаться при необходимости. Многие из старых объектов, сохранившихся в неизменном виде ещё с выхода игры, вроде технологий и принципов, кодируются в качестве объектов в специально созданной базе данных. Поскольку код для чтения этой базы данных переписывается или копируется при каждом определении объекта, могут меняться как порядок чтения файла (от A до Z или от Z до A), так и подход к дублированию. В некоторых случаях такой подход оправдан, например, on_action действительно требуют к себе особого отношения, и мы хотим, чтобы мододелы могли использовать on_action без необходимости беспокоиться из-за контента самой игры. Это также распространяется на базы данных, сильно привязанные к коду, например, дипломатические действия, в случае которых нельзя просто добавить строку и ждать, что код поймёт, что с ней делать.

Но в большинстве случаев дело просто в техническом долге: сейчас мы куда лучше умеем кодировать базы данных. Последние несколько лет при добавлении нового объекта мы добавляем его в TGameDatabaseObject в TSingleObjectGameDatabase. Стандарный код TSingleObjectGameDatabase справляется с чтением объектов и не нуждается в копировании. Но самое главное с точки зрения мододелов, он делает перезапись через удаление существующего объекта и замену его на новый. В большинстве случаев модерам это подходит, но есть исключения: в случае должностей, районов, классов планет и ещё нескольких объектов это приведёт к поломке модификаторов. например, модификатор, добавляющий должности шахтёров, в итоге ничего не будет делать. По сути получится так, что должность создаст модификатор, который а) добавится в базу данных модификаторов и б) сохранится в определении должностей (не файле скриптов, а в результате прочтения такого файла), что позволит игре прикрепить эффект к этому модификатору (то есть добавить x таких должностей). Затем должность удаляется и заменяется новой. Аналогично создаются и модификаторы. Но теперь в списке модификаторов будет две записи с одинаковым ключом. Затем, когда игра сталкивается с таким модификатором при использовании файла скриптов, она открывает список, находит первое совпадение и полагает, что именно оно ей и нужно. Увы, сама должность считает, что к ней применяется второй модификатор. В результате модификатор можно смело считать нерабочим.

Но я могу сообщить хорошие новости — мы исправили эту проблему. Теперь эти объекты можно безопасно переписывать. Ну или по крайней мере перезапись больше не будет ломаться по этой конкретной причине, и модификаторы будут работать как надо. К чему такая осторожность? Если запись в одной базе данных меняет другую, то неосторожная перезапись может привести к проблемам. К счастью, подобное встречается редко, если не считать модификаторы. Надеемся, это пригодится мододелам, поскольку должности и районы чаще всего подвергаются изменениям.

Напоследок замечу, что поскольку TGameDatabaseObjects читается одними и теми же строками кода, мы оставили мододелам небольшой подарок: журнал ошибок, предупреждающий о перезаписях, в котором теперь будет указываться, что именно используется. Благодаря этому перезапись будет более понятной. Поэтому теперь при виде этого сообщения, вы точно поймёте, что делает игра:

[14:03:02][game_singleobjectdatabase.h:147]: Объект с ключом: miner уже существует и использует файл: common/pop_jobs/03_worker_jobs.txt line: 319

Также замечу, что мы продлили период сбора отзывов по открытой бете 3.3 до понедельника, 7 февраля. Сама версия останется доступной до выхода 3.3, и те, кто сейчас играет в эту версию, смогут продолжить свои партии после выхода обновления 3.3. Если вы ещё этого не сделали, оставьте свой отзыв по открытой бете в этой теме: https://forum.paradoxplaza.com/forum/developer-diary/stellaris-dev-diary-240-scripting-improvements-in-3-3.1508908/

Не пропустите новый эпизод Dev Clash 2022 в понедельник, 7 февраля, в 17:00 МСК на каналеhttp://twitch.tv/paradoxinteractive

На этой неделе у нас всё! На следующей неделе вернётся Eladrin и поделится своими мыслями об открытой бете и Dev Clash!

3468 views·4 shares